From 6ee9f4d4d2d305e66a534a5d609b83284c67a933 Mon Sep 17 00:00:00 2001 From: AllsVeam <101079338+AllsVeam@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:55:35 -0600 Subject: [PATCH] WEB-233: Improve the OIDC integration with 2FA (#2616) --- docker-compose.yml | 5 + src/app/app-routing.module.ts | 5 + src/app/app.module.ts | 25 +- .../authentication/authentication.service.ts | 26 +- src/app/core/http/cache.interceptor.ts | 2 +- .../core/shell/sidenav/sidenav.component.ts | 13 +- .../core/shell/toolbar/toolbar.component.ts | 16 +- .../login-form/login-form.component.html | 115 +++--- .../login-form/login-form.component.scss | 5 + .../login/login-form/login-form.component.ts | 24 +- .../add-role/add-role.component.ts | 11 +- .../edit-role/edit-role.component.ts | 14 +- .../view-role/view-role.component.ts | 11 +- src/app/users/users-routing.module.ts | 140 +++++-- src/app/users/users.module.ts | 13 +- src/app/web-app.component.ts | 32 +- src/app/zitadel/auth.guard.ts | 10 + src/app/zitadel/auth.service.ts | 381 ++++++++++++++++++ .../zitadel/callback/callback.component.html | 6 + .../zitadel/callback/callback.component.ts | 23 ++ src/app/zitadel/constants/coutry-codes.ts | 117 ++++++ src/app/zitadel/constants/languages.ts | 9 + .../change-password-dialog.component.html | 35 ++ .../change-password-dialog.component.scss | 8 + .../change-password-dialog.component.spec.ts | 24 ++ .../change-password-dialog.component.ts | 124 ++++++ src/app/zitadel/shared/shared.module.ts | 34 ++ src/app/zitadel/token.interceptor.ts | 64 +++ .../create-user/create-user.component.html | 205 ++++++++++ .../create-user/create-user.component.scss | 13 + .../create-user/create-user.component.spec.ts | 24 ++ .../create-user/create-user.component.ts | 353 ++++++++++++++++ .../users/edit-user/edit-user.component.html | 125 ++++++ .../users/edit-user/edit-user.component.scss | 3 + .../edit-user/edit-user.component.spec.ts | 24 ++ .../users/edit-user/edit-user.component.ts | 220 ++++++++++ src/app/zitadel/users/userZitadel.resolver.ts | 30 ++ src/app/zitadel/users/users.component.html | 47 +++ src/app/zitadel/users/users.component.scss | 11 + src/app/zitadel/users/users.component.ts | 126 ++++++ .../users/usersZitadel-template.resolver.ts | 26 ++ .../zitadel/users/usersZitadel.resolver.ts | 29 ++ src/app/zitadel/users/usersZitadel.service.ts | 129 ++++++ .../users/view-user/view-user.component.html | 96 +++++ .../users/view-user/view-user.component.scss | 14 + .../view-user/view-user.component.spec.ts | 24 ++ .../users/view-user/view-user.component.ts | 113 ++++++ src/assets/env.js | 13 +- src/assets/env.template.js | 9 +- src/assets/translations/cs-CS.json | 123 ++++++ src/assets/translations/de-DE.json | 123 ++++++ src/assets/translations/en-US.json | 123 ++++++ src/assets/translations/es-CL.json | 123 ++++++ src/assets/translations/es-MX.json | 123 ++++++ src/assets/translations/fr-FR.json | 123 ++++++ src/assets/translations/it-IT.json | 123 ++++++ src/assets/translations/ko-KO.json | 123 ++++++ src/assets/translations/lt-LT.json | 123 ++++++ src/assets/translations/lv-LV.json | 123 ++++++ src/assets/translations/ne-NE.json | 123 ++++++ src/assets/translations/pt-PT.json | 123 ++++++ src/assets/translations/sw-SW.json | 123 ++++++ src/environments/.env.ts | 4 +- src/environments/environment.prod.ts | 10 +- src/environments/environment.ts | 10 +- src/index.html | 11 + 66 files changed, 4425 insertions(+), 135 deletions(-) create mode 100644 src/app/zitadel/auth.guard.ts create mode 100644 src/app/zitadel/auth.service.ts create mode 100644 src/app/zitadel/callback/callback.component.html create mode 100644 src/app/zitadel/callback/callback.component.ts create mode 100644 src/app/zitadel/constants/coutry-codes.ts create mode 100644 src/app/zitadel/constants/languages.ts create mode 100644 src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.html create mode 100644 src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.scss create mode 100644 src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.spec.ts create mode 100644 src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.ts create mode 100644 src/app/zitadel/shared/shared.module.ts create mode 100644 src/app/zitadel/token.interceptor.ts create mode 100644 src/app/zitadel/users/create-user/create-user.component.html create mode 100644 src/app/zitadel/users/create-user/create-user.component.scss create mode 100644 src/app/zitadel/users/create-user/create-user.component.spec.ts create mode 100644 src/app/zitadel/users/create-user/create-user.component.ts create mode 100644 src/app/zitadel/users/edit-user/edit-user.component.html create mode 100644 src/app/zitadel/users/edit-user/edit-user.component.scss create mode 100644 src/app/zitadel/users/edit-user/edit-user.component.spec.ts create mode 100644 src/app/zitadel/users/edit-user/edit-user.component.ts create mode 100644 src/app/zitadel/users/userZitadel.resolver.ts create mode 100644 src/app/zitadel/users/users.component.html create mode 100644 src/app/zitadel/users/users.component.scss create mode 100644 src/app/zitadel/users/users.component.ts create mode 100644 src/app/zitadel/users/usersZitadel-template.resolver.ts create mode 100644 src/app/zitadel/users/usersZitadel.resolver.ts create mode 100644 src/app/zitadel/users/usersZitadel.service.ts create mode 100644 src/app/zitadel/users/view-user/view-user.component.html create mode 100644 src/app/zitadel/users/view-user/view-user.component.scss create mode 100644 src/app/zitadel/users/view-user/view-user.component.spec.ts create mode 100644 src/app/zitadel/users/view-user/view-user.component.ts diff --git a/docker-compose.yml b/docker-compose.yml index f09dd1ae5..5c6565f2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,3 +24,8 @@ services: - MIFOS_SUPPORTED_LANGUAGES=cs-CS,de-DE,en-US,es-MX,es-CL,fr-FR,it-IT,ko-KO,lt-LT,lv-LV,ne-NE,pt-PT,sw-SW - MIFOS_SESSION_IDLE_TIMEOUT=300000 - MIFOS_OAUTH_SERVER_ENABLED=false + - FINERACT_PLUGIN_OIDC_ENABLED=false + - FINERACT_PLUGIN_OIDC_BASE_URL=${FINERACT_PLUGIN_OIDC_BASE_URL} + - FINERACT_PLUGIN_OIDC_CLIENT_ID=${FINERACT_PLUGIN_OIDC_CLIENT_ID} + - FINERACT_PLUGIN_OIDC_API_URL=${FINERACT_PLUGIN_OIDC_API_URL} + - FINERACT_PLUGIN_OIDC_FRONTEND_URL=${FINERACT_PLUGIN_OIDC_FRONTEND_URL} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 8636091bb..bb060d336 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,11 +4,16 @@ import { Routes, RouterModule } from '@angular/router'; // Not Found Component import { NotFoundComponent } from './not-found/not-found.component'; +import { CallbackComponent } from './zitadel/callback/callback.component'; /** * Fallback to this route when no prior route is matched. */ const routes: Routes = [ + { + path: 'callback', + component: CallbackComponent + }, { path: '**', component: NotFoundComponent diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 76375ac28..90c6d8aef 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,7 +2,13 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { HttpBackend, HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { + HttpBackend, + HttpClient, + provideHttpClient, + withInterceptorsFromDi, + HTTP_INTERCEPTORS +} from '@angular/common/http'; /** Environment Configuration */ @@ -47,6 +53,12 @@ import { } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { AuthenticationInterceptor as TokenInterceptor } from './core/authentication/authentication.interceptor'; +import { TokenInterceptor as ZitadelTokenInterceptor } from './zitadel/token.interceptor'; +import { AuthService } from './zitadel/auth.service'; +import { environment } from '../environments/environment'; +import { CallbackComponent } from './zitadel/callback/callback.component'; + export class CustomMissingTranslationHandler implements MissingTranslationHandler { handle(params: MissingTranslationHandlerParams): string { // Remove the 'labels.catalogs.' prefix and return the fallback value @@ -107,11 +119,18 @@ export function HttpLoaderFactory(http: HttpClient) { TasksModule, ConfigurationWizardModule, AppRoutingModule, - NotFoundComponent + NotFoundComponent, + CallbackComponent ], providers: [ DatePipe, - provideHttpClient(withInterceptorsFromDi())] + AuthService, + { + provide: HTTP_INTERCEPTORS, + useClass: !environment.OIDC.oidcServerEnabled ? TokenInterceptor : ZitadelTokenInterceptor, + multi: true + } + ] }) export class AppModule {} diff --git a/src/app/core/authentication/authentication.service.ts b/src/app/core/authentication/authentication.service.ts index 199eef2bc..1d2b1910a 100644 --- a/src/app/core/authentication/authentication.service.ts +++ b/src/app/core/authentication/authentication.service.ts @@ -105,15 +105,12 @@ export class AuthenticationService { httpParams = httpParams.set('grant_type', 'password'); let headers = new HttpHeaders(); headers = headers.set('Content-Type', 'application/x-www-form-urlencoded'); - return this.http - .disableApiPrefix() - .post(`${environment.oauth.serverUrl}/token`, httpParams.toString(), { headers: headers }) - .pipe( - map((tokenResponse: OAuth2Token) => { - this.getUserDetails(tokenResponse); - return of(true); - }) - ); + return this.http.post(`${environment.oauth.serverUrl}/token`, httpParams.toString(), { headers: headers }).pipe( + map((tokenResponse: OAuth2Token) => { + this.getUserDetails(tokenResponse); + return of(true); + }) + ); } else { return this.http .post('/authentication', { username: loginContext.username, password: loginContext.password }) @@ -137,7 +134,6 @@ export class AuthenticationService { let headers = new HttpHeaders(); headers = headers.set('Authorization', 'bearer ' + tokenResponse.access_token); this.http - .disableApiPrefix() .get(`${environment.serverUrl}/userdetails`, { headers: headers }) .subscribe((credentials: Credentials) => { this.onLoginSuccess(credentials); @@ -174,7 +170,6 @@ export class AuthenticationService { let headers = new HttpHeaders(); headers = headers.set('Content-Type', 'application/x-www-form-urlencoded'); return this.http - .disableApiPrefix() .post(`${environment.oauth.serverUrl}/token`, httpParams.toString(), { headers: headers }) .subscribe((tokenResponse: OAuth2Token) => { this.storage.setItem(this.oAuthTokenDetailsStorageKey, JSON.stringify(tokenResponse)); @@ -241,7 +236,6 @@ export class AuthenticationService { let headers = new HttpHeaders(); headers = headers.set('Content-Type', 'application/x-www-form-urlencoded'); return this.http - .disableApiPrefix() .post(`${environment.oauth.serverUrl}/logout`, httpParams.toString(), { headers: headers }) .subscribe(); } @@ -318,6 +312,14 @@ export class AuthenticationService { } } + public saveZitadelCredentials(credentials: Credentials): void { + this.setCredentials(credentials); + } + + public saveZitadeloAuthTokenDetailsStorageKey(tokenResponse: OAuth2Token): void { + this.storage.setItem(this.oAuthTokenDetailsStorageKey, JSON.stringify(tokenResponse)); + } + /** * Following functions are for two factor authentication and require * first level authorization headers to be setup for the requests. diff --git a/src/app/core/http/cache.interceptor.ts b/src/app/core/http/cache.interceptor.ts index 8f60c0372..8898df688 100644 --- a/src/app/core/http/cache.interceptor.ts +++ b/src/app/core/http/cache.interceptor.ts @@ -7,7 +7,7 @@ import { Observable, Subscriber } from 'rxjs'; /** Custom Services */ import { HttpCacheService } from './http-cache.service'; -import { environment } from 'environments/environment'; +import { environment } from '../../../environments/environment'; /** * Caches HTTP requests. diff --git a/src/app/core/shell/sidenav/sidenav.component.ts b/src/app/core/shell/sidenav/sidenav.component.ts index 4bf792029..d6b82eda1 100644 --- a/src/app/core/shell/sidenav/sidenav.component.ts +++ b/src/app/core/shell/sidenav/sidenav.component.ts @@ -24,6 +24,10 @@ import { MatIcon } from '@angular/material/icon'; import { MatLine } from '@angular/material/grid-list'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** Environment Configuration and Zitadel*/ +import { environment } from '../../../../environments/environment'; +import { AuthService } from 'app/zitadel/auth.service'; + /** * Sidenav component. */ @@ -82,7 +86,8 @@ export class SidenavComponent implements OnInit, AfterViewInit { private authenticationService: AuthenticationService, private settingsService: SettingsService, private configurationWizardService: ConfigurationWizardService, - private popoverService: PopoverService + private popoverService: PopoverService, + private authService: AuthService ) { this.userActivity = JSON.parse(localStorage.getItem('mifosXLocation')); } @@ -100,7 +105,11 @@ export class SidenavComponent implements OnInit, AfterViewInit { * Logs out the authenticated user and redirects to login page. */ logout() { - this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); + if (!environment.OIDC.oidcServerEnabled) { + this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); + } else { + this.authService.logout(); + } } /** diff --git a/src/app/core/shell/toolbar/toolbar.component.ts b/src/app/core/shell/toolbar/toolbar.component.ts index 2dfb3eeab..93c709fef 100644 --- a/src/app/core/shell/toolbar/toolbar.component.ts +++ b/src/app/core/shell/toolbar/toolbar.component.ts @@ -41,6 +41,10 @@ import { NotificationsTrayComponent as NotificationsTrayComponent_1 } from '../. import { ThemeToggleComponent } from '../../../shared/theme-toggle/theme-toggle.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** Environment Configuration and Zitadel*/ +import { environment } from '../../../../environments/environment'; +import { AuthService } from 'app/zitadel/auth.service'; + /** * Toolbar component. */ @@ -103,7 +107,8 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec private popoverService: PopoverService, private configurationWizardService: ConfigurationWizardService, private dialog: MatDialog, - private changeDetector: ChangeDetectorRef + private changeDetector: ChangeDetectorRef, + private authService: AuthService ) {} /** @@ -140,10 +145,11 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec * Logs out the authenticated user and redirects to login page. */ logout() { - this.authenticationService.logout().subscribe(() => { - this.notificationsTray.destroy(); - this.router.navigate(['/login'], { replaceUrl: true }); - }); + if (!environment.OIDC.oidcServerEnabled) { + this.authenticationService.logout().subscribe(() => this.router.navigate(['/login'], { replaceUrl: true })); + } else { + this.authService.logout(); + } } /** diff --git a/src/app/login/login-form/login-form.component.html b/src/app/login/login-form/login-form.component.html index 890b39ac2..20adf770b 100644 --- a/src/app/login/login-form/login-form.component.html +++ b/src/app/login/login-form/login-form.component.html @@ -1,55 +1,72 @@ -
- - - - - {{ 'labels.inputs.Username' | translate }} - - - {{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is required' | translate }} - - +
+ + + + + + {{ + 'labels.inputs.Remember me' | translate + }} + + + + - + +
- {{ - 'labels.inputs.Remember me' | translate - }} - - - - - - - +
+
+ +
+
+

Welcome! You are logged in.

+ + +
{{ protectedData | json }}
+
+
diff --git a/src/app/login/login-form/login-form.component.scss b/src/app/login/login-form/login-form.component.scss index a6195571c..c0ae7a27a 100644 --- a/src/app/login/login-form/login-form.component.scss +++ b/src/app/login/login-form/login-form.component.scss @@ -18,3 +18,8 @@ ::ng-deep .mat-mdc-form-field-infix { width: 144px !important; } + +.container-margin { + margin-top: 8rem; + margin-bottom: 8rem; +} diff --git a/src/app/login/login-form/login-form.component.ts b/src/app/login/login-form/login-form.component.ts index 08813b0ef..a4a6fc989 100644 --- a/src/app/login/login-form/login-form.component.ts +++ b/src/app/login/login-form/login-form.component.ts @@ -15,6 +15,10 @@ import { MatProgressBar } from '@angular/material/progress-bar'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** Custom Service Zitadel */ +import { AuthService } from '../../zitadel/auth.service'; +import { environment } from '../../../environments/environment'; + /** * Login form component. */ @@ -39,6 +43,7 @@ export class LoginFormComponent implements OnInit { passwordInputType: string = 'password'; /** True if loading. */ loading = false; + oidcServerEnabled = environment.OIDC.oidcServerEnabled; /** * @param {FormBuilder} formBuilder Form Builder. @@ -46,7 +51,8 @@ export class LoginFormComponent implements OnInit { */ constructor( private formBuilder: FormBuilder, - private authenticationService: AuthenticationService + private authenticationService: AuthenticationService, + private authService: AuthService ) {} /** @@ -78,6 +84,22 @@ export class LoginFormComponent implements OnInit { .subscribe(); } + /** + * Authenticates the user if the credentials are valid to Zitadel. + */ + + loginOIDC() { + this.authService.login(); + } + + getUsers() { + this.authService.getUsers(); + } + + logout() { + this.authService.logout(); + } + /** * Toggles the visibility of the password input field. * diff --git a/src/app/system/roles-and-permissions/add-role/add-role.component.ts b/src/app/system/roles-and-permissions/add-role/add-role.component.ts index 0cf7fed0b..c29c055f0 100644 --- a/src/app/system/roles-and-permissions/add-role/add-role.component.ts +++ b/src/app/system/roles-and-permissions/add-role/add-role.component.ts @@ -7,6 +7,10 @@ import { Router, ActivatedRoute, RouterLink } from '@angular/router'; import { SystemService } from '../../system.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** Custom Service Zitadel */ +import { environment } from '../../../../environments/environment'; +import { AuthService } from 'app/zitadel/auth.service'; + /** * Add Role Component. */ @@ -21,6 +25,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; export class AddRoleComponent implements OnInit { /** Role form. */ roleForm: UntypedFormGroup; + /** Add role zitadel */ /** * @param {FormBuilder} formBuilder Form Builder. @@ -32,7 +37,8 @@ export class AddRoleComponent implements OnInit { private formBuilder: UntypedFormBuilder, private systemService: SystemService, private route: ActivatedRoute, - private router: Router + private router: Router, + private authService: AuthService ) {} /** @@ -64,6 +70,9 @@ export class AddRoleComponent implements OnInit { */ submit() { this.systemService.createRole(this.roleForm.value).subscribe((response: any) => { + if (environment.OIDC.oidcServerEnabled) { + this.authService.createRole(response.resourceId, this.roleForm.value.name, this.roleForm.value.description); + } this.router.navigate(['../'], { relativeTo: this.route }); }); } diff --git a/src/app/system/roles-and-permissions/edit-role/edit-role.component.ts b/src/app/system/roles-and-permissions/edit-role/edit-role.component.ts index 539a75240..609dccf84 100644 --- a/src/app/system/roles-and-permissions/edit-role/edit-role.component.ts +++ b/src/app/system/roles-and-permissions/edit-role/edit-role.component.ts @@ -7,6 +7,10 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { SystemService } from '../../system.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** Custom Service Zitadel */ +import { environment } from '../../../../environments/environment'; +import { AuthService } from 'app/zitadel/auth.service'; + /** * Edit Role Description Component. */ @@ -35,7 +39,8 @@ export class EditRoleComponent implements OnInit { private formBuilder: UntypedFormBuilder, private systemService: SystemService, private route: ActivatedRoute, - private router: Router + private router: Router, + private authService: AuthService ) { this.route.data.subscribe((data: { role: any }) => { this.roleData = data.role; @@ -71,6 +76,13 @@ export class EditRoleComponent implements OnInit { */ submit() { this.systemService.updateRole(this.roleForm.value, this.roleData.id).subscribe(() => { + if (environment.OIDC.oidcServerEnabled) { + this.authService.updateRole( + this.roleData.id, + this.roleForm.get('name')?.value, + this.roleForm.value.description + ); + } this.router.navigate(['../../'], { relativeTo: this.route }); }); } diff --git a/src/app/system/roles-and-permissions/view-role/view-role.component.ts b/src/app/system/roles-and-permissions/view-role/view-role.component.ts index 40b9ff976..de66bac4f 100644 --- a/src/app/system/roles-and-permissions/view-role/view-role.component.ts +++ b/src/app/system/roles-and-permissions/view-role/view-role.component.ts @@ -18,6 +18,10 @@ import { MatDivider } from '@angular/material/divider'; import { MatCheckbox } from '@angular/material/checkbox'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +/** Custom Service Zitadel */ +import { environment } from '../../../../environments/environment'; +import { AuthService } from 'app/zitadel/auth.service'; + /** * View Role and Permissions Component */ @@ -68,6 +72,7 @@ export class ViewRoleComponent implements OnInit { permissions: { permissions: { code: string; id: number }[]; } = { permissions: [] }; + /** Add role zitadel */ /** * Retrieves the roledetails data from `resolve`. @@ -84,7 +89,8 @@ export class ViewRoleComponent implements OnInit { private router: Router, private formBuilder: UntypedFormBuilder, private translateService: TranslateService, - public dialog: MatDialog + public dialog: MatDialog, + private authService: AuthService ) { this.route.data.subscribe((data: { roledetails: any }) => { this.rolePermissionService = data.roledetails; @@ -281,6 +287,9 @@ export class ViewRoleComponent implements OnInit { deleteRoleDialogRef.afterClosed().subscribe((response: any) => { if (response.delete) { this.systemService.deleteRole(this.roleId).subscribe(() => { + if (environment.OIDC.oidcServerEnabled) { + this.authService.deleteRole(this.roleId); + } this.router.navigate(['/system/roles-and-permissions']); }); } else { diff --git a/src/app/users/users-routing.module.ts b/src/app/users/users-routing.module.ts index cd3a1fbe7..d838ff1e6 100644 --- a/src/app/users/users-routing.module.ts +++ b/src/app/users/users-routing.module.ts @@ -16,51 +16,106 @@ import { UsersResolver } from './users.resolver'; import { UsersTemplateResolver } from './users-template.resolver'; import { UserResolver } from './user.resolver'; -/** Users Routes */ +/** Custom Resolvers for Zitadel */ +import { UsersZitadelResolver } from '../zitadel/users/usersZitadel.resolver'; +import { UsersZitadelTemplateResolver } from '../zitadel/users/usersZitadel-template.resolver'; +import { UserZitadelResolver } from '../zitadel/users/userZitadel.resolver'; + +/** Custom Components for Zitadel */ +import { UsersComponent as UserZitadelComponent } from '../zitadel/users/users.component'; +import { ViewUserComponent as ViewUserZitadelComponent } from '../zitadel/users/view-user/view-user.component'; +import { CreateUserComponent as CreateUserZitadelComponent } from '../zitadel/users/create-user/create-user.component'; +import { EditUserComponent as EditUserZitadelComponent } from '../zitadel/users/edit-user/edit-user.component'; + +/** Environment */ +import { environment } from '../../environments/environment'; + +/** Users Routes for Mifos */ +const mifosRoutes: Routes = [ + { + path: '', + component: UsersComponent, + resolve: { users: UsersResolver } + }, + { + path: 'create', + component: CreateUserComponent, + data: { title: 'Create User', breadcrumb: 'Create User' }, + resolve: { usersTemplate: UsersTemplateResolver } + }, + { + path: ':id', + data: { title: 'View User', routeParamBreadcrumb: 'id' }, + children: [ + { + path: '', + component: ViewUserComponent, + resolve: { user: UserResolver } + }, + { + path: 'edit', + component: EditUserComponent, + data: { title: 'Edit User', breadcrumb: 'Edit', routeResolveBreadcrumb: false }, + resolve: { + user: UserResolver, + usersTemplate: UsersTemplateResolver + } + } + ] + } +]; + +/** + * Route definition for Zitadel + */ +const zitadelRoutes: Routes = [ + { + path: '', + component: UserZitadelComponent, + resolve: { users: UsersZitadelResolver } + }, + { + path: 'create', + component: CreateUserZitadelComponent, + data: { title: 'Create User', breadcrumb: 'Create User' }, + resolve: { usersTemplate: UsersTemplateResolver } + }, + { + path: ':id', + data: { title: 'View User', routeParamBreadcrumb: 'id' }, + children: [ + { + path: '', + component: ViewUserZitadelComponent, + resolve: { user: UserZitadelResolver } + }, + { + path: 'edit', + component: EditUserZitadelComponent, + data: { title: 'Edit User', breadcrumb: 'Edit', routeResolveBreadcrumb: false }, + resolve: { + user: UserZitadelResolver, + usersTemplate: UsersZitadelTemplateResolver + } + } + ] + } +]; + +/** + * Choose the correct route set according to the environment + */ +const selectedRoutes = !environment.OIDC.oidcServerEnabled ? mifosRoutes : zitadelRoutes; + +/** + * Final routes with the shell + */ const routes: Routes = [ Route.withShell([ { path: 'appusers', data: { title: 'Users', breadcrumb: 'Users' }, - children: [ - { - path: '', - component: UsersComponent, - resolve: { - users: UsersResolver - } - }, - { - path: 'create', - component: CreateUserComponent, - data: { title: 'Create User', breadcrumb: 'Create User' }, - resolve: { - usersTemplate: UsersTemplateResolver - } - }, - { - path: ':id', - data: { title: 'View User', routeParamBreadcrumb: 'id' }, - children: [ - { - path: '', - component: ViewUserComponent, - resolve: { - user: UserResolver - } - }, - { - path: 'edit', - component: EditUserComponent, - data: { title: 'Edit User', breadcrumb: 'Edit', routeResolveBreadcrumb: false }, - resolve: { - user: UserResolver, - usersTemplate: UsersTemplateResolver - } - } - ] - } - ] + children: selectedRoutes } ]) @@ -77,7 +132,10 @@ const routes: Routes = [ providers: [ UsersResolver, UsersTemplateResolver, - UserResolver + UserResolver, + UsersZitadelResolver, + UserZitadelResolver, + UsersZitadelTemplateResolver ] }) export class UsersRoutingModule {} diff --git a/src/app/users/users.module.ts b/src/app/users/users.module.ts index 04621849d..14732c240 100644 --- a/src/app/users/users.module.ts +++ b/src/app/users/users.module.ts @@ -11,6 +11,12 @@ import { CreateUserComponent } from './create-user/create-user.component'; import { ViewUserComponent } from './view-user/view-user.component'; import { EditUserComponent } from './edit-user/edit-user.component'; +/** Custom Components of Zitadel */ +import { ViewUserComponent as ViewUserZitadelComponent } from '../zitadel/users/view-user/view-user.component'; +import { EditUserComponent as EditUserZitadelComponent } from '../zitadel/users/edit-user/edit-user.component'; +import { CreateUserComponent as CreateUserZitadelComponent } from '../zitadel/users/create-user/create-user.component'; +import { UsersComponent as UserZitadelComponent } from '../zitadel/users/users.component'; + /** * Users Module * @@ -23,7 +29,12 @@ import { EditUserComponent } from './edit-user/edit-user.component'; UsersComponent, CreateUserComponent, ViewUserComponent, - EditUserComponent + EditUserComponent, + // Users Zitadel + UserZitadelComponent, + ViewUserZitadelComponent, + EditUserZitadelComponent, + CreateUserZitadelComponent ] }) export class UsersModule {} diff --git a/src/app/web-app.component.ts b/src/app/web-app.component.ts index 8670e6f4c..c551fbab2 100644 --- a/src/app/web-app.component.ts +++ b/src/app/web-app.component.ts @@ -32,6 +32,8 @@ import { animate, style, transition, trigger } from '@angular/animations'; import { I18nService } from './core/i18n/i18n.service'; import { ThemingService } from './shared/theme-toggle/theming.service'; +import { AuthService } from './zitadel/auth.service'; + /** Initialize Logger */ const log = new Logger('MifosX'); @@ -90,6 +92,8 @@ export class WebAppComponent implements OnInit { i18nService: I18nService; + isLoggedIn = false; + /** * @param {Router} router Router for navigation. * @param {ActivatedRoute} activatedRoute Activated Route. @@ -117,7 +121,8 @@ export class WebAppComponent implements OnInit { private themingService: ThemingService, private dateUtils: Dates, private idle: IdleTimeoutService, - private dialog: MatDialog + private dialog: MatDialog, + private authService: AuthService ) {} @HostBinding('class') public cssClass: string; @@ -133,6 +138,11 @@ export class WebAppComponent implements OnInit { * * 4) Alerts */ + + login() { + this.authService.login(); + } + ngOnInit() { this.themingService.theme.subscribe((value: string) => { this.cssClass = value; @@ -222,14 +232,18 @@ export class WebAppComponent implements OnInit { // Subscribe to session timeout If IdleTimeout is higher than 0 (zero) if (environment.session.timeout.idleTimeout > 0) { this.idle.$onSessionTimeout.subscribe(() => { - if (this.authenticationService.getUserLoggedIn()) { - this.alertService.alert({ - type: 'Session timeout', - message: this.translateService.instant('labels.text.Session timed out') - }); - this.dialog.open(SessionTimeoutDialogComponent); - this.logout(); - } + this.alertService.alert({ + type: 'Session timeout', + message: this.translateService.instant('labels.text.Session timed out') + }); + this.dialog.open(SessionTimeoutDialogComponent); + setTimeout(() => { + if (environment.OIDC.oidcServerEnabled) { + this.logout(); + } else { + this.authService.logout(); + } + }, 1000); }); } } diff --git a/src/app/zitadel/auth.guard.ts b/src/app/zitadel/auth.guard.ts new file mode 100644 index 000000000..e2c707e07 --- /dev/null +++ b/src/app/zitadel/auth.guard.ts @@ -0,0 +1,10 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router, UrlTree, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from './auth.service'; + +export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const authService = inject(AuthService); + const router = inject(Router); + + return true; +}; diff --git a/src/app/zitadel/auth.service.ts b/src/app/zitadel/auth.service.ts new file mode 100644 index 000000000..c79ab77d4 --- /dev/null +++ b/src/app/zitadel/auth.service.ts @@ -0,0 +1,381 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http'; +import { ActivatedRoute, Router } from '@angular/router'; + +/** Custom Services */ +import { AuthenticationService } from '../core/authentication/authentication.service'; +import { Credentials } from '../core/authentication/credentials.model'; +import { OAuth2Token } from '../core/authentication/o-auth2-token.model'; +import { environment } from '../../environments/environment'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + private baseUrl = environment.OIDC.oidcBaseUrl; + private authUrl = `${this.baseUrl}oauth/v2/authorize`; + private tokenUrl = `${this.baseUrl}oauth/v2/token`; + private clientId = environment.OIDC.oidcClientId; + private api = environment.OIDC.oidcApiUrl; + private frontUrl = environment.OIDC.oidcFrontUrl; + private redirectUri = `${this.frontUrl}#/callback`; + private refreshTimeoutId: any = null; + + constructor( + private authenticationService: AuthenticationService, + private http: HttpClient, + private route: ActivatedRoute, + private router: Router + ) {} + + async login() { + const codeVerifier = this.generateRandomString(); + const codeChallenge = await this.generateCodeChallenge(codeVerifier); + localStorage.setItem('code_verifier', codeVerifier); + const url = + `${this.authUrl}` + + `?client_id=${encodeURIComponent(this.clientId)}` + + `&redirect_uri=${encodeURIComponent(this.redirectUri)}` + + `&response_type=code` + + `&scope=openid profile email offline_access` + + `&code_challenge=${codeChallenge}` + + `&code_challenge_method=S256`; + window.location.href = url; + } + + logout() { + const idToken = localStorage.getItem('id_token'); + const postLogoutRedirectUri = this.frontUrl + '#/login'; + + if (this.refreshTimeoutId) { + clearTimeout(this.refreshTimeoutId); + this.refreshTimeoutId = null; + } + + if (!idToken) { + window.location.href = postLogoutRedirectUri; + return; + } + + sessionStorage.removeItem('mifosXCredentials'); + sessionStorage.removeItem('mifosXZitadelTokenDetails'); + localStorage.removeItem('access_token'); + localStorage.removeItem('expires_in'); + localStorage.removeItem('id_token'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('refresh_expires_in'); + localStorage.removeItem('token_start_time'); + localStorage.removeItem('code_verifier'); + localStorage.removeItem('mifosXZitadel'); + localStorage.removeItem('auth_code'); + + const logoutUrl = `${this.baseUrl}/oidc/v1/end_session?id_token_hint=${idToken}&post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}`; + window.location.href = logoutUrl; + } + + getAccessToken(): string | null { + const rawToken = sessionStorage.getItem('mifosXZitadelTokenDetails'); + + if (rawToken) { + const parsedToken: OAuth2Token = JSON.parse(rawToken); + return parsedToken.access_token; + } + + return null; + } + + generateRandomString(length = 128) { + const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + let result = ''; + for (let i = 0; i < length; i++) { + result += charset.charAt(Math.floor(Math.random() * charset.length)); + } + return result; + } + + async generateCodeChallenge(verifier: string): Promise { + const data = new TextEncoder().encode(verifier); + const digest = await crypto.subtle.digest('SHA-256', data); + const base64 = btoa(String.fromCharCode(...new Uint8Array(digest))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + return base64; + } + + exchangeCodeForTokens(code: string, codeVerifier: string | null) { + const payload = { + code: code, + code_verifier: codeVerifier || '' + }; + + fetch(this.api + 'authentication/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + .then((res) => { + if (!res.ok) { + throw new Error(`Error exchanging code: ${res.status} ${res.statusText}`); + } + return res.json(); + }) + .then( + (tokens: { + access_token: string; + id_token: string; + refresh_token: string; + expires_in: number; + token_type: string; + }) => { + const token: OAuth2Token = { + access_token: tokens.access_token, + token_type: tokens.token_type, + refresh_token: tokens.refresh_token, + expires_in: tokens.expires_in, + scope: 'Bearer' + }; + + localStorage.setItem('id_token', tokens.id_token); + localStorage.setItem('mifosXZitadel', 'true'); + sessionStorage.setItem('mifosXZitadelTokenDetails', JSON.stringify(token)); + localStorage.setItem('refresh_token', tokens.refresh_token); + this.scheduleRefresh(tokens.expires_in); + localStorage.removeItem('auth_code'); + localStorage.removeItem('code_verifier'); + this.userdetails(); + } + ) + .catch((error) => { + localStorage.removeItem('auth_code'); + localStorage.removeItem('code_verifier'); + window.location.href = '/#/login'; + }); + } + + userdetails() { + const accessToken = this.getAccessToken(); + if (!accessToken) { + console.error('No access token found'); + return; + } + + fetch(this.api + 'authentication/userdetails', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ token: accessToken }) + }) + .then((res) => { + if (!res.ok) { + throw new Error('Error retrieving user data from backend'); + } + return res.json(); + }) + .then((userInfo) => { + const user = userInfo.object; + const credentials: Credentials = user; + this.authenticationService.saveZitadelCredentials(credentials); + window.location.href = '/#/home'; + }) + .catch((error) => { + console.error('Error consuming backend:', error); + }); + } + + public deletUser(userId: string) { + fetch(`${this.api}authentication/user/${userId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getAccessToken()}` + } + }) + .then((res) => res.json()) + .then((data) => { + if (data.status === 200) { + this.router.navigate(['/appusers']); + } else { + alert(data.msg); + } + }) + .catch((error) => { + alert(error.msg); + }); + } + + public activeUser(userId: string) { + fetch(`${this.api}authentication/user/act/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getAccessToken()}` + }, + body: JSON.stringify({ userId }) + }) + .then((res) => res.json()) + .then((data) => { + window.location.reload(); + }) + .catch((error) => { + alert(error.msg); + }); + } + + public desactiveUser(userId: string) { + fetch(`${this.api}authentication/user/des/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getAccessToken()}` + } + }) + .then((res) => res.json()) + .then((data) => { + window.location.reload(); + }) + .catch((error) => { + alert(error.msg); + }); + } + + public getUsers() { + let getUsers: any[] = []; + fetch(`${this.api}authentication/user`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getAccessToken()}` + } + }) + .then((res) => res.json()) + .then((response) => { + const users = response.data?.result; + if (Array.isArray(users)) { + users.forEach((element: { human: any }) => { + const human = element.human; + if (human) { + getUsers.push(human); + } + }); + } + }) + .catch((error) => console.error(`Error retrieving users: ${error}`)); + } + + public createRole(roleKey: string, displayName: string, group: string) { + fetch(`${this.api}authentication/role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getAccessToken()}` + }, + body: JSON.stringify({ roleKey, displayName, group }) + }) + .then((res) => res.json()) + .then((data) => {}) + .catch((error) => { + alert(error.msg); + }); + } + + public updateRole(roleKey: string, displayName: string, group: string) { + fetch(`${this.api}authentication/role`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getAccessToken()}` + }, + body: JSON.stringify({ roleKey, displayName, group }) + }) + .then((res) => res.json()) + .then((data) => {}) + .catch((error) => { + alert(error.msg); + }); + } + + public deleteRole(roleKey: string) { + fetch(`${this.api}authentication/role/${roleKey}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.getAccessToken()}` + }, + body: JSON.stringify({ roleKey }) + }) + .then((res) => res.json()) + .then((data) => {}) + .catch((error) => { + alert(error.msg); + }); + } + + refreshToken(): Promise { + return new Promise((resolve, reject) => { + const rt = localStorage.getItem('refresh_token'); + + if (!rt) { + //this.logout(); + return reject('Sin refresh_token'); + } + + const payload = new URLSearchParams(); + payload.set('grant_type', 'refresh_token'); + payload.set('refresh_token', rt); + payload.set('client_id', this.clientId); + + fetch(this.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: payload.toString() + }) + .then((res) => { + if (!res.ok) { + return res.text().then((text) => { + throw new Error(text); + }); + } + return res.json(); + }) + .then((tokens) => { + if (!tokens || !tokens.access_token || !tokens.expires_in) { + throw new Error("The server's response does not contain the expected fields."); + } + localStorage.setItem('access_token', tokens.access_token); + localStorage.setItem('id_token', tokens.id_token ?? ''); + localStorage.setItem('refresh_token', tokens.refresh_token ?? ''); + localStorage.setItem('expires_in', tokens.expires_in.toString()); + localStorage.setItem('refresh_expires_in', tokens.refresh_expires_in?.toString() ?? ''); + localStorage.setItem('token_start_time', Date.now().toString()); + this.scheduleRefresh(tokens.expires_in); + resolve(); + }) + .catch((err) => { + setTimeout(() => { + //this.logout(); + }, 300000); + reject(err); + }); + }); + } + + private scheduleRefresh(expiresIn: number) { + const refreshInMs = (expiresIn - 3539) * 1000; + if (refreshInMs <= 0) { + this.refreshToken(); + return; + } + + if (this.refreshTimeoutId) { + clearTimeout(this.refreshTimeoutId); + } + + this.refreshTimeoutId = setTimeout(() => { + this.refreshToken(); + }, refreshInMs); + } +} diff --git a/src/app/zitadel/callback/callback.component.html b/src/app/zitadel/callback/callback.component.html new file mode 100644 index 000000000..da86c5b8a --- /dev/null +++ b/src/app/zitadel/callback/callback.component.html @@ -0,0 +1,6 @@ +
+
+
+
+
+
diff --git a/src/app/zitadel/callback/callback.component.ts b/src/app/zitadel/callback/callback.component.ts new file mode 100644 index 000000000..97697e11a --- /dev/null +++ b/src/app/zitadel/callback/callback.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { AuthService } from '../auth.service'; + +@Component({ + selector: 'mifosx-callback', + templateUrl: './callback.component.html' +}) +export class CallbackComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private authService: AuthService + ) {} + + ngOnInit(): void { + let code = localStorage.getItem('auth_code'); + + if (code) { + const codeVerifier = localStorage.getItem('code_verifier'); + this.authService.exchangeCodeForTokens(code, codeVerifier); + } + } +} diff --git a/src/app/zitadel/constants/coutry-codes.ts b/src/app/zitadel/constants/coutry-codes.ts new file mode 100644 index 000000000..f54b86a37 --- /dev/null +++ b/src/app/zitadel/constants/coutry-codes.ts @@ -0,0 +1,117 @@ +export const COUNTRY_CODES = [ + { code: '+376', key: 'AD' }, + { code: '+971', key: 'AE' }, + { code: '+93', key: 'AF' }, + { code: '+355', key: 'AL' }, + { code: '+374', key: 'AM' }, + { code: '+244', key: 'AO' }, + { code: '+54', key: 'AR' }, + { code: '+43', key: 'AT' }, + { code: '+61', key: 'AU' }, + { code: '+994', key: 'AZ' }, + { code: '+387', key: 'BA' }, + { code: '+880', key: 'BD' }, + { code: '+32', key: 'BE' }, + { code: '+359', key: 'BG' }, + { code: '+226', key: 'BF' }, + { code: '+257', key: 'BI' }, + { code: '+973', key: 'BH' }, + { code: '+591', key: 'BO' }, + { code: '+55', key: 'BR' }, + { code: '+375', key: 'BY' }, + { code: '+1', key: 'CA' }, + { code: '+237', key: 'CM' }, + { code: '+56', key: 'CL' }, + { code: '+86', key: 'CN' }, + { code: '+57', key: 'CO' }, + { code: '+506', key: 'CR' }, + { code: '+53', key: 'CU' }, + { code: '+357', key: 'CY' }, + { code: '+420', key: 'CZ' }, + { code: '+49', key: 'DE' }, + { code: '+45', key: 'DK' }, + { code: '+213', key: 'DZ' }, + { code: '+20', key: 'EG' }, + { code: '+503', key: 'SV' }, + { code: '+372', key: 'EE' }, + { code: '+34', key: 'ES' }, + { code: '+251', key: 'ET' }, + { code: '+358', key: 'FI' }, + { code: '+33', key: 'FR' }, + { code: '+44', key: 'GB' }, + { code: '+995', key: 'GE' }, + { code: '+233', key: 'GH' }, + { code: '+30', key: 'GR' }, + { code: '+502', key: 'GT' }, + { code: '+504', key: 'HN' }, + { code: '+385', key: 'HR' }, + { code: '+36', key: 'HU' }, + { code: '+62', key: 'ID' }, + { code: '+91', key: 'IN' }, + { code: '+353', key: 'IE' }, + { code: '+972', key: 'IL' }, + { code: '+964', key: 'IQ' }, + { code: '+98', key: 'IR' }, + { code: '+354', key: 'IS' }, + { code: '+39', key: 'IT' }, + { code: '+81', key: 'JP' }, + { code: '+962', key: 'JO' }, + { code: '+254', key: 'KE' }, + { code: '+996', key: 'KG' }, + { code: '+855', key: 'KH' }, + { code: '+82', key: 'KR' }, + { code: '+965', key: 'KW' }, + { code: '+371', key: 'LV' }, + { code: '+961', key: 'LB' }, + { code: '+94', key: 'LK' }, + { code: '+370', key: 'LT' }, + { code: '+352', key: 'LU' }, + { code: '+218', key: 'LY' }, + { code: '+212', key: 'MA' }, + { code: '+377', key: 'MC' }, + { code: '+373', key: 'MD' }, + { code: '+389', key: 'MK' }, + { code: '+976', key: 'MN' }, + { code: '+52', key: 'MX' }, + { code: '+60', key: 'MY' }, + { code: '+258', key: 'MZ' }, + { code: '+234', key: 'NG' }, + { code: '+505', key: 'NI' }, + { code: '+31', key: 'NL' }, + { code: '+47', key: 'NO' }, + { code: '+977', key: 'NP' }, + { code: '+64', key: 'NZ' }, + { code: '+507', key: 'PA' }, + { code: '+92', key: 'PK' }, + { code: '+595', key: 'PY' }, + { code: '+51', key: 'PE' }, + { code: '+63', key: 'PH' }, + { code: '+48', key: 'PL' }, + { code: '+351', key: 'PT' }, + { code: '+974', key: 'QA' }, + { code: '+40', key: 'RO' }, + { code: '+381', key: 'RS' }, + { code: '+7', key: 'RU' }, + { code: '+966', key: 'SA' }, + { code: '+221', key: 'SN' }, + { code: '+46', key: 'SE' }, + { code: '+65', key: 'SG' }, + { code: '+421', key: 'SK' }, + { code: '+386', key: 'SI' }, + { code: '+503', key: 'SV' }, + { code: '+27', key: 'ZA' }, + { code: '+41', key: 'CH' }, + { code: '+886', key: 'TW' }, + { code: '+66', key: 'TH' }, + { code: '+90', key: 'TR' }, + { code: '+380', key: 'UA' }, + { code: '+256', key: 'UG' }, + { code: '+1', key: 'US' }, + { code: '+598', key: 'UY' }, + { code: '+998', key: 'UZ' }, + { code: '+58', key: 'VE' }, + { code: '+84', key: 'VN' }, + { code: '+967', key: 'YE' }, + { code: '+260', key: 'ZM' }, + { code: '+263', key: 'ZW' } +]; diff --git a/src/app/zitadel/constants/languages.ts b/src/app/zitadel/constants/languages.ts new file mode 100644 index 000000000..64012096c --- /dev/null +++ b/src/app/zitadel/constants/languages.ts @@ -0,0 +1,9 @@ +export const ZITADEL_LANGUAGES = [ + { code: 'de', key: 'de-DE', name: 'German' }, + { code: 'en', key: 'en-US', name: 'English' }, + { code: 'es', key: 'es-MX', name: 'Español' }, + { code: 'fr', key: 'fr-FR', name: 'French' }, + { code: 'it', key: 'it-IT', name: 'Italian' }, + { code: 'ko', key: 'ko-KO', name: '한국어' }, + { code: 'pt', key: 'pt-PT', name: 'Portuguese' } +]; diff --git a/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.html b/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.html new file mode 100644 index 000000000..b699ca6ad --- /dev/null +++ b/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.html @@ -0,0 +1,35 @@ +

{{ 'labels.heading.Change Password' | translate }}

+ +
+
+ + {{ 'labels.inputs.Password Current' | translate }} + + + {{ 'labels.inputs.Password Current Required' | translate }} + + + + {{ 'labels.inputs.Password New' | translate }} + + + {{ 'labels.commons.Password validation' | translate: { minchar: minPasswordLength } }} + + + + {{ 'labels.inputs.Repeat Password' | translate }} + + + {{ 'inputs.passwordsDoNotMatch' | translate }} + + + + + + +
+
diff --git a/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.scss b/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.scss new file mode 100644 index 000000000..e6103db4d --- /dev/null +++ b/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.scss @@ -0,0 +1,8 @@ +.error { + color: red; +} + +mat-dialog-content { + min-width: 240px; + width: 240px; +} diff --git a/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.spec.ts b/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.spec.ts new file mode 100644 index 000000000..1144ba12d --- /dev/null +++ b/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChangePasswordDialogComponent } from './change-password-dialog.component'; + +describe('ChangePasswordDialogComponent', () => { + let component: ChangePasswordDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ChangePasswordDialogComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChangePasswordDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.ts b/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.ts new file mode 100644 index 000000000..91313816e --- /dev/null +++ b/src/app/zitadel/shared/change-password-dialog/change-password-dialog.component.ts @@ -0,0 +1,124 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA, MatDialogActions } from '@angular/material/dialog'; +import { UntypedFormBuilder, Validators, AbstractControl, ValidationErrors, FormGroup } from '@angular/forms'; +import { confirmPasswordValidator } from 'app/login/reset-password/confirm-password.validator'; +import { HttpClient } from '@angular/common/http'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { environment } from '../../../../environments/environment'; + +/** Custom Components */ +import { TranslateService } from '@ngx-translate/core'; + +/** + * Change Password Dialog component. + */ +@Component({ + selector: 'mifosx-change-password-dialog', + templateUrl: './change-password-dialog.component.html', + styleUrls: ['./change-password-dialog.component.scss'], + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogActions + ] +}) +export class ChangePasswordDialogComponent implements OnInit { + minPasswordLength: number = environment.minPasswordLength || 12; + changePasswordForm: FormGroup; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + private formBuilder: UntypedFormBuilder, + private http: HttpClient, + private translateService: TranslateService + ) {} + + private api = environment.OIDC.oidcApiUrl; + + ngOnInit(): void { + this.createChangePasswordForm(); + this.setupPasswordMatchValidation(); + } + + createChangePasswordForm(): void { + this.changePasswordForm = this.formBuilder.group( + { + currentPassword: [ + '', + Validators.required + ], + password: [ + '', + [ + Validators.required, + Validators.minLength(this.minPasswordLength), + Validators.maxLength(50), + Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).+$/)] + ], + repeatPassword: [ + '', + Validators.required + ] + }, + { validators: confirmPasswordValidator } + ); + } + + setupPasswordMatchValidation(): void { + this.changePasswordForm.get('password')?.valueChanges.subscribe(() => { + this.changePasswordForm.get('repeatPassword')?.updateValueAndValidity(); + }); + + this.changePasswordForm.get('repeatPassword')?.setValidators([ + Validators.required, + this.matchOtherControl('password')]); + } + + matchOtherControl(controlNameToMatch: string) { + return (control: AbstractControl): ValidationErrors | null => { + const parent = control?.parent; + if (!parent) return null; + + const matchingControl = parent.get(controlNameToMatch); + if (!matchingControl) return null; + + return control.value === matchingControl.value ? null : { notequal: true }; + }; + } + + onSubmit(): void { + if (this.changePasswordForm.invalid) return; + + const requestBody = { + userId: this.data.id, + currentPassword: this.changePasswordForm.value.currentPassword, + newPassword: { + password: this.changePasswordForm.value.password, + changeRequired: false + } + }; + + this.http.put(this.api + 'authentication/user/password', requestBody).subscribe({ + next: (res) => { + this.dialogRef.close(true); + }, + error: (err) => { + try { + const backendError = JSON.parse(err.error.message); + if (backendError.code === 3) { + alert(this.translateService.instant('labels.inputs.The current password is not correct')); + } else if (backendError.code === 9) { + alert( + this.translateService.instant('labels.inputs.New password cannot be the same as your current password') + ); + } else { + alert('Error: ' + backendError.message); + } + } catch (e) { + alert(this.translateService.instant('labels.inputs.Unable to update password')); + } + } + }); + } +} diff --git a/src/app/zitadel/shared/shared.module.ts b/src/app/zitadel/shared/shared.module.ts new file mode 100644 index 000000000..6b2dafb06 --- /dev/null +++ b/src/app/zitadel/shared/shared.module.ts @@ -0,0 +1,34 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; + +// Angular Material Modules +import { MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TranslateModule } from '@ngx-translate/core'; + +import { ChangePasswordDialogComponent } from './change-password-dialog/change-password-dialog.component'; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + TranslateModule, + MatDialogModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + MatIconModule, + MatTooltipModule, + ChangePasswordDialogComponent + ], + exports: [ + ChangePasswordDialogComponent + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class SharedModule {} diff --git a/src/app/zitadel/token.interceptor.ts b/src/app/zitadel/token.interceptor.ts new file mode 100644 index 000000000..a4bf3b121 --- /dev/null +++ b/src/app/zitadel/token.interceptor.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http'; +import { Observable, throwError, from } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { AuthService } from './auth.service'; +import { environment } from '../../environments/environment'; + +@Injectable() +export class TokenInterceptor implements HttpInterceptor { + public environment = environment; + FINERACT_PLATFORM_TENANT_IDENTIFIER = environment.fineractPlatformTenantId; + + constructor(private authService: AuthService) {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const token = this.authService.getAccessToken(); + let headersConfig: { [key: string]: string } = { + 'Fineract-Platform-TenantId': this.FINERACT_PLATFORM_TENANT_IDENTIFIER, + 'Content-Type': req.headers.get('Content-Type') || 'application/json' + }; + const publicEndpoints = [ + '/auth/test', + '/health' + ]; + const isPublicEndpoint = publicEndpoints.some((url) => req.url.includes(url)); + + if (token && !isPublicEndpoint) { + headersConfig['Authorization'] = `Bearer ${token}`; + } + + const authReq = req.clone({ setHeaders: headersConfig }); + + return next.handle(authReq).pipe( + catchError((err) => { + if (err instanceof HttpErrorResponse && err.status === 401 && !isPublicEndpoint) { + return from(this.handle401Error(authReq, next)); + } + return throwError(() => err); + }) + ); + } + + private async handle401Error(request: HttpRequest, next: HttpHandler): Promise> { + try { + await this.authService.refreshToken(); + const newToken = this.authService.getAccessToken(); + if (newToken) { + const retriedReq = request.clone({ + setHeaders: { + Authorization: `Bearer ${newToken}`, + 'Fineract-Platform-TenantId': this.FINERACT_PLATFORM_TENANT_IDENTIFIER, + 'Content-Type': request.headers.get('Content-Type') || 'application/json' + } + }); + return next.handle(retriedReq).toPromise() as Promise>; + } else { + throw new Error('No new access token obtained after refresh'); + } + } catch (e) { + console.error('Error in handle401Error, forcing logout'); + throw e; + } + } +} diff --git a/src/app/zitadel/users/create-user/create-user.component.html b/src/app/zitadel/users/create-user/create-user.component.html new file mode 100644 index 000000000..f9376ae41 --- /dev/null +++ b/src/app/zitadel/users/create-user/create-user.component.html @@ -0,0 +1,205 @@ +
+ +
+ +
+ + + {{ 'labels.inputs.Username' | translate }} + + + {{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + + {{ 'labels.inputs.Email' | translate }} + + + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.invalid' | translate }} + + + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + + {{ 'labels.inputs.First Name' | translate }} + + + + + + {{ 'labels.inputs.Last Name' | translate }} + + + + + + {{ 'labels.inputs.Language' | translate }} + + + {{ 'languages.' + lang.key | translate }} + + + + + + {{ 'labels.inputs.Gender' | translate }} + + {{ 'labels.catalogs.male' | translate }} + {{ 'labels.catalogs.female' | translate }} + {{ 'labels.catalogs.other' | translate }} + + + + +
+ + {{ 'labels.inputs.Code' | translate }} + + + {{ 'countries.' + country.key | translate }} ({{ country.code }}) + + + + + + {{ 'labels.inputs.Phone Number' | translate }} + + +
+ + + {{ 'labels.inputs.Office' | translate }} + + + {{ office.name }} + + + + {{ 'labels.inputs.Office' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Staff' | translate }} + + + {{ staff.displayName }} + + + + + + + {{ 'labels.inputs.Password' | translate }} + + + {{ 'labels.inputs.Password' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + {{ 'labels.commons.Password should be at least' | translate }} + {{ 'labels.commons.12 characters long' | translate }} + + + {{ 'labels.commons.Password should not be more than' | translate }} + {{ 'labels.commons.50 characters long' | translate }} + + + {{ 'labels.commons.Password should include a' | translate }} + {{ 'labels.commons.numeral' | translate }} + {{ 'labels.commons.and at' | translate }} + {{ 'labels.commons.least one uppercase' | translate }}, + {{ 'labels.commons.lowercase and special character' | translate }} + + + + + + {{ 'labels.inputs.Repeat Password' | translate }} + + + {{ 'labels.inputs.Password' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + +

+ {{ 'labels.inputs.Passwords' | translate }} + {{ 'labels.commons.do not match' | translate }} +

+ + + + {{ 'labels.inputs.Roles' | translate }} + + + {{ role.name }} + + + + {{ 'labels.commons.At least one role' | translate }} + {{ 'labels.commons.must be selected' | translate }} + + +
+
+ + + + + +
+
+
+ + +

{{ 'labels.heading.Create User' | translate }}

+

+ {{ 'labels.text.Filling Details' | translate }}: + {{ + 'labels.inputs.Users' | translate + }} +

+
+ + + + +
+
diff --git a/src/app/zitadel/users/create-user/create-user.component.scss b/src/app/zitadel/users/create-user/create-user.component.scss new file mode 100644 index 000000000..81f0b20e2 --- /dev/null +++ b/src/app/zitadel/users/create-user/create-user.component.scss @@ -0,0 +1,13 @@ +.password-never-expires-wrapper { + padding: 17.5px 0; +} + +.send-password-to-email-wrapper { + padding: 17.5px 0; +} + +.reset-password-error { + color: #f44336; + font-size: 80%; + margin: 0; +} diff --git a/src/app/zitadel/users/create-user/create-user.component.spec.ts b/src/app/zitadel/users/create-user/create-user.component.spec.ts new file mode 100644 index 000000000..c2f14c7ec --- /dev/null +++ b/src/app/zitadel/users/create-user/create-user.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateUserComponent } from './create-user.component'; + +describe('CreateUserComponent', () => { + let component: CreateUserComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CreateUserComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateUserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/zitadel/users/create-user/create-user.component.ts b/src/app/zitadel/users/create-user/create-user.component.ts new file mode 100644 index 000000000..d6cad47bf --- /dev/null +++ b/src/app/zitadel/users/create-user/create-user.component.ts @@ -0,0 +1,353 @@ +/** Angular Imports */ +import { Component, OnInit, TemplateRef, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; +import { + UntypedFormGroup, + UntypedFormBuilder, + UntypedFormControl, + Validators, + ReactiveFormsModule +} from '@angular/forms'; +import { Router, ActivatedRoute, RouterModule } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; + +/** Custom Services */ +//import { UsersService } from '../users.service'; +import { UsersServiceZitadel } from '../usersZitadel.service'; +import { PopoverService } from '../../../configuration-wizard/popover/popover.service'; + +/** Custom Dialog Component */ +import { PasswordsUtility } from 'app/core/utils/passwords-utility'; +import { confirmPasswordValidator } from 'app/login/reset-password/confirm-password.validator'; +import { ConfigurationWizardService } from 'app/configuration-wizard/configuration-wizard.service'; +import { ContinueSetupDialogComponent } from 'app/configuration-wizard/continue-setup-dialog/continue-setup-dialog.component'; +import { UsersService } from 'app/users/users.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { COUNTRY_CODES } from 'app/zitadel/constants/coutry-codes'; +import { ZITADEL_LANGUAGES } from 'app/zitadel/constants/languages'; + +/** + * Create user component. + */ +@Component({ + selector: 'mifosx-create-user', + templateUrl: './create-user.component.html', + styleUrls: ['./create-user.component.scss'], + standalone: true, + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class CreateUserComponent implements OnInit, AfterViewInit { + /** User form. */ + userForm: UntypedFormGroup; + /** Offices data. */ + officesData: any; + /** Roles data. */ + rolesData: any; + /** Staff data. */ + staffData: any; + /** Country Codes */ + countryCodes = COUNTRY_CODES; + /** Language Codes */ + languageCodes = ZITADEL_LANGUAGES; + + /* Reference of create user form */ + @ViewChild('userFormRef') userFormRef: ElementRef; + /* Template for popover on create user form */ + @ViewChild('templateUserFormRef') templateUserFormRef: TemplateRef; + + /** + * Retrieves the offices and roles data from `resolve`. + * @param {FormBuilder} formBuilder Form Builder. + * @param {UsersServiceZitadel} UsersService Users Service. + * @param {ActivatedRoute} route Activated Route. + * @param {Router} router Router for navigation. + * @param {ConfigurationWizardService} configurationWizardService ConfigurationWizard Service. + * @param {PopoverService} popoverService PopoverService. + */ + constructor( + private formBuilder: UntypedFormBuilder, + private usersService: UsersServiceZitadel, + private usersService2: UsersService, + private route: ActivatedRoute, + private router: Router, + private popoverService: PopoverService, + private configurationWizardService: ConfigurationWizardService, + private dialog: MatDialog, + private passwordsUtility: PasswordsUtility + ) { + this.route.data.subscribe((data: { usersTemplate: any }) => { + this.officesData = data.usersTemplate.allowedOffices; + this.rolesData = data.usersTemplate.availableRoles; + }); + } + + /** + * Creates the user form, sets the staff data and conditional controls of the user form. + */ + ngOnInit() { + this.createUserForm(); + this.setStaffData(); + this.setConditionalControls(); + } + + /** + * Creates the user form. + */ + createUserForm(): void { + this.userForm = this.formBuilder.group( + { + username: [ + '', + Validators.required + ], + email: [ + '', + [ + Validators.required, + Validators.email + ] + ], + firstName: [ + '', + Validators.required + ], + lastName: [ + '', + Validators.required + ], + preferredLanguage: [ + '', + Validators.required + ], + gender: [ + '', + Validators.required + ], + countryCode: [ + '+1', + Validators.required + ], + phoneNumber: [ + '', + [ + Validators.required, + Validators.pattern(/^[0-9]{7,15}$/)] + ], + password: [ + '', + [ + Validators.required, + Validators.minLength(12), + Validators.maxLength(50), + Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).+$/)] + ], + repeatPassword: [ + '', + Validators.required + ], + roles: [ + '', + Validators.required + ], + officeId: [ + '', + Validators.required + ], + staffId: [''] + }, + { validators: confirmPasswordValidator } + ); + } + + /** + * Sets the staff data each time the user selects a new office + */ + setStaffData() { + this.userForm.get('officeId').valueChanges.subscribe((officeId: string) => { + this.staffData = []; + this.usersService2.getStaff(officeId).subscribe((staff: any) => { + this.staffData = staff; + }); + }); + } + + /** + * Sets the conditional controls of the user form + */ + setConditionalControls() { + this.userForm.get('sendPasswordToEmail').valueChanges.subscribe((sendPasswordToEmail: boolean) => { + const passwordControl = this.userForm.get('password'); + const repeatPasswordControl = this.userForm.get('repeatPassword'); + + if (sendPasswordToEmail) { + passwordControl.disable(); + repeatPasswordControl.disable(); + this.userForm.get('email')?.setValidators([ + Validators.required, + Validators.email + ]); + } else { + passwordControl.enable(); + repeatPasswordControl.enable(); + this.userForm.get('email')?.setValidators([Validators.email]); + } + + this.userForm.get('email')?.updateValueAndValidity(); + }); + } + + /** + * Submits the user form and creates user, + * if successful redirects to view created user. + */ + submit() { + const fullForm = this.userForm.value; + + const fullPhone = `${fullForm.countryCode}${fullForm.phoneNumber}`; + const password = `${fullForm.repeatPassword}`; + const givenName = `${fullForm.firstName}`; + const familyName = `${fullForm.lastName}`; + const nickName = `${fullForm.username}`; + + const user = { + ...fullForm, + phone: fullPhone, + password: password, + givenName: givenName, + familyName: familyName, + nickName: nickName, + displayName: `${fullForm.firstName} ${fullForm.lastName}` + }; + + const dataOffi = { + officeId: fullForm.officeId, + staffId: fullForm.staffId + }; + + const selectedRoleIds = this.userForm.get('roles')?.value; + + delete user.officeId; + delete user.staffId; + delete user.roles; + delete user.countryCode; + delete user.phoneNumber; + delete user.repeatPassword; + delete user.firstName; + delete user.lastName; + + this.usersService.createUser(user).subscribe((response: any) => { + const userId = response.object?.userId; + + if (userId) { + const body = { + id: userId, + officeId: dataOffi.officeId, + staffId: dataOffi.staffId, + username: user.nickName, + firstname: user.givenName, + lastname: user.familyName, + roleIds: selectedRoleIds || [] + }; + + this.usersService.createUserBd(body).subscribe( + (response: any) => { + if (selectedRoleIds?.length > 0) { + this.usersService.assignRolesToUser(userId, selectedRoleIds).subscribe( + () => { + if (this.configurationWizardService.showUsersForm === true) { + this.configurationWizardService.showUsersForm = false; + this.openDialog(); + } else { + this.router.navigate(['/appusers']); + } + }, + (error) => { + console.error('Failed to assign roles to user:', error); + } + ); + } else { + console.warn('No roles were selected for this user.'); + } + }, + (error) => { + console.error('Failed to create user record in database:', error); + } + ); + } else { + console.error('User creation failed: userId not returned by API.'); + } + }); + } + + /** + * Popover function + * @param template TemplateRef. + * @param target HTMLElement | ElementRef. + * @param position String. + * @param backdrop Boolean. + */ + showPopover( + template: TemplateRef, + target: HTMLElement | ElementRef, + position: string, + backdrop: boolean + ): void { + setTimeout(() => this.popoverService.open(template, target, position, backdrop, {}), 200); + } + + /** + * To show popover. + */ + ngAfterViewInit() { + if (this.configurationWizardService.showUsersForm === true) { + setTimeout(() => { + this.showPopover(this.templateUserFormRef, this.userFormRef.nativeElement, 'top', true); + }); + } + } + + /** + * Next Step (Maker Checker Tasks System Page) Configuration Wizard. + */ + nextStep() { + this.configurationWizardService.showUsersForm = false; + this.configurationWizardService.showMakerCheckerTable = true; + this.router.navigate(['/system']); + } + + /** + * Previous Step (Users page) Configuration Wizard. + */ + previousStep() { + this.configurationWizardService.showUsersForm = false; + this.configurationWizardService.showUsersList = true; + this.router.navigate(['/appusers']); + } + + /** + * Opens dialog if the user wants to create more users. + */ + openDialog() { + const continueSetupDialogRef = this.dialog.open(ContinueSetupDialogComponent, { + data: { + stepName: 'user' + } + }); + continueSetupDialogRef.afterClosed().subscribe((response: { step: number }) => { + if (response.step === 1) { + this.configurationWizardService.showUsersForm = false; + this.router.navigate(['../'], { relativeTo: this.route }); + } else if (response.step === 2) { + this.configurationWizardService.showUsersForm = true; + this.router.routeReuseStrategy.shouldReuseRoute = () => false; + this.router.onSameUrlNavigation = 'reload'; + this.router.navigate(['/organization/users/create']); + } else if (response.step === 3) { + this.configurationWizardService.showUsersForm = false; + this.configurationWizardService.showMakerCheckerTable = true; + this.router.navigate(['/system']); + } + }); + } +} diff --git a/src/app/zitadel/users/edit-user/edit-user.component.html b/src/app/zitadel/users/edit-user/edit-user.component.html new file mode 100644 index 000000000..5002c242a --- /dev/null +++ b/src/app/zitadel/users/edit-user/edit-user.component.html @@ -0,0 +1,125 @@ +
+ +
+ +
+ + {{ 'labels.inputs.Username' | translate }} + + + {{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Email' | translate }} + + + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.invalid' | translate }} + + + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.First Name' | translate }} + + + {{ 'labels.inputs.First Name' | translate }} + {{ 'labels.commons.cannot begin with a special character or number' | translate }} + + + {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Last Name' | translate }} + + + {{ 'labels.inputs.Last Name' | translate }} + {{ 'labels.commons.cannot begin with a special character or number' | translate }} + + + {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Code' | translate }} + + + {{ 'countries.' + country.key | translate }} ({{ country.code }}) + + + + + + {{ 'labels.inputs.Phone Number' | translate }} + + + + + {{ 'labels.inputs.Gender' | translate }} + + {{ 'labels.catalogs.male' | translate }} + {{ 'labels.catalogs.female' | translate }} + {{ 'labels.catalogs.other' | translate }} + + + + + {{ 'labels.inputs.Language' | translate }} + + + {{ 'languages.' + lang.key | translate }} + + + + + + {{ 'labels.inputs.Office' | translate }} + + + {{ office.name }} + + + + + + {{ 'labels.inputs.Staff' | translate }} + + + {{ staff.displayName }} + + + + + + {{ 'labels.inputs.Roles' | translate }} + + + {{ rol.name }} + + + + + + + + +
+
+
+
+
diff --git a/src/app/zitadel/users/edit-user/edit-user.component.scss b/src/app/zitadel/users/edit-user/edit-user.component.scss new file mode 100644 index 000000000..d042d1108 --- /dev/null +++ b/src/app/zitadel/users/edit-user/edit-user.component.scss @@ -0,0 +1,3 @@ +.password-never-expires-wrapper { + padding: 17.5px 0; +} diff --git a/src/app/zitadel/users/edit-user/edit-user.component.spec.ts b/src/app/zitadel/users/edit-user/edit-user.component.spec.ts new file mode 100644 index 000000000..a8d5bb071 --- /dev/null +++ b/src/app/zitadel/users/edit-user/edit-user.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditUserComponent } from './edit-user.component'; + +describe('EditUserComponent', () => { + let component: EditUserComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [EditUserComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditUserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/zitadel/users/edit-user/edit-user.component.ts b/src/app/zitadel/users/edit-user/edit-user.component.ts new file mode 100644 index 000000000..d6b3bc2ef --- /dev/null +++ b/src/app/zitadel/users/edit-user/edit-user.component.ts @@ -0,0 +1,220 @@ +/** Angular Imports */ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +/** Custom Services */ +import { UsersServiceZitadel } from '../usersZitadel.service'; +import { UsersService } from 'app/users/users.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { COUNTRY_CODES } from 'app/zitadel/constants/coutry-codes'; +import { ZITADEL_LANGUAGES } from 'app/zitadel/constants/languages'; +/** + * Edit User Component. + */ +@Component({ + selector: 'mifosx-edit-user', + templateUrl: './edit-user.component.html', + styleUrls: ['./edit-user.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS + ] +}) +export class EditUserComponent implements OnInit { + /** User Data */ + userData: any; + /** Offices Data */ + officesData: any; + /** Staff Data */ + staffData: any; + /** Roles Data */ + rolesData: any; + /** Edit User form. */ + editUserForm: UntypedFormGroup; + /** Country Codes */ + countryCodes = COUNTRY_CODES; + /** Language Codes */ + languageCodes = ZITADEL_LANGUAGES; + + /** + * Retrieves the offices data from `resolve`. + * @param {FormBuilder} formBuilder Form Builder. + * @param {UsersServiceZitadel} UsersServiceZitadel Users Service. + * @param {ActivatedRoute} route Activated Route. + * @param {UsersService} UsersService Users Service. + * @param {ConfigurationWizardService} configurationWizardService ConfigurationWizard Service. + * @param {Router} router Router for navigation. + */ + constructor( + private formBuilder: UntypedFormBuilder, + private UsersServiceZitadel: UsersServiceZitadel, + private UsersService: UsersService, + private route: ActivatedRoute, + private router: Router + ) { + this.route.data.subscribe((data: { user: any; usersTemplate: any }) => { + const fullUserData = data.user; + this.userData = fullUserData.object?.result?.[0] || {}; + this.officesData = data.usersTemplate.allowedOffices; + this.rolesData = data.usersTemplate.availableRoles; + this.UsersServiceZitadel.getExtraUserData(this.userData.id).subscribe((resp: any) => { + const data = resp.object; + this.userData = { + ...this.userData, + userName: data.username_zitadel, + officeId: data.office_id, + staffId: data.staff_id, + selectedRoles: data.roles || [] + }; + this.createEditUserForm(); + this.officeChanged(this.userData.officeId); + this.UsersServiceZitadel.getRoles().subscribe((response: any) => { + const rolesZitadel = response.object?.result || []; + this.rolesData = rolesZitadel.map((r: any) => ({ + id: r.key, + name: r.displayName + })); + }); + }); + }); + } + + // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method + ngOnInit() {} + + createEditUserForm() { + const profile = this.userData?.human?.profile ?? {}; + const email = this.userData?.human?.email?.email || ''; + const phone = this.userData?.human?.phone?.phone || ''; + const defaultCode = '+1'; + let countryCode = defaultCode; + let phoneNumber = phone; + const gender = profile?.gender && profile.gender !== '' ? profile.gender : 'GENDER_MALE'; + const preferredLanguage = profile.preferredLanguage || 'es'; + + for (const c of this.countryCodes) { + if (phone.startsWith(c.code)) { + countryCode = c.code; + phoneNumber = phone.replace(c.code, ''); + break; + } + } + + this.editUserForm = this.formBuilder.group({ + username: [ + this.userData.userName, + Validators.required + ], + email: [ + email, + [ + Validators.required, + Validators.email + ] + ], + firstname: [ + profile.firstName || '', + [ + Validators.required, + Validators.pattern('(^[A-z]).*')] + ], + lastname: [ + profile.lastName || '', + [ + Validators.required, + Validators.pattern('(^[A-z]).*')] + ], + countryCode: [ + countryCode, + Validators.required + ], + phoneNumber: [ + phoneNumber, + Validators.required + ], + gender: [ + gender, + Validators.required + ], + preferredLanguage: [ + preferredLanguage, + Validators.required + ], + officeId: [ + this.userData.officeId, + Validators.required + ], + staffId: [ + this.userData.staffId || null + ], + roles: [ + this.userData.selectedRoles.map((role: any) => role.id.toString()), + Validators.required + ] + }); + } + + /** + * Fetches the staff for the selected office + * @param officeId the selected office id + */ + officeChanged(officeId: number | undefined) { + if (!officeId) { + console.warn('No officeId provided to load staff.'); + return; + } + + this.staffData = []; + this.UsersService.getStaff(officeId).subscribe((staff: any) => { + this.staffData = staff; + }); + } + + /** + * Submits the user form and edits the user, + * if successful redirects to the updated user. + */ + submit() { + const form = this.editUserForm.value; + const fullPhone = `${form.countryCode}${form.phoneNumber}`; + + const userPayload: any = { + userId: this.userData.id, + email: { + email: form.email, + isVerified: true + }, + phone: { + phone: fullPhone, + isVerified: true + }, + profile: { + username: form.username, + givenName: form.firstname, + familyName: form.lastname, + displayName: `${form.firstname} ${form.lastname}`, + nickName: form.firstname, + preferredLanguage: form.preferredLanguage, + gender: form.gender + } + }; + + const rolesPayload = { + userId: this.userData.id, + roleKeys: this.editUserForm.value.roles + }; + + const officePayload = { + userId: this.userData.id, + officeId: form.officeId, + staffId: form.staffId + }; + + this.UsersServiceZitadel.editUser(userPayload).subscribe(); + + this.UsersServiceZitadel.editRoles(rolesPayload).subscribe(); + + this.UsersServiceZitadel.editOffice(officePayload).subscribe((response: any) => { + this.router.navigate(['/appusers']); + }); + } +} diff --git a/src/app/zitadel/users/userZitadel.resolver.ts b/src/app/zitadel/users/userZitadel.resolver.ts new file mode 100644 index 000000000..3e0303c62 --- /dev/null +++ b/src/app/zitadel/users/userZitadel.resolver.ts @@ -0,0 +1,30 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { UsersServiceZitadel } from './usersZitadel.service'; + +/** + * UsersZitadel data resolver. + */ +@Injectable() +export class UserZitadelResolver { + /** + * @param {UsersServiceZitadel} usersServiceZitadel Users service. + */ + constructor(private usersServiceZitadel: UsersServiceZitadel) {} + + /** + * Returns the user data. + * @returns {Observable} + */ + + resolve(route: ActivatedRouteSnapshot): Observable { + const userId = route.paramMap.get('id'); + return this.usersServiceZitadel.getUser(userId); + } +} diff --git a/src/app/zitadel/users/users.component.html b/src/app/zitadel/users/users.component.html new file mode 100644 index 000000000..1d0736973 --- /dev/null +++ b/src/app/zitadel/users/users.component.html @@ -0,0 +1,47 @@ +
+
+ +
+
+ +
+
+ + {{ 'labels.inputs.Filter' | translate }} + + +
+
+ +
+

{{ 'labels.heading.Users' | translate }}

+ + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.First Name' | translate }}{{ user.firstname }}{{ 'labels.inputs.Last Name' | translate }}{{ user.lastname }}{{ 'labels.inputs.Email' | translate }}{{ user.email }}{{ 'labels.inputs.Office' | translate }}{{ user.officeName }}
+ + +
diff --git a/src/app/zitadel/users/users.component.scss b/src/app/zitadel/users/users.component.scss new file mode 100644 index 000000000..8dfce208d --- /dev/null +++ b/src/app/zitadel/users/users.component.scss @@ -0,0 +1,11 @@ +table { + width: 100%; + + .select-row:hover { + cursor: pointer; + } +} + +.container-margin { + margin-top: 1rem; +} diff --git a/src/app/zitadel/users/users.component.ts b/src/app/zitadel/users/users.component.ts new file mode 100644 index 000000000..3c400b9c3 --- /dev/null +++ b/src/app/zitadel/users/users.component.ts @@ -0,0 +1,126 @@ +import { MatPaginatorModule } from '@angular/material/paginator'; +/** Angular Imports */ +import { Component, OnInit, TemplateRef, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, MatSortModule } from '@angular/material/sort'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; + +/** Custom Services */ +import { PopoverService } from '../../configuration-wizard/popover/popover.service'; +import { ConfigurationWizardService } from '../../configuration-wizard/configuration-wizard.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; + +/** + * Users component. + */ +@Component({ + selector: 'mifosx-users', + templateUrl: './users.component.html', + styleUrls: ['./users.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FontAwesomeModule, + MatPaginatorModule, + MatSortModule, + MatTableModule + ] +}) +export class UsersComponent implements OnInit, AfterViewInit { + /** User data */ + usersData: any[] = []; + usersZitadelData: any[] = []; + + /** Columns */ + displayedColumns: string[] = [ + 'firstname', + 'lastname', + 'email', + 'officeName' + ]; + + /** DataSources */ + dataSource = new MatTableDataSource(); + dataSourceZitadel = new MatTableDataSource(); + + /** References for pagination and sorting */ + @ViewChild('paginatorUsers', { static: true }) paginatorUsers!: MatPaginator; + @ViewChild('sortUsers', { static: true }) sortUsers!: MatSort; + + @ViewChild('paginatorZitadel', { static: true }) paginatorZitadel!: MatPaginator; + @ViewChild('sortZitadel', { static: true }) sortZitadel!: MatSort; + + /* Buttons and popovers */ + @ViewChild('buttonCreateUser') buttonCreateUser!: ElementRef; + @ViewChild('templateButtonCreateUser') templateButtonCreateUser!: TemplateRef; + @ViewChild('usersTable') usersTable!: ElementRef; + @ViewChild('templateUsersTable') templateUsersTable!: TemplateRef; + + constructor( + private route: ActivatedRoute, + private router: Router, + public configurationWizardService: ConfigurationWizardService, + private popoverService: PopoverService + ) {} + + ngOnInit() { + // Get data from resolver + this.route.data.subscribe((data: { users: any; usersZitadel: any }) => { + this.usersData = data.users || []; + this.usersZitadelData = data.usersZitadel || []; + + // Initialize tables + this.dataSource = new MatTableDataSource(this.usersData); + this.dataSourceZitadel = new MatTableDataSource(this.usersZitadelData); + + // Assign paginator and sort AFTER having data + this.dataSource.paginator = this.paginatorUsers; + this.dataSource.sort = this.sortUsers; + + this.dataSourceZitadel.paginator = this.paginatorZitadel; + this.dataSourceZitadel.sort = this.sortZitadel; + }); + } + + /** Filter tables */ + applyFilter(filterValue: string) { + const filter = filterValue.trim().toLowerCase(); + this.dataSource.filter = filter; + this.dataSourceZitadel.filter = filter; + } + + /** Show popover */ + showPopover(template: TemplateRef, target: HTMLElement | ElementRef, position: string, backdrop: boolean) { + setTimeout(() => this.popoverService.open(template, target, position, backdrop, {}), 200); + } + + ngAfterViewInit() { + if (this.configurationWizardService.showUsers) { + setTimeout(() => { + this.showPopover(this.templateButtonCreateUser, this.buttonCreateUser.nativeElement, 'bottom', true); + }); + } + + if (this.configurationWizardService.showUsersList) { + setTimeout(() => { + this.showPopover(this.templateUsersTable, this.usersTable.nativeElement, 'top', true); + }); + } + } + + /** Navigate */ + nextStep() { + this.configurationWizardService.showUsers = false; + this.configurationWizardService.showUsersList = false; + this.configurationWizardService.showUsersForm = true; + this.router.navigate(['/appusers/create']); + } + + previousStep() { + this.configurationWizardService.showUsers = false; + this.configurationWizardService.showUsersList = false; + this.configurationWizardService.showRolesandPermissionList = true; + this.router.navigate(['/system/roles-and-permissions']); + } +} diff --git a/src/app/zitadel/users/usersZitadel-template.resolver.ts b/src/app/zitadel/users/usersZitadel-template.resolver.ts new file mode 100644 index 000000000..74e4f8446 --- /dev/null +++ b/src/app/zitadel/users/usersZitadel-template.resolver.ts @@ -0,0 +1,26 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { UsersServiceZitadel } from './usersZitadel.service'; +/** + * Users template data resolver. + */ +@Injectable() +export class UsersZitadelTemplateResolver { + /** + * @param { UsersServiceZitadel } usersServiceZitadel Users service. + */ + constructor(private usersServiceZitadel: UsersServiceZitadel) {} + + /** + * Returns the users template data. + * @returns {Observable} + */ + resolve(): Observable { + return this.usersServiceZitadel.getUsersTemplate(); + } +} diff --git a/src/app/zitadel/users/usersZitadel.resolver.ts b/src/app/zitadel/users/usersZitadel.resolver.ts new file mode 100644 index 000000000..f19e9b11e --- /dev/null +++ b/src/app/zitadel/users/usersZitadel.resolver.ts @@ -0,0 +1,29 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; + +/** rxjs Imports */ +import { Observable } from 'rxjs'; + +/** Custom Services */ +import { UsersServiceZitadel } from './usersZitadel.service'; + +/** + * UsersZitadel data resolver. + */ + +@Injectable() +export class UsersZitadelResolver { + /** + * @param {UsersServiceZitadel} usersServiceZitadel Users service. + */ + constructor(private usersServiceZitadel: UsersServiceZitadel) {} + + /** + * Returns the users data. + * @returns {Observable} + */ + + resolve(): Observable { + return this.usersServiceZitadel.getUsers(); + } +} diff --git a/src/app/zitadel/users/usersZitadel.service.ts b/src/app/zitadel/users/usersZitadel.service.ts new file mode 100644 index 000000000..92df2d250 --- /dev/null +++ b/src/app/zitadel/users/usersZitadel.service.ts @@ -0,0 +1,129 @@ +/** Angular Imports */ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; + +/** rxjs Imports */ +import { from, Observable, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { AuthService } from '../auth.service'; +import { environment } from '../../../environments/environment'; + +/** + * UsersZitadel service. + */ +@Injectable({ + providedIn: 'root' +}) +export class UsersServiceZitadel { + private api = environment.OIDC.oidcApiUrl; + + /** + * @param {HttpClient} http Http Client to send requests. + */ + constructor( + private http: HttpClient, + private autservice: AuthService + ) {} + + /** + * @param {any} user User to be created. + * @returns {Observable} + */ + createUser(user: any): Observable { + return this.http.post(this.api + 'authentication/user', user); + } + + getExtraUserData(userId: string): Observable { + return this.http.get(`${this.api}authentication/user/db/${userId}`); + } + + assignRolesToUser(userId: string, roleKeys: string[]): Observable { + const payload = { + userId: userId, + roleKeys: roleKeys.map(String) + }; + return this.http.post(this.api + 'authentication/user/role', payload); + } + + createUserBd(user: any): Observable { + return this.http.post(this.api + 'authentication/user/db', user); + } + + /** + * @returns {Observable} Users template data + */ + getUsersTemplate(): Observable { + return this.http.get('/users/template'); + } + + /** + * @returns {Observable} Users data + */ + getUsers(): Observable { + const token = this.autservice.getAccessToken(); + return from( + fetch(`${this.api}authentication/user`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + ).pipe( + switchMap((res) => res.json()), + map((response) => { + const out: any[] = []; + const users = response.object?.result; + if (Array.isArray(users)) { + users.forEach((user) => { + if (user.human) { + out.push({ + id: user.id, + firstname: user.human.profile.firstName, + lastname: user.human.profile.lastName, + email: user.human.email.email, + officeName: 'Head Office' + }); + } + }); + } + return out; + }) + ); + } + + /** + * @param {string} userId user ID of user. + * @returns {Observable} User. + */ + getUser(userId: string): Observable { + const url = `${this.api}authentication/user/${userId}`; + return from( + fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.autservice.getAccessToken()}` + } + }) + ).pipe( + switchMap((res) => res.json()), + map((response) => response) + ); + } + + editUser(user: any): Observable { + return this.http.put(this.api + 'authentication/user', user); + } + + editRoles(roles: any): Observable { + return this.http.put(this.api + 'authentication/user/role', roles); + } + + editOffice(office: any): Observable { + return this.http.put(this.api + 'authentication/user/office', office); + } + + getRoles() { + return this.http.get(this.api + 'authentication/role'); + } +} diff --git a/src/app/zitadel/users/view-user/view-user.component.html b/src/app/zitadel/users/view-user/view-user.component.html new file mode 100644 index 000000000..379e2c941 --- /dev/null +++ b/src/app/zitadel/users/view-user/view-user.component.html @@ -0,0 +1,96 @@ +
+ + +
+ +
+
+ + + +
+
+ +
+ + +
+
+ {{ 'labels.inputs.Login Name' | translate }} +
+ +
+ {{ userData.username }} +
+ +
+ {{ 'labels.inputs.First Name' | translate }} +
+ +
+ {{ userData.firstname }} +
+ +
+ {{ 'labels.inputs.Last Name' | translate }} +
+ +
+ {{ userData.lastname }} +
+ +
+ {{ 'labels.inputs.Email' | translate }} +
+ +
+ {{ userData.email }} +
+ +
+ {{ 'labels.inputs.Office' | translate }} +
+ +
+ {{ userData.officeName }} +
+ +
+ {{ 'labels.inputs.Roles' | translate }} +
+ +
+ + {{ role.name }} + +
+
+ +
+ +
+
+
+
diff --git a/src/app/zitadel/users/view-user/view-user.component.scss b/src/app/zitadel/users/view-user/view-user.component.scss new file mode 100644 index 000000000..b154f3f2b --- /dev/null +++ b/src/app/zitadel/users/view-user/view-user.component.scss @@ -0,0 +1,14 @@ +.container { + max-width: 37rem; + + .content { + div { + margin: 1rem 0; + word-wrap: break-word; + + span { + display: block; + } + } + } +} diff --git a/src/app/zitadel/users/view-user/view-user.component.spec.ts b/src/app/zitadel/users/view-user/view-user.component.spec.ts new file mode 100644 index 000000000..1d235f02b --- /dev/null +++ b/src/app/zitadel/users/view-user/view-user.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ViewUserComponent as ViewUserZitadelComponent } from './view-user.component'; + +describe('ViewUserComponent', () => { + let component: ViewUserZitadelComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ViewUserZitadelComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ViewUserZitadelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/zitadel/users/view-user/view-user.component.ts b/src/app/zitadel/users/view-user/view-user.component.ts new file mode 100644 index 000000000..35bacb2ee --- /dev/null +++ b/src/app/zitadel/users/view-user/view-user.component.ts @@ -0,0 +1,113 @@ +/** Angular Imports */ +import { Component } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; + +/** Custom Services */ +//import { UsersService } from '../users.service'; +import { UsersServiceZitadel } from '../usersZitadel.service'; + +/** aUTH zITADEL */ +import { AuthService } from 'app/zitadel/auth.service'; + +/** Custom Components */ +import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; +import { ChangePasswordDialogComponent } from 'app/zitadel/shared/change-password-dialog/change-password-dialog.component'; +import { strings } from '@angular-devkit/schematics'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; + +/** + * View user component. + */ +@Component({ + selector: 'mifosx-view-user', + templateUrl: './view-user.component.html', + styleUrls: ['./view-user.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FontAwesomeModule + ] +}) +export class ViewUserComponent { + /** User Data. */ + userData: any; + + /** + * Retrieves the user data from `resolve`. + * @param {UsersServiceZitadel} usersServiceZitadel Users Service. + * @param {ActivatedRoute} route Activated Route. + * @param {Router} router Router for navigation. + * @param {MatDialog} dialog Dialog reference. + */ + constructor( + private usersService: UsersServiceZitadel, + private route: ActivatedRoute, + private router: Router, + private dialog: MatDialog, + private authService: AuthService + ) { + this.route.data.subscribe((data: { user: any }) => { + const u = data.user.object?.result?.[0]; + + const user = { + id: u.id, + username: u.userName, + firstname: u.human?.profile?.firstName, + lastname: u.human?.profile?.lastName, + officeName: 'Head Office', + officeId: u.user_uuid ?? null, + availableRoles: u.availableRoles ?? 'Admin', + selectedRoles: u.selectedRoles ?? 'Admin', + isSelfServiceUser: u.state + }; + + this.userData = user; + }); + } + + /** + * Deletes the user and redirects to users. + */ + delete() { + const deleteUserDialogRef = this.dialog.open(DeleteDialogComponent, { + data: { deleteContext: `user ${this.userData.id}` } + }); + + deleteUserDialogRef.afterClosed().subscribe((response: any) => { + if (response.delete) { + this.authService.deletUser(this.userData.id); + } + }); + } + + /** + * Change Password of the Users. + */ + changeUserPassword(userId: string) { + const changeUserPasswordDialogRef = this.dialog.open(ChangePasswordDialogComponent, { + width: '440px', + data: { id: userId } + }); + changeUserPasswordDialogRef.afterClosed().subscribe((response: any) => { + if (response.password && response.repeatPassword) { + const password = response.password; + const repeatPassword = response.repeatPassword; + const firstname = this.userData.firstname; + const data = { password: password, repeatPassword: repeatPassword, firstname: firstname }; + } + }); + } + + /** + * disable User + */ + + desactiveUser(userId: string) { + this.authService.desactiveUser(userId); + } + + activeUser(userId: string) { + this.authService.activeUser(userId); + } +} diff --git a/src/assets/env.js b/src/assets/env.js index 334e1c240..9d9fdb120 100644 --- a/src/assets/env.js +++ b/src/assets/env.js @@ -35,13 +35,20 @@ // Time in milliseconds for Session idle timeout, default 300000 seconds window['env']['sessionIdleTimeout'] = '0'; - // OAuth Server Enabled + // OAuth Server Enabled window['env']['oauthServerEnabled'] = ''; - // OAuth Server URL + // OAuth Server URL window['env']['oauthServerUrl'] = ''; - // OAuth Client Id + // OAuth Client Id window['env']['oauthAppId'] = ''; + // OIDC Plugin Environment variables + window['env']['oidcServerEnabled'] = false; + window['env']['oidcBaseUrl'] = ''; + window['env']['oidcClientId'] = ''; + window['env']['oidcApiUrl'] = ''; + window['env']['oidcFrontUrl'] = ''; + })(this); diff --git a/src/assets/env.template.js b/src/assets/env.template.js index 54ae9470b..7c5373e47 100644 --- a/src/assets/env.template.js +++ b/src/assets/env.template.js @@ -36,7 +36,7 @@ window['env']['sessionIdleTimeout'] = '$MIFOS_SESSION_IDLE_TIMEOUT'; // OAuth Server Enabled - window['env']['oauthServerEnabled'] = '$MIFOS_OAUTH_SERVER_ENABLED'; + window['env']['oauthServerEnabled'] = $MIFOS_OAUTH_SERVER_ENABLED; // OAuth Server URL window['env']['oauthServerUrl'] = '$MIFOS_OAUTH_SERVER_URL'; @@ -54,4 +54,11 @@ window['env']['vNextApiProvider'] = '$VNEXT_API_PROVIDER'; window['env']['vNextApiVersion'] = '$VNEXT_API_VERSION'; window['env']['interbankTransfers'] = '$VNEXT_INTERBANK_TRANSFERS'; + + // OIDC Plugin Environment variables + window['env']['oidcServerEnabled'] = $FINERACT_PLUGIN_OIDC_ENABLED; + window['env']['oidcBaseUrl'] = '$FINERACT_PLUGIN_OIDC_BASE_URL'; + window['env']['oidcClientId'] = '$FINERACT_PLUGIN_OIDC_CLIENT_ID'; + window['env']['oidcApiUrl'] = '$FINERACT_PLUGIN_OIDC_API_URL'; + window['env']['oidcFrontUrl'] = '$FINERACT_PLUGIN_OIDC_FRONTEND_URL'; })(this); diff --git a/src/assets/translations/cs-CS.json b/src/assets/translations/cs-CS.json index 113f23d49..4c9fc880e 100644 --- a/src/assets/translations/cs-CS.json +++ b/src/assets/translations/cs-CS.json @@ -2177,6 +2177,9 @@ "Repayments": "Splátky", "Repayments Every": "Splátky Každý", "Repayments Scheduled To": "Splátky naplánovány na", + "Password Current": "Aktuální heslo", + "Password Current Required": "Aktuální heslo je povinné", + "Password New": "Nové heslo", "Repeat Password": "Zopakovat heslo", "Repeats": "Opakuje se", "Repeats Every": "Opakuje se každý", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "Heslo musí obsahovat alespoň jeden speciální znak (@$!%*?&)", "Password minimun length must be": "Minimální délka hesla musí být", "Passwords do not match": "Hesla se neshodují", + "Unable to update password": "Nelze aktualizovat heslo", + "The current password is not correct": "Aktuální heslo není správné", + "New password cannot be the same as your current password": "Nové heslo nemůže být stejné jako vaše aktuální heslo", "Enable income capitalization": "Povolit kapitalizaci příjmu", "Income capitalization calculation type": "Typ výpočtu kapitalizace příjmu", "Income capitalization strategy": "Strategie kapitalizace příjmu", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "Datum, kdy se úvěrový produkt stane neaktivním a nedostupným pro klienty. Pokud je prázdné, produkt zatížení se nikdy nestane neaktivním.", "Refund transactions where interest refund will automatically be calculated": "Transakce s vrácením peněz, kde bude automaticky vypočítána refundace úroků", "Create Interest Refund": "Vytvořit vrácení úroků" + }, + "countries": { + "AF": "Afghánistán", + "AL": "Albánie", + "DZ": "Alžírsko", + "AD": "Andorra", + "AO": "Angola", + "AR": "Argentina", + "AM": "Arménie", + "AU": "Austrálie", + "AT": "Rakousko", + "AZ": "Ázerbájdžán", + "BH": "Bahrajn", + "BD": "Bangladéš", + "BY": "Bělorusko", + "BE": "Belgie", + "BO": "Bolívie", + "BA": "Bosna a Hercegovina", + "BR": "Brazílie", + "BG": "Bulharsko", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Kambodža", + "CM": "Kamerun", + "CA": "Kanada", + "CL": "Chile", + "CN": "Čína", + "CO": "Kolumbie", + "CR": "Kostarika", + "HR": "Chorvatsko", + "CU": "Kuba", + "CY": "Kypr", + "CZ": "Česko", + "DK": "Dánsko", + "EG": "Egypt", + "SV": "Salvador", + "ES": "Španělsko", + "EE": "Estonsko", + "ET": "Etiopie", + "FI": "Finsko", + "FR": "Francie", + "GE": "Gruzie", + "DE": "Německo", + "GH": "Ghana", + "GR": "Řecko", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Maďarsko", + "IS": "Island", + "IN": "Indie", + "ID": "Indonésie", + "IQ": "Irák", + "IR": "Írán", + "IE": "Irsko", + "IL": "Izrael", + "IT": "Itálie", + "JP": "Japonsko", + "JO": "Jordánsko", + "KZ": "Kazachstán", + "KE": "Keňa", + "KW": "Kuvajt", + "KG": "Kyrgyzstán", + "LV": "Lotyšsko", + "LB": "Libanon", + "LY": "Libye", + "LT": "Litva", + "LU": "Lucembursko", + "MK": "Severní Makedonie", + "MY": "Malajsie", + "MX": "Mexiko", + "MD": "Moldavsko", + "MC": "Monako", + "MN": "Mongolsko", + "MA": "Maroko", + "MZ": "Mosambik", + "NP": "Nepál", + "NL": "Nizozemsko", + "NZ": "Nový Zéland", + "NI": "Nikaragua", + "NG": "Nigérie", + "NO": "Norsko", + "PK": "Pákistán", + "PA": "Panama", + "PY": "Paraguay", + "PE": "Peru", + "PH": "Filipíny", + "PL": "Polsko", + "PT": "Portugalsko", + "QA": "Katar", + "RO": "Rumunsko", + "RU": "Rusko", + "SA": "Saúdská Arábie", + "SN": "Senegal", + "RS": "Srbsko", + "SG": "Singapur", + "SK": "Slovensko", + "SI": "Slovinsko", + "ZA": "Jihoafrická republika", + "KR": "Jižní Korea", + "LK": "Srí Lanka", + "SE": "Švédsko", + "CH": "Švýcarsko", + "TW": "Tchaj-wan", + "TH": "Thajsko", + "TR": "Turecko", + "UG": "Uganda", + "UA": "Ukrajina", + "AE": "Spojené arabské emiráty", + "GB": "Spojené království", + "US": "Spojené státy", + "UY": "Uruguay", + "UZ": "Uzbekistán", + "VE": "Venezuela", + "VN": "Vietnam", + "YE": "Jemen", + "ZM": "Zambie", + "ZW": "Zimbabwe" } } diff --git a/src/assets/translations/de-DE.json b/src/assets/translations/de-DE.json index 18e026104..3c54f1975 100644 --- a/src/assets/translations/de-DE.json +++ b/src/assets/translations/de-DE.json @@ -2177,6 +2177,9 @@ "Repayments": "Rückzahlungen", "Repayments Every": "Rückzahlungen alle", "Repayments Scheduled To": "Rückzahlungen geplant bis", + "Password Current": "Aktuelles Passwort", + "Password Current Required": "Aktuelles Passwort erforderlich", + "Password New": "Neues Passwort", "Repeat Password": "Passwort wiederholen", "Repeats": "Wiederholt", "Repeats Every": "Wird alle wiederholt", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "Das Passwort muss mindestens ein Sonderzeichen enthalten (@$!%*?&)", "Password minimun length must be": "Die Mindestlänge des Passworts muss betragen", "Passwords do not match": "Passwörter stimmen nicht überein", + "Unable to update password": "Passwort konnte nicht aktualisiert werden", + "The current password is not correct": "Das aktuelle Passwort ist nicht korrekt", + "New password cannot be the same as your current password": "Das neue Passwort darf nicht dasselbe wie Ihr aktuelles Passwort sein", "Enable income capitalization": "Aktivieren Sie die Einkommenskapitalisierung", "Income capitalization calculation type": "Berechnungsart der Einkommenskapitalisierung", "Income capitalization strategy": "Strategie zur Ertragskapitalisierung", @@ -3535,5 +3541,122 @@ "loan product will become inactive and unavailable to clients": "Das Datum, an dem das Kreditprodukt inaktiv wird und für Kunden nicht mehr verfügbar ist. Wenn es leer ist, wird das Ladeprodukt niemals inaktiv.", "Refund transactions where interest refund will automatically be calculated": "Rückerstattungstransaktionen, bei denen die Zinsrückerstattung automatisch berechnet wird", "Create Interest Refund": "Zinsrückerstattung erstellen" + }, + "countries": { + "AF": "Afghanistan", + "AL": "Albanien", + "DZ": "Algerien", + "AD": "Andorra", + "AO": "Angola", + "AR": "Argentinien", + "AM": "Armenien", + "AU": "Australien", + "AT": "Österreich", + "AZ": "Aserbaidschan", + "BH": "Bahrain", + "BD": "Bangladesch", + "BY": "Weißrussland", + "BE": "Belgien", + "BO": "Bolivien", + "BA": "Bosnien und Herzegowina", + "BR": "Brasilien", + "BG": "Bulgarien", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Kambodscha", + "CM": "Kamerun", + "CA": "Kanada", + "CL": "Chile", + "CN": "China", + "CO": "Kolumbien", + "CR": "Costa Rica", + "HR": "Kroatien", + "CU": "Kuba", + "CY": "Zypern", + "CZ": "Tschechien", + "DK": "Dänemark", + "EG": "Ägypten", + "SV": "El Salvador", + "ES": "Spanien", + "EE": "Estland", + "ET": "Äthiopien", + "FI": "Finnland", + "FR": "Frankreich", + "GE": "Georgien", + "DE": "Deutschland", + "GH": "Ghana", + "GR": "Griechenland", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Ungarn", + "IS": "Island", + "IN": "Indien", + "ID": "Indonesien", + "IQ": "Irak", + "IR": "Iran", + "IE": "Irland", + "IL": "Israel", + "IT": "Italien", + "JP": "Japan", + "JO": "Jordanien", + "KZ": "Kasachstan", + "KE": "Kenia", + "KW": "Kuwait", + "KG": "Kirgisistan", + "LV": "Lettland", + "LB": "Libanon", + "LY": "Libyen", + "LT": "Litauen", + "LU": "Luxemburg", + "MK": "Nordmazedonien", + "MY": "Malaysia", + "MX": "Mexiko", + "MD": "Moldawien", + "MC": "Monaco", + "MN": "Mongolei", + "MA": "Marokko", + "MZ": "Mosambik", + "NP": "Nepal", + "NL": "Niederlande", + "NZ": "Neuseeland", + "NI": "Nicaragua", + "NG": "Nigeria", + "NO": "Norwegen", + "PK": "Pakistan", + "PA": "Panama", + "PY": "Paraguay", + "PE": "Peru", + "PH": "Philippinen", + "PL": "Polen", + "PT": "Portugal", + "QA": "Katar", + "RO": "Rumänien", + "RU": "Russland", + "SA": "Saudi-Arabien", + "SN": "Senegal", + "RS": "Serbien", + "SG": "Singapur", + "SK": "Slowakei", + "SI": "Slowenien", + "ZA": "Südafrika", + "KR": "Südkorea", + "LK": "Sri Lanka", + "SE": "Schweden", + "CH": "Schweiz", + "TW": "Taiwan", + "TH": "Thailand", + "TR": "Türkei", + "UG": "Uganda", + "UA": "Ukraine", + "AE": "Vereinigte Arabische Emirate", + "GB": "Vereinigtes Königreich", + "US": "Vereinigte Staaten", + "UY": "Uruguay", + "UZ": "Usbekistan", + "VE": "Venezuela", + "VN": "Vietnam", + "YE": "Jemen", + "ZM": "Sambia", + "ZW": "Simbabwe" } } diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 1220bae01..8a3f92879 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -2184,6 +2184,9 @@ "Repayments": "Repayments", "Repayments Every": "Repayments Every", "Repayments Scheduled To": "Repayments Scheduled To", + "Password Current": "Password Current", + "Password Current Required": "Password Current Required", + "Password New": "Password New", "Repeat Password": "Repeat Password", "Repeats": "Repeats", "Repeats Every": "Repeats Every", @@ -2494,6 +2497,9 @@ "Password must contain at least one special character (@$!%*?&)": "Password must contain at least one special character (@$!%*?&)", "Password minimun length must be": "Password minimun length must be", "Passwords do not match": "Passwords do not match", + "Unable to update password": "Unable to update password", + "The current password is not correct": "The current password is not correct", + "New password cannot be the same as your current password": "New password cannot be the same as your current password", "Enable income capitalization": "Enable income capitalization", "Income capitalization calculation type": "Income capitalization calculation type", "Income capitalization strategy": "Income capitalization strategy", @@ -3641,5 +3647,122 @@ "loan product will become inactive and unavailable to clients": "The date that the loan product will become inactive and unavailable to clients. If blank, the load product will never become inactive.", "Refund transactions where interest refund will automatically be calculated": "Refund transactions where interest refund will automatically be calculated", "Create Interest Refund": "Create Interest Refund" + }, + "countries": { + "AF": "Afghanistan", + "AL": "Albania", + "DZ": "Algeria", + "AD": "Andorra", + "AO": "Angola", + "AR": "Argentina", + "AM": "Armenia", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaijan", + "BH": "Bahrain", + "BD": "Bangladesh", + "BY": "Belarus", + "BE": "Belgium", + "BO": "Bolivia", + "BA": "Bosnia and Herzegovina", + "BR": "Brazil", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambodia", + "CM": "Cameroon", + "CA": "Canada", + "CL": "Chile", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "HR": "Croatia", + "CU": "Cuba", + "CY": "Cyprus", + "CZ": "Czechia", + "DK": "Denmark", + "EG": "Egypt", + "SV": "El Salvador", + "ES": "Spain", + "EE": "Estonia", + "ET": "Ethiopia", + "FI": "Finland", + "FR": "France", + "GE": "Georgia", + "DE": "Germany", + "GH": "Ghana", + "GR": "Greece", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Hungary", + "IS": "Iceland", + "IN": "India", + "ID": "Indonesia", + "IQ": "Iraq", + "IR": "Iran", + "IE": "Ireland", + "IL": "Israel", + "IT": "Italy", + "JP": "Japan", + "JO": "Jordan", + "KZ": "Kazakhstan", + "KE": "Kenya", + "KW": "Kuwait", + "KG": "Kyrgyzstan", + "LV": "Latvia", + "LB": "Lebanon", + "LY": "Libya", + "LT": "Lithuania", + "LU": "Luxembourg", + "MK": "North Macedonia", + "MY": "Malaysia", + "MX": "Mexico", + "MD": "Moldova", + "MC": "Monaco", + "MN": "Mongolia", + "MA": "Morocco", + "MZ": "Mozambique", + "NP": "Nepal", + "NL": "Netherlands", + "NZ": "New Zealand", + "NI": "Nicaragua", + "NG": "Nigeria", + "NO": "Norway", + "PK": "Pakistan", + "PA": "Panama", + "PY": "Paraguay", + "PE": "Peru", + "PH": "Philippines", + "PL": "Poland", + "PT": "Portugal", + "QA": "Qatar", + "RO": "Romania", + "RU": "Russia", + "SA": "Saudi Arabia", + "SN": "Senegal", + "RS": "Serbia", + "SG": "Singapore", + "SK": "Slovakia", + "SI": "Slovenia", + "ZA": "South Africa", + "KR": "South Korea", + "LK": "Sri Lanka", + "SE": "Sweden", + "CH": "Switzerland", + "TW": "Taiwan", + "TH": "Thailand", + "TR": "Turkey", + "UG": "Uganda", + "UA": "Ukraine", + "AE": "United Arab Emirates", + "GB": "United Kingdom", + "US": "United States", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VE": "Venezuela", + "VN": "Vietnam", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabwe" } } diff --git a/src/assets/translations/es-CL.json b/src/assets/translations/es-CL.json index 2d5e91104..e916cecf0 100644 --- a/src/assets/translations/es-CL.json +++ b/src/assets/translations/es-CL.json @@ -2177,6 +2177,9 @@ "Repayments": "Reembolsos", "Repayments Every": "Reembolsos cada", "Repayments Scheduled To": "Reembolsos programados para", + "Password Current": "Contraseña actual", + "Password Current Required": "Contraseña actual requerida", + "Password New": "Contraseña nueva", "Repeat Password": "Repita la contraseña", "Repeats": "Se repite", "Repeats Every": "Se repite cada", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "La contraseña debe contener al menos un carácter especial (@$!%*?&)", "Password minimun length must be": "La longitud mínima de la contraseña debe ser", "Passwords do not match": "Las contraseñas no coinciden", + "Unable to update password": "No es posible actualizar contraseña", + "The current password is not correct": "La contraseña actual no es correcta", + "New password cannot be the same as your current password": "La nueva contraseña no puede ser la misma que su contraseña actual", "Enable income capitalization": "Habilitar capitalización de ingresos", "Income capitalization calculation type": "Tipo de cálculo de capitalización de ingresos", "Income capitalization strategy": "Estrategia de capitalización de ingresos", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "La fecha en que el producto de Crédito quedará inactivo y no estará disponible para los clientes. Si está en blanco, el producto de carga nunca quedará inactivo.", "Refund transactions where interest refund will automatically be calculated": "Transacciones de reembolso en las que el reembolso de intereses se calculará automáticamente", "Create Interest Refund": "Crear reembolso de intereses" + }, + "countries": { + "AF": "Afganistán", + "AL": "Albania", + "DZ": "Argelia", + "AD": "Andorra", + "AO": "Angola", + "AR": "Argentina", + "AM": "Armenia", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaiyán", + "BH": "Baréin", + "BD": "Bangladés", + "BY": "Bielorrusia", + "BE": "Bélgica", + "BO": "Bolivia", + "BA": "Bosnia y Herzegovina", + "BR": "Brasil", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Camboya", + "CM": "Camerún", + "CA": "Canadá", + "CL": "Chile", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "HR": "Croacia", + "CU": "Cuba", + "CY": "Chipre", + "CZ": "Chequia", + "DK": "Dinamarca", + "EG": "Egipto", + "SV": "El Salvador", + "ES": "España", + "EE": "Estonia", + "ET": "Etiopía", + "FI": "Finlandia", + "FR": "Francia", + "GE": "Georgia", + "DE": "Alemania", + "GH": "Ghana", + "GR": "Grecia", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Hungría", + "IS": "Islandia", + "IN": "India", + "ID": "Indonesia", + "IQ": "Irak", + "IR": "Irán", + "IE": "Irlanda", + "IL": "Israel", + "IT": "Italia", + "JP": "Japón", + "JO": "Jordania", + "KZ": "Kazajistán", + "KE": "Kenia", + "KW": "Kuwait", + "KG": "Kirguistán", + "LV": "Letonia", + "LB": "Líbano", + "LY": "Libia", + "LT": "Lituania", + "LU": "Luxemburgo", + "MK": "Macedonia del Norte", + "MY": "Malasia", + "MX": "México", + "MD": "Moldavia", + "MC": "Mónaco", + "MN": "Mongolia", + "MA": "Marruecos", + "MZ": "Mozambique", + "NP": "Nepal", + "NL": "Países Bajos", + "NZ": "Nueva Zelanda", + "NI": "Nicaragua", + "NG": "Nigeria", + "NO": "Noruega", + "PK": "Pakistán", + "PA": "Panamá", + "PY": "Paraguay", + "PE": "Perú", + "PH": "Filipinas", + "PL": "Polonia", + "PT": "Portugal", + "QA": "Catar", + "RO": "Rumanía", + "RU": "Rusia", + "SA": "Arabia Saudita", + "SN": "Senegal", + "RS": "Serbia", + "SG": "Singapur", + "SK": "Eslovaquia", + "SI": "Eslovenia", + "ZA": "Sudáfrica", + "KR": "Corea del Sur", + "LK": "Sri Lanka", + "SE": "Suecia", + "CH": "Suiza", + "TW": "Taiwán", + "TH": "Tailandia", + "TR": "Turquía", + "UG": "Uganda", + "UA": "Ucrania", + "AE": "Emiratos Árabes Unidos", + "GB": "Reino Unido", + "US": "Estados Unidos", + "UY": "Uruguay", + "UZ": "Uzbekistán", + "VE": "Venezuela", + "VN": "Vietnam", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabue" } } diff --git a/src/assets/translations/es-MX.json b/src/assets/translations/es-MX.json index 4c865cea9..33ca8eb38 100644 --- a/src/assets/translations/es-MX.json +++ b/src/assets/translations/es-MX.json @@ -2177,6 +2177,9 @@ "Repayments": "Reembolsos", "Repayments Every": "Reembolsos cada", "Repayments Scheduled To": "Reembolsos programados para", + "Password Current": "Contraseña actual", + "Password Current Required": "Contraseña actual requerida", + "Password New": "Contraseña nueva", "Repeat Password": "Repita la contraseña", "Repeats": "Se repite", "Repeats Every": "Se repite cada", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "La contraseña debe contener al menos un carácter especial (@$!%*?&)", "Password minimun length must be": "La longitud mínima de la contraseña debe ser", "Passwords do not match": "Las contraseñas no coinciden", + "Unable to update password": "No es posible actualizar contraseña", + "The current password is not correct": "La contraseña actual no es correcta", + "New password cannot be the same as your current password": "La nueva contraseña no puede ser la misma que su contraseña actual", "Enable income capitalization": "Habilitar capitalización de ingresos", "Income capitalization calculation type": "Tipo de cálculo de capitalización de ingresos", "Income capitalization strategy": "Estrategia de capitalización de ingresos", @@ -3537,5 +3543,122 @@ "loan product will become inactive and unavailable to clients": "La fecha en que el producto de Crédito quedará inactivo y no estará disponible para los clientes. Si está en blanco, el producto de carga nunca quedará inactivo.", "Refund transactions where interest refund will automatically be calculated": "Transacciones de reembolso en las que el reembolso de intereses se calculará automáticamente", "Create Interest Refund": "Crear reembolso de intereses" + }, + "countries": { + "AF": "Afganistán", + "AL": "Albania", + "DZ": "Argelia", + "AD": "Andorra", + "AO": "Angola", + "AR": "Argentina", + "AM": "Armenia", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaiyán", + "BH": "Baréin", + "BD": "Bangladés", + "BY": "Bielorrusia", + "BE": "Bélgica", + "BO": "Bolivia", + "BA": "Bosnia y Herzegovina", + "BR": "Brasil", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Camboya", + "CM": "Camerún", + "CA": "Canadá", + "CL": "Chile", + "CN": "China", + "CO": "Colombia", + "CR": "Costa Rica", + "HR": "Croacia", + "CU": "Cuba", + "CY": "Chipre", + "CZ": "Chequia", + "DK": "Dinamarca", + "EG": "Egipto", + "SV": "El Salvador", + "ES": "España", + "EE": "Estonia", + "ET": "Etiopía", + "FI": "Finlandia", + "FR": "Francia", + "GE": "Georgia", + "DE": "Alemania", + "GH": "Ghana", + "GR": "Grecia", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Hungría", + "IS": "Islandia", + "IN": "India", + "ID": "Indonesia", + "IQ": "Irak", + "IR": "Irán", + "IE": "Irlanda", + "IL": "Israel", + "IT": "Italia", + "JP": "Japón", + "JO": "Jordania", + "KZ": "Kazajistán", + "KE": "Kenia", + "KW": "Kuwait", + "KG": "Kirguistán", + "LV": "Letonia", + "LB": "Líbano", + "LY": "Libia", + "LT": "Lituania", + "LU": "Luxemburgo", + "MK": "Macedonia del Norte", + "MY": "Malasia", + "MX": "México", + "MD": "Moldavia", + "MC": "Mónaco", + "MN": "Mongolia", + "MA": "Marruecos", + "MZ": "Mozambique", + "NP": "Nepal", + "NL": "Países Bajos", + "NZ": "Nueva Zelanda", + "NI": "Nicaragua", + "NG": "Nigeria", + "NO": "Noruega", + "PK": "Pakistán", + "PA": "Panamá", + "PY": "Paraguay", + "PE": "Perú", + "PH": "Filipinas", + "PL": "Polonia", + "PT": "Portugal", + "QA": "Catar", + "RO": "Rumanía", + "RU": "Rusia", + "SA": "Arabia Saudita", + "SN": "Senegal", + "RS": "Serbia", + "SG": "Singapur", + "SK": "Eslovaquia", + "SI": "Eslovenia", + "ZA": "Sudáfrica", + "KR": "Corea del Sur", + "LK": "Sri Lanka", + "SE": "Suecia", + "CH": "Suiza", + "TW": "Taiwán", + "TH": "Tailandia", + "TR": "Turquía", + "UG": "Uganda", + "UA": "Ucrania", + "AE": "Emiratos Árabes Unidos", + "GB": "Reino Unido", + "US": "Estados Unidos", + "UY": "Uruguay", + "UZ": "Uzbekistán", + "VE": "Venezuela", + "VN": "Vietnam", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabue" } } diff --git a/src/assets/translations/fr-FR.json b/src/assets/translations/fr-FR.json index 56aa19606..7ff8cf9b8 100644 --- a/src/assets/translations/fr-FR.json +++ b/src/assets/translations/fr-FR.json @@ -2177,6 +2177,9 @@ "Repayments": "Remboursements", "Repayments Every": "Remboursements tous les", "Repayments Scheduled To": "Remboursements prévus jusqu'au", + "Password Current": "Mot de passe actuel", + "Password Current Required": "Mot de passe actuel requis", + "Password New": "Nouveau mot de passe", "Repeat Password": "Répéter le mot de passe", "Repeats": "Répétitions", "Repeats Every": "Se répète tous les", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "Le mot de passe doit contenir au moins un caractère spécial (@$!%*?&)", "Password minimun length must be": "La longueur minimale du mot de passe doit être", "Passwords do not match": "Les mots de passe ne correspondent pas", + "Unable to update password": "Impossible de mettre à jour le mot de passe", + "The current password is not correct": "Le mot de passe actuel est incorrect", + "New password cannot be the same as your current password": "Le nouveau mot de passe ne peut pas être identique à votre mot de passe actuel", "Enable income capitalization": "Activer la capitalisation des revenus", "Income capitalization calculation type": "Type de calcul de capitalisation des revenus", "Income capitalization strategy": "Stratégie de capitalisation des revenus", @@ -3535,5 +3541,122 @@ "loan product will become inactive and unavailable to clients": "Date à laquelle le produit de prêt deviendra inactif et indisponible pour les clients. S'il est vide, le produit chargé ne deviendra jamais inactif.", "Refund transactions where interest refund will automatically be calculated": "Opérations de remboursement pour lesquelles le remboursement des intérêts sera automatiquement calculé", "Create Interest Refund": "Créer un remboursement d’intérêts" + }, + "countries": { + "AF": "Afghanistan", + "AL": "Albanie", + "DZ": "Algérie", + "AD": "Andorre", + "AO": "Angola", + "AR": "Argentine", + "AM": "Arménie", + "AU": "Australie", + "AT": "Autriche", + "AZ": "Azerbaïdjan", + "BH": "Bahreïn", + "BD": "Bangladesh", + "BY": "Biélorussie", + "BE": "Belgique", + "BO": "Bolivie", + "BA": "Bosnie-Herzégovine", + "BR": "Brésil", + "BG": "Bulgarie", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambodge", + "CM": "Cameroun", + "CA": "Canada", + "CL": "Chili", + "CN": "Chine", + "CO": "Colombie", + "CR": "Costa Rica", + "HR": "Croatie", + "CU": "Cuba", + "CY": "Chypre", + "CZ": "Tchéquie", + "DK": "Danemark", + "EG": "Égypte", + "SV": "Salvador", + "ES": "Espagne", + "EE": "Estonie", + "ET": "Éthiopie", + "FI": "Finlande", + "FR": "France", + "GE": "Géorgie", + "DE": "Allemagne", + "GH": "Ghana", + "GR": "Grèce", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Hongrie", + "IS": "Islande", + "IN": "Inde", + "ID": "Indonésie", + "IQ": "Irak", + "IR": "Iran", + "IE": "Irlande", + "IL": "Israël", + "IT": "Italie", + "JP": "Japon", + "JO": "Jordanie", + "KZ": "Kazakhstan", + "KE": "Kenya", + "KW": "Koweït", + "KG": "Kirghizistan", + "LV": "Lettonie", + "LB": "Liban", + "LY": "Libye", + "LT": "Lituanie", + "LU": "Luxembourg", + "MK": "Macédoine du Nord", + "MY": "Malaisie", + "MX": "Mexique", + "MD": "Moldavie", + "MC": "Monaco", + "MN": "Mongolie", + "MA": "Maroc", + "MZ": "Mozambique", + "NP": "Népal", + "NL": "Pays-Bas", + "NZ": "Nouvelle-Zélande", + "NI": "Nicaragua", + "NG": "Nigeria", + "NO": "Norvège", + "PK": "Pakistan", + "PA": "Panama", + "PY": "Paraguay", + "PE": "Pérou", + "PH": "Philippines", + "PL": "Pologne", + "PT": "Portugal", + "QA": "Qatar", + "RO": "Roumanie", + "RU": "Russie", + "SA": "Arabie Saoudite", + "SN": "Sénégal", + "RS": "Serbie", + "SG": "Singapour", + "SK": "Slovaquie", + "SI": "Slovénie", + "ZA": "Afrique du Sud", + "KR": "Corée du Sud", + "LK": "Sri Lanka", + "SE": "Suède", + "CH": "Suisse", + "TW": "Taïwan", + "TH": "Thaïlande", + "TR": "Turquie", + "UG": "Ouganda", + "UA": "Ukraine", + "AE": "Émirats arabes unis", + "GB": "Royaume-Uni", + "US": "États-Unis", + "UY": "Uruguay", + "UZ": "Ouzbékistan", + "VE": "Venezuela", + "VN": "Viêt Nam", + "YE": "Yémen", + "ZM": "Zambie", + "ZW": "Zimbabwe" } } diff --git a/src/assets/translations/it-IT.json b/src/assets/translations/it-IT.json index 170af90ed..d72e7ed61 100644 --- a/src/assets/translations/it-IT.json +++ b/src/assets/translations/it-IT.json @@ -2177,6 +2177,9 @@ "Repayments": "Rimborsi", "Repayments Every": "Rimborsi ogni", "Repayments Scheduled To": "Rimborsi previsti", + "Password Current": "Password corrente", + "Password Current Required": "Password corrente richiesta", + "Password New": "Nuova password", "Repeat Password": "Ripeti la password", "Repeats": "Si ripete", "Repeats Every": "Si ripete ogni", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "La password deve contenere almeno un carattere speciale (@$!%*?&)", "Password minimun length must be": "La lunghezza minima della password deve essere", "Passwords do not match": "Le password non corrispondono", + "Unable to update password": "Impossibile aggiornare la password", + "The current password is not correct": "La password corrente non è corretta", + "New password cannot be the same as your current password": "La nuova password non può essere uguale alla password corrente", "Enable income capitalization": "Abilita la capitalizzazione del reddito", "Income capitalization calculation type": "Tipo di calcolo della capitalizzazione del reddito", "Income capitalization strategy": "Strategia di capitalizzazione del reddito", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "La data in cui il prodotto di prestito diventerà inattivo e non disponibile per i clienti. Se vuoto, il prodotto caricato non diventerà mai inattivo.", "Refund transactions where interest refund will automatically be calculated": "Transazioni di rimborso in cui il rimborso degli interessi verrà calcolato automaticamente", "Create Interest Refund": "Crea rimborso interessi" + }, + "countries": { + "AF": "Afghanistan", + "AL": "Albania", + "DZ": "Algeria", + "AD": "Andorra", + "AO": "Angola", + "AR": "Argentina", + "AM": "Armenia", + "AU": "Australia", + "AT": "Austria", + "AZ": "Azerbaigian", + "BH": "Bahrein", + "BD": "Bangladesh", + "BY": "Bielorussia", + "BE": "Belgio", + "BO": "Bolivia", + "BA": "Bosnia ed Erzegovina", + "BR": "Brasile", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Cambogia", + "CM": "Camerun", + "CA": "Canada", + "CL": "Cile", + "CN": "Cina", + "CO": "Colombia", + "CR": "Costa Rica", + "HR": "Croazia", + "CU": "Cuba", + "CY": "Cipro", + "CZ": "Cechia", + "DK": "Danimarca", + "EG": "Egitto", + "SV": "El Salvador", + "ES": "Spagna", + "EE": "Estonia", + "ET": "Etiopia", + "FI": "Finlandia", + "FR": "Francia", + "GE": "Georgia", + "DE": "Germania", + "GH": "Ghana", + "GR": "Grecia", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Ungheria", + "IS": "Islanda", + "IN": "India", + "ID": "Indonesia", + "IQ": "Iraq", + "IR": "Iran", + "IE": "Irlanda", + "IL": "Israele", + "IT": "Italia", + "JP": "Giappone", + "JO": "Giordania", + "KZ": "Kazakistan", + "KE": "Kenya", + "KW": "Kuwait", + "KG": "Kirghizistan", + "LV": "Lettonia", + "LB": "Libano", + "LY": "Libia", + "LT": "Lituania", + "LU": "Lussemburgo", + "MK": "Macedonia del Nord", + "MY": "Malesia", + "MX": "Messico", + "MD": "Moldavia", + "MC": "Monaco", + "MN": "Mongolia", + "MA": "Marocco", + "MZ": "Mozambico", + "NP": "Nepal", + "NL": "Paesi Bassi", + "NZ": "Nuova Zelanda", + "NI": "Nicaragua", + "NG": "Nigeria", + "NO": "Norvegia", + "PK": "Pakistan", + "PA": "Panamá", + "PY": "Paraguay", + "PE": "Perù", + "PH": "Filippine", + "PL": "Polonia", + "PT": "Portogallo", + "QA": "Qatar", + "RO": "Romania", + "RU": "Russia", + "SA": "Arabia Saudita", + "SN": "Senegal", + "RS": "Serbia", + "SG": "Singapore", + "SK": "Slovacchia", + "SI": "Slovenia", + "ZA": "Sudafrica", + "KR": "Corea del Sud", + "LK": "Sri Lanka", + "SE": "Svezia", + "CH": "Svizzera", + "TW": "Taiwan", + "TH": "Thailandia", + "TR": "Turchia", + "UG": "Uganda", + "UA": "Ucraina", + "AE": "Emirati Arabi Uniti", + "GB": "Regno Unito", + "US": "Stati Uniti", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VE": "Venezuela", + "VN": "Vietnam", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabwe" } } diff --git a/src/assets/translations/ko-KO.json b/src/assets/translations/ko-KO.json index 0cdfcd7ef..93dfa6cff 100644 --- a/src/assets/translations/ko-KO.json +++ b/src/assets/translations/ko-KO.json @@ -2178,6 +2178,9 @@ "Repayments": "상환", "Repayments Every": "상환 간격", "Repayments Scheduled To": "상환 예정 날짜", + "Password Current": "현재 비밀번호", + "Password Current Required": "현재 비밀번호가 필요합니다", + "Password New": "새 비밀번호", "Repeat Password": "비밀번호 반복", "Repeats": "반복", "Repeats Every": "반복 간격", @@ -2488,6 +2491,9 @@ "Password must contain at least one special character (@$!%*?&)": "비밀번호에는 특수 문자가 하나 이상 포함되어야 합니다. (@$!%*?&)", "Password minimun length must be": "비밀번호의 최소 길이는 다음과 같아야 합니다.", "Passwords do not match": "비밀번호가 일치하지 않습니다.", + "Unable to update password": "비밀번호를 업데이트할 수 없습니다", + "The current password is not correct": "현재 비밀번호가 올바르지 않습니다", + "New password cannot be the same as your current password": "새 비밀번호는 현재 비밀번호와 같을 수 없습니다", "Enable income capitalization": "소득 자본화 활성화", "Income capitalization calculation type": "소득 자본화 계산 유형", "Income capitalization strategy": "소득 자본화 전략", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "대출 상품이 비활성화되어 고객이 사용할 수 없게 되는 날짜입니다. 비어 있으면 로드 제품이 비활성화되지 않습니다.", "Refund transactions where interest refund will automatically be calculated": "이자 환불이 자동으로 계산되는 환불 거래", "Create Interest Refund": "이자 환불 생성" + }, + "countries": { + "AF": "아프가니스탄", + "AL": "알바니아", + "DZ": "알제리", + "AD": "안도라", + "AO": "앙골라", + "AR": "아르헨티나", + "AM": "아르메니아", + "AU": "호주", + "AT": "오스트리아", + "AZ": "아제르바이잔", + "BH": "바레인", + "BD": "방글라데시", + "BY": "벨라루스", + "BE": "벨기에", + "BO": "볼리비아", + "BA": "보스니아 헤르체고비나", + "BR": "브라질", + "BG": "불가리아", + "BF": "부르키나파소", + "BI": "부룬디", + "KH": "캄보디아", + "CM": "카메룬", + "CA": "캐나다", + "CL": "칠레", + "CN": "중국", + "CO": "콜롬비아", + "CR": "코스타리카", + "HR": "크로아티아", + "CU": "쿠바", + "CY": "키프로스", + "CZ": "체코", + "DK": "덴마크", + "EG": "이집트", + "SV": "엘살바도르", + "ES": "스페인", + "EE": "에스토니아", + "ET": "에티오피아", + "FI": "핀란드", + "FR": "프랑스", + "GE": "조지아", + "DE": "독일", + "GH": "가나", + "GR": "그리스", + "GT": "과테말라", + "HN": "온두라스", + "HU": "헝가리", + "IS": "아이슬란드", + "IN": "인도", + "ID": "인도네시아", + "IQ": "이라크", + "IR": "이란", + "IE": "아일랜드", + "IL": "이스라엘", + "IT": "이탈리아", + "JP": "일본", + "JO": "요르단", + "KZ": "카자흐스탄", + "KE": "케냐", + "KW": "쿠웨이트", + "KG": "키르기스스탄", + "LV": "라트비아", + "LB": "레바논", + "LY": "리비아", + "LT": "리투아니아", + "LU": "룩셈부르크", + "MK": "북마케도니아", + "MY": "말레이시아", + "MX": "멕시코", + "MD": "몰도바", + "MC": "모나코", + "MN": "몽골", + "MA": "모로코", + "MZ": "모잠비크", + "NP": "네팔", + "NL": "네덜란드", + "NZ": "뉴질랜드", + "NI": "니카라과", + "NG": "나이지리아", + "NO": "노르웨이", + "PK": "파키스탄", + "PA": "파나마", + "PY": "파라과이", + "PE": "페루", + "PH": "필리핀", + "PL": "폴란드", + "PT": "포르투갈", + "QA": "카타르", + "RO": "루마니아", + "RU": "러시아", + "SA": "사우디아라비아", + "SN": "세네갈", + "RS": "세르비아", + "SG": "싱가포르", + "SK": "슬로바키아", + "SI": "슬로베니아", + "ZA": "남아프리카 공화국", + "KR": "대한민국", + "LK": "스리랑카", + "SE": "스웨덴", + "CH": "스위스", + "TW": "대만", + "TH": "태국", + "TR": "터키", + "UG": "우간다", + "UA": "우크라이나", + "AE": "아랍에미리트", + "GB": "영국", + "US": "미국", + "UY": "우루과이", + "UZ": "우즈베키스탄", + "VE": "베네수엘라", + "VN": "베트남", + "YE": "예멘", + "ZM": "잠비아", + "ZW": "짐바브웨" } } diff --git a/src/assets/translations/lt-LT.json b/src/assets/translations/lt-LT.json index 5c51c5583..96dff844d 100644 --- a/src/assets/translations/lt-LT.json +++ b/src/assets/translations/lt-LT.json @@ -2177,6 +2177,9 @@ "Repayments": "Grąžinimai", "Repayments Every": "Grąžinimai Kiekvienas", "Repayments Scheduled To": "Suplanuoti grąžinimai iki", + "Password Current": "Dabartinis slaptažodis", + "Password Current Required": "Reikalingas dabartinis slaptažodis", + "Password New": "Naujas slaptažodis", "Repeat Password": "Pakartokite slaptažodį", "Repeats": "Pasikartoja", "Repeats Every": "Kartojasi kas", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "Slaptažodyje turi būti bent vienas specialusis simbolis (@$!%*?&)", "Password minimun length must be": "Minimalus slaptažodžio ilgis turi būti", "Passwords do not match": "Slaptažodžiai nesutampa", + "Unable to update password": "Nepavyko atnaujinti slaptažodžio", + "The current password is not correct": "Dabartinis slaptažodis nėra teisingas", + "New password cannot be the same as your current password": "Naujas slaptažodis negali būti toks pat kaip dabartinis slaptažodis", "Enable income capitalization": "Įgalinti pajamų kapitalizaciją", "Income capitalization calculation type": "Pajamų kapitalizavimo skaičiavimo tipas", "Income capitalization strategy": "Pajamų kapitalizavimo strategija", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "Data, kai paskolos produktas taps neaktyvus ir nepasiekiamas klientams. Jei tuščia, įkėlimo produktas niekada netaps neaktyvus.", "Refund transactions where interest refund will automatically be calculated": "Atmaksas darījumi, kuros procentu atmaksa tiks aprēķināta automātiski", "Create Interest Refund": "Sukurti palūkanų grąžinimą" + }, + "countries": { + "AF": "Afganistanas", + "AL": "Albanija", + "DZ": "Alžyras", + "AD": "Andora", + "AO": "Angola", + "AR": "Argentina", + "AM": "Armėnija", + "AU": "Australija", + "AT": "Austrija", + "AZ": "Azerbaidžanas", + "BH": "Bahreinas", + "BD": "Bangladešas", + "BY": "Baltarusija", + "BE": "Belgija", + "BO": "Bolivija", + "BA": "Bosnija ir Hercegovina", + "BR": "Brazilija", + "BG": "Bulgarija", + "BF": "Burkina Fasas", + "BI": "Burundis", + "KH": "Kambodža", + "CM": "Kamerūnas", + "CA": "Kanada", + "CL": "Čilė", + "CN": "Kinija", + "CO": "Kolumbija", + "CR": "Kosta Rika", + "HR": "Kroatija", + "CU": "Kuba", + "CY": "Kipras", + "CZ": "Čekija", + "DK": "Danija", + "EG": "Egiptas", + "SV": "Salvadoras", + "ES": "Ispanija", + "EE": "Estija", + "ET": "Etiopija", + "FI": "Suomija", + "FR": "Prancūzija", + "GE": "Gruzija", + "DE": "Vokietija", + "GH": "Gana", + "GR": "Graikija", + "GT": "Gvatemala", + "HN": "Hondūras", + "HU": "Vengrija", + "IS": "Islandija", + "IN": "Indija", + "ID": "Indonezija", + "IQ": "Irakas", + "IR": "Iranas", + "IE": "Airija", + "IL": "Izraelis", + "IT": "Italija", + "JP": "Japonija", + "JO": "Jordanija", + "KZ": "Kazachstanas", + "KE": "Kenija", + "KW": "Kuveitas", + "KG": "Kirgizija", + "LV": "Latvija", + "LB": "Libanas", + "LY": "Libija", + "LT": "Lietuva", + "LU": "Liuksemburgas", + "MK": "Šiaurės Makedonija", + "MY": "Malaizija", + "MX": "Meksika", + "MD": "Moldova", + "MC": "Monakas", + "MN": "Mongolija", + "MA": "Marokas", + "MZ": "Mozambikas", + "NP": "Nepalas", + "NL": "Nyderlandai", + "NZ": "Naujoji Zelandija", + "NI": "Nikaragva", + "NG": "Nigerija", + "NO": "Norvegija", + "PK": "Pakistanas", + "PA": "Panama", + "PY": "Paragvajus", + "PE": "Peru", + "PH": "Filipinai", + "PL": "Lenkija", + "PT": "Portugalija", + "QA": "Kataras", + "RO": "Rumunija", + "RU": "Rusija", + "SA": "Saudo Arabija", + "SN": "Senegalas", + "RS": "Serbija", + "SG": "Singapūras", + "SK": "Slovakija", + "SI": "Slovėnija", + "ZA": "Pietų Afrika", + "KR": "Pietų Korėja", + "LK": "Šri Lanka", + "SE": "Švedija", + "CH": "Šveicarija", + "TW": "Taivanas", + "TH": "Tailandas", + "TR": "Turkija", + "UG": "Uganda", + "UA": "Ukraina", + "AE": "Jungtiniai Arabų Emyratai", + "GB": "Jungtinė Karalystė", + "US": "Jungtinės Valstijos", + "UY": "Urugvajus", + "UZ": "Uzbekistanas", + "VE": "Venesuela", + "VN": "Vietnamas", + "YE": "Jemenas", + "ZM": "Zambija", + "ZW": "Zimbabvė" } } diff --git a/src/assets/translations/lv-LV.json b/src/assets/translations/lv-LV.json index 3ea818aea..1cca10de4 100644 --- a/src/assets/translations/lv-LV.json +++ b/src/assets/translations/lv-LV.json @@ -2177,6 +2177,9 @@ "Repayments": "Atmaksas", "Repayments Every": "Atmaksas katru", "Repayments Scheduled To": "Atmaksas ieplānotas uz", + "Password Current": "Pašreizējā parole", + "Password Current Required": "Pašreizējā parole ir obligāta", + "Password New": "Jaunā parole", "Repeat Password": "Atkārtojiet paroli", "Repeats": "Atkārtojas", "Repeats Every": "Atkārtojas katru", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "Parolē jāsatur vismaz viena speciālā rakstzīme (@$!%*?&)", "Password minimun length must be": "Paroles minimālajam garumam jābūt", "Passwords do not match": "Paroles nesakrīt", + "Unable to update password": "Neizdevās atjaunināt paroli", + "The current password is not correct": "Pašreizējā parole nav pareiza", + "New password cannot be the same as your current password": "Jaunā parole nevar būt tāda pati kā jūsu pašreizējā parole", "Enable income capitalization": "Iespējot ienākumu kapitalizāciju", "Income capitalization calculation type": "Ienākumu kapitalizācijas aprēķina veids", "Income capitalization strategy": "Ienākumu kapitalizācijas stratēģija", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "Datums, kad aizdevuma produkts kļūs neaktīvs un klientiem nebūs pieejams. Ja lauks ir tukšs, ielādes produkts nekad nekļūs neaktīvs.", "Refund transactions where interest refund will automatically be calculated": "Grąžinimo operacijos, kai palūkanų grąžinimas bus skaičiuojamas automatiškai", "Create Interest Refund": "Izveidot procentu atmaksu" + }, + "countries": { + "AF": "Afganistāna", + "AL": "Albānija", + "DZ": "Alžīrija", + "AD": "Andora", + "AO": "Angola", + "AR": "Argentīna", + "AM": "Armēnija", + "AU": "Austrālija", + "AT": "Austrija", + "AZ": "Azerbaidžāna", + "BH": "Bahreina", + "BD": "Bangladeša", + "BY": "Baltkrievija", + "BE": "Beļģija", + "BO": "Bolīvija", + "BA": "Bosnija un Hercegovina", + "BR": "Brazīlija", + "BG": "Bulgārija", + "BF": "Burkinafaso", + "BI": "Burundi", + "KH": "Kambodža", + "CM": "Kamerūna", + "CA": "Kanāda", + "CL": "Čīle", + "CN": "Ķīna", + "CO": "Kolumbija", + "CR": "Kostarika", + "HR": "Horvātija", + "CU": "Kuba", + "CY": "Kipra", + "CZ": "Čehija", + "DK": "Dānija", + "EG": "Ēģipte", + "SV": "Salvadora", + "ES": "Spānija", + "EE": "Igaunija", + "ET": "Etiopija", + "FI": "Somija", + "FR": "Francija", + "GE": "Gruzija", + "DE": "Vācija", + "GH": "Gana", + "GR": "Grieķija", + "GT": "Gvatemala", + "HN": "Hondurasa", + "HU": "Ungārija", + "IS": "Islande", + "IN": "Indija", + "ID": "Indonēzija", + "IQ": "Irāka", + "IR": "Irāna", + "IE": "Īrija", + "IL": "Izraēla", + "IT": "Itālija", + "JP": "Japāna", + "JO": "Jordānija", + "KZ": "Kazahstāna", + "KE": "Kenija", + "KW": "Kuveita", + "KG": "Kirgizstāna", + "LV": "Latvija", + "LB": "Libāna", + "LY": "Lībija", + "LT": "Lietuva", + "LU": "Luksemburga", + "MK": "Ziemeļmaķedonija", + "MY": "Malaizija", + "MX": "Meksika", + "MD": "Moldova", + "MC": "Monako", + "MN": "Mongolija", + "MA": "Maroka", + "MZ": "Mozambika", + "NP": "Nepāla", + "NL": "Nīderlande", + "NZ": "Jaunzēlande", + "NI": "Nikaragva", + "NG": "Nigērija", + "NO": "Norvēģija", + "PK": "Pakistāna", + "PA": "Panama", + "PY": "Paragvaja", + "PE": "Peru", + "PH": "Filipīnas", + "PL": "Polija", + "PT": "Portugāle", + "QA": "Katara", + "RO": "Rumānija", + "RU": "Krievija", + "SA": "Saūda Arābija", + "SN": "Senegāla", + "RS": "Serbija", + "SG": "Singapūra", + "SK": "Slovākija", + "SI": "Slovēnija", + "ZA": "Dienvidāfrika", + "KR": "Dienvidkoreja", + "LK": "Šrilanka", + "SE": "Zviedrija", + "CH": "Šveice", + "TW": "Taivāna", + "TH": "Taizeme", + "TR": "Turcija", + "UG": "Uganda", + "UA": "Ukraina", + "AE": "Apvienotie Arābu Emirāti", + "GB": "Apvienotā Karaliste", + "US": "Amerikas Savienotās Valstis", + "UY": "Urugvaja", + "UZ": "Uzbekistāna", + "VE": "Venecuēla", + "VN": "Vjetnama", + "YE": "Jemena", + "ZM": "Zambija", + "ZW": "Zimbabve" } } diff --git a/src/assets/translations/ne-NE.json b/src/assets/translations/ne-NE.json index 43021e13f..dce3e86fc 100644 --- a/src/assets/translations/ne-NE.json +++ b/src/assets/translations/ne-NE.json @@ -2177,6 +2177,9 @@ "Repayments": "भुक्तानीहरू", "Repayments Every": "भुक्तानी प्रत्येक", "Repayments Scheduled To": "भुक्तानीहरू अनुसूचित", + "Password Current": "हालको पासवर्ड", + "Password Current Required": "हालको पासवर्ड आवश्यक छ", + "Password New": "नयाँ पासवर्ड", "Repeat Password": "पासवर्ड फेरि दोहोर्याउँनुहोस्", "Repeats": "दोहोरिन्छ", "Repeats Every": "प्रत्येक दोहोर्याउँछ", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "पासवर्डमा कम्तिमा एउटा विशेष वर्ण हुनुपर्छ (@$!%*?&)", "Password minimun length must be": "पासवर्ड न्यूनतम लम्बाइ हुनुपर्छ", "Passwords do not match": "पासवर्डहरू मेल खाँदैनन्", + "Unable to update password": "पासवर्ड अद्यावधिक गर्न सकिँदैन", + "The current password is not correct": "हालको पासवर्ड सही छैन", + "New password cannot be the same as your current password": "नयाँ पासवर्ड तपाईंको हालको पासवर्डसँग उस्तै हुन सक्दैन", "Enable income capitalization": "आय पूंजीकरण सक्षम गर्नुहोस्", "Income capitalization calculation type": "आय पूंजीकरण गणना प्रकार", "Income capitalization strategy": "आय पूंजीकरण रणनीति", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "ऋण उत्पादन निष्क्रिय र ग्राहकहरु को लागी अनुपलब्ध हुने मिति। यदि खाली छ भने, लोड उत्पादन कहिल्यै निष्क्रिय हुनेछैन।", "Refund transactions where interest refund will automatically be calculated": "फिर्ता लेनदेन जहाँ ब्याज फिर्ता स्वतः गणना हुनेछ", "Create Interest Refund": "ब्याज फिर्ता सिर्जना गर्नुहोस्" + }, + "countries": { + "AF": "अफगानिस्तान", + "AL": "अल्बानिया", + "DZ": "अल्जेरिया", + "AD": "अन्डोरा", + "AO": "अङ्गोला", + "AR": "अर्जेन्टिना", + "AM": "आर्मेनिया", + "AU": "अष्ट्रेलिया", + "AT": "अष्ट्रिया", + "AZ": "अजरबैजान", + "BH": "बाह्रैन", + "BD": "बङ्गलादेश", + "BY": "बेलारुस", + "BE": "बेल्जियम", + "BO": "बोलिभिया", + "BA": "बोस्निया एण्ड हर्जगोभिनिया", + "BR": "ब्राजिल", + "BG": "बुल्गेरिया", + "BF": "बुर्किना फासो", + "BI": "बुरुन्डी", + "KH": "कम्बोडिया", + "CM": "क्यामरुन", + "CA": "क्यानाडा", + "CL": "चिली", + "CN": "चीन", + "CO": "कोलम्बिया", + "CR": "कोष्टा रिका", + "HR": "क्रोएसिया", + "CU": "क्युबा", + "CY": "साइप्रस", + "CZ": "चेकिया", + "DK": "डेनमार्क", + "EG": "इजिप्ट", + "SV": "एल साल्भाडोर", + "ES": "स्पेन", + "EE": "एस्टोनिया", + "ET": "इथियोपिया", + "FI": "फिनल्यान्ड", + "FR": "फ्रान्स", + "GE": "जर्जिया", + "DE": "जर्मनी", + "GH": "घाना", + "GR": "ग्रीस", + "GT": "ग्वाटेमाला", + "HN": "हन्डुरस", + "HU": "हङ्गेरी", + "IS": "आइसल्यान्ड", + "IN": "भारत", + "ID": "इण्डोनेशिया", + "IQ": "इराक", + "IR": "इरान", + "IE": "आयरल्याण्ड", + "IL": "इजरायल", + "IT": "इटली", + "JP": "जापान", + "JO": "जोर्डन", + "KZ": "काजाकस्तान", + "KE": "केन्या", + "KW": "कुवेत", + "KG": "किर्गिस्तान", + "LV": "लाट्भिया", + "LB": "लेबनान", + "LY": "लिबिया", + "LT": "लिथुआनिया", + "LU": "लक्जेम्बर्ग", + "MK": "उत्तर म्यासेडोनिया", + "MY": "मलेसिया", + "MX": "मेक्सिको", + "MD": "मोल्डोभा", + "MC": "मोनाको", + "MN": "मङ्गोलिया", + "MA": "मोरोक्को", + "MZ": "मोजाम्बिक", + "NP": "नेपाल", + "NL": "नेदरल्याण्ड", + "NZ": "न्यूजील्याण्ड", + "NI": "निकारागुवा", + "NG": "नाइजेरिया", + "NO": "नर्वे", + "PK": "पाकिस्तान", + "PA": "पनामा", + "PY": "प्याराग्वे", + "PE": "पेरु", + "PH": "फिलिपिन्स", + "PL": "पोल्यान्ड", + "PT": "पोर्चुगल", + "QA": "कतार", + "RO": "रोमानिया", + "RU": "रूस", + "SA": "साउदी अरब", + "SN": "सेनेगल", + "RS": "सर्बिया", + "SG": "सिङ्गापुर", + "SK": "स्लोभाकिया", + "SI": "स्लोभेनिया", + "ZA": "दक्षिण अफ्रिका", + "KR": "दक्षिण कोरिया", + "LK": "श्रीलंका", + "SE": "स्वीडेन", + "CH": "स्विजरल्यान्ड", + "TW": "ताइवान", + "TH": "थाइल्याण्ड", + "TR": "टर्की", + "UG": "उगान्डा", + "UA": "युक्रेन", + "AE": "संयुक्त अरब इमिरेट्स", + "GB": "संयुक्त अधिराज्य", + "US": "संयुक्त राज्य अमेरिका", + "UY": "उरुग्वे", + "UZ": "उज्बेकिस्तान", + "VE": "भेनेजुएला", + "VN": "भियतनाम", + "YE": "यमन", + "ZM": "जाम्बिया", + "ZW": "जिम्बावे" } } diff --git a/src/assets/translations/pt-PT.json b/src/assets/translations/pt-PT.json index b0346964b..7cfbeade6 100644 --- a/src/assets/translations/pt-PT.json +++ b/src/assets/translations/pt-PT.json @@ -2177,6 +2177,9 @@ "Repayments": "Reembolsos", "Repayments Every": "Reembolsos a cada", "Repayments Scheduled To": "Reembolsos agendados para", + "Password Current": "Palavra-passe atual", + "Password Current Required": "Palavra-passe atual é obrigatória", + "Password New": "Nova palavra-passe", "Repeat Password": "Repita a senha", "Repeats": "Repete", "Repeats Every": "Repete a cada", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "A senha deve conter pelo menos um caractere especial (@$!%*?&)", "Password minimun length must be": "O comprimento mínimo da senha deve ser", "Passwords do not match": "As senhas não coincidem", + "Unable to update password": "Não foi possível atualizar a palavra-passe", + "The current password is not correct": "A palavra-passe atual não está correta", + "New password cannot be the same as your current password": "A nova palavra-passe não pode ser igual à sua palavra-passe atual", "Enable income capitalization": "Habilitar capitalização de renda", "Income capitalization calculation type": "Tipo de cálculo de capitalização de renda", "Income capitalization strategy": "Estratégia de capitalização de renda", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "A data em que o produto de empréstimo ficará inativo e indisponível para os clientes. Se estiver em branco, o produto de carregamento nunca ficará inativo.", "Refund transactions where interest refund will automatically be calculated": "Transações de reembolso em que o reembolso de juros será calculado automaticamente", "Create Interest Refund": "Criar reembolso de juros" + }, + "countries": { + "AF": "Afeganistão", + "AL": "Albânia", + "DZ": "Argélia", + "AD": "Andorra", + "AO": "Angola", + "AR": "Argentina", + "AM": "Arménia", + "AU": "Austrália", + "AT": "Áustria", + "AZ": "Azerbaijão", + "BH": "Bahrein", + "BD": "Bangladesh", + "BY": "Bielorrússia", + "BE": "Bélgica", + "BO": "Bolívia", + "BA": "Bósnia e Herzegovina", + "BR": "Brasil", + "BG": "Bulgária", + "BF": "Burkina Faso", + "BI": "Burúndi", + "KH": "Camboja", + "CM": "Camarões", + "CA": "Canadá", + "CL": "Chile", + "CN": "China", + "CO": "Colômbia", + "CR": "Costa Rica", + "HR": "Croácia", + "CU": "Cuba", + "CY": "Chipre", + "CZ": "Chéquia", + "DK": "Dinamarca", + "EG": "Egito", + "SV": "El Salvador", + "ES": "Espanha", + "EE": "Estónia", + "ET": "Etiópia", + "FI": "Finlândia", + "FR": "França", + "GE": "Geórgia", + "DE": "Alemanha", + "GH": "Gana", + "GR": "Grécia", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Hungria", + "IS": "Islândia", + "IN": "Índia", + "ID": "Indonésia", + "IQ": "Iraque", + "IR": "Irão", + "IE": "Irlanda", + "IL": "Israel", + "IT": "Itália", + "JP": "Japão", + "JO": "Jordânia", + "KZ": "Cazaquistão", + "KE": "Quénia", + "KW": "Kuwait", + "KG": "Quirguistão", + "LV": "Letónia", + "LB": "Líbano", + "LY": "Líbia", + "LT": "Lituânia", + "LU": "Luxemburgo", + "MK": "Macedónia do Norte", + "MY": "Malásia", + "MX": "México", + "MD": "Moldávia", + "MC": "Mónaco", + "MN": "Mongólia", + "MA": "Marrocos", + "MZ": "Moçambique", + "NP": "Nepal", + "NL": "Países Baixos", + "NZ": "Nova Zelândia", + "NI": "Nicarágua", + "NG": "Nigéria", + "NO": "Noruega", + "PK": "Paquistão", + "PA": "Panamá", + "PY": "Paraguai", + "PE": "Peru", + "PH": "Filipinas", + "PL": "Polónia", + "PT": "Portugal", + "QA": "Catar", + "RO": "Roménia", + "RU": "Rússia", + "SA": "Arábia Saudita", + "SN": "Senegal", + "RS": "Sérvia", + "SG": "Singapura", + "SK": "Eslováquia", + "SI": "Eslovénia", + "ZA": "África do Sul", + "KR": "Coreia do Sul", + "LK": "Sri Lanka", + "SE": "Suécia", + "CH": "Suíça", + "TW": "Taiwan", + "TH": "Tailândia", + "TR": "Turquia", + "UG": "Uganda", + "UA": "Ucrânia", + "AE": "Emirados Árabes Unidos", + "GB": "Reino Unido", + "US": "Estados Unidos", + "UY": "Uruguai", + "UZ": "Uzbequistão", + "VE": "Venezuela", + "VN": "Vietname", + "YE": "Iémen", + "ZM": "Zâmbia", + "ZW": "Zimbabwe" } } diff --git a/src/assets/translations/sw-SW.json b/src/assets/translations/sw-SW.json index 39bd0f602..408fc4007 100644 --- a/src/assets/translations/sw-SW.json +++ b/src/assets/translations/sw-SW.json @@ -2177,6 +2177,9 @@ "Repayments": "Malipo", "Repayments Every": "Malipo Kila", "Repayments Scheduled To": "Malipo Yamepangwa Kwa", + "Password Current": "Neno la siri la sasa", + "Password Current Required": "Neno la siri la sasa linahitajika", + "Password New": "Neno jipya la siri", "Repeat Password": "Rudia neno siri", "Repeats": "Hurudia", "Repeats Every": "Hurudia Kila", @@ -2487,6 +2490,9 @@ "Password must contain at least one special character (@$!%*?&)": "Nenosiri lazima liwe na angalau herufi moja maalum (@$!%*?&)", "Password minimun length must be": "Nenosiri la urefu wa chini lazima liwe", "Passwords do not match": "Manenosiri hayalingani", + "Unable to update password": "Haiwezi kusasisha nenosiri", + "The current password is not correct": "Nenosiri la sasa si sahihi", + "New password cannot be the same as your current password": "Nenosiri jipya haliwezi kuwa sawa na nenosiri lako la sasa", "Enable income capitalization": "Washa mtaji wa mapato", "Income capitalization calculation type": "Aina ya kukokotoa mtaji wa mapato", "Income capitalization strategy": "Mkakati wa mtaji wa mapato", @@ -3536,5 +3542,122 @@ "loan product will become inactive and unavailable to clients": "Tarehe ambayo bidhaa ya mkopo itaacha kutumika na haitapatikana kwa wateja. Ikiwa tupu, bidhaa ya kupakia haitafanya kazi kamwe.", "Refund transactions where interest refund will automatically be calculated": "Shughuli za kurejesha pesa ambapo urejeshaji wa riba utahesabiwa kiotomatiki", "Create Interest Refund": "Unda Marejesho ya Riba" + }, + "countries": { + "AF": "Afghanistani", + "AL": "Albania", + "DZ": "Aljeria", + "AD": "Andorra", + "AO": "Angola", + "AR": "Ajentina", + "AM": "Armenia", + "AU": "Australia", + "AT": "Ustria", + "AZ": "Azerbaijani", + "BH": "Bahraini", + "BD": "Bangladeshi", + "BY": "Belarusi", + "BE": "Ubelgiji", + "BO": "Bolivia", + "BA": "Bosnia na Herzegovina", + "BR": "Brazil", + "BG": "Bulgaria", + "BF": "Burkina Faso", + "BI": "Burundi", + "KH": "Kambodia", + "CM": "Kameruni", + "CA": "Kanada", + "CL": "Chile", + "CN": "China", + "CO": "Kolombia", + "CR": "Costa Rica", + "HR": "Croatia", + "CU": "Kuba", + "CY": "Cyprus", + "CZ": "Cheki", + "DK": "Denmark", + "EG": "Misri", + "SV": "El Salvador", + "ES": "Hispania", + "EE": "Estonia", + "ET": "Ethiopia", + "FI": "Finlandi", + "FR": "Ufaransa", + "GE": "Jorgia", + "DE": "Ujerumani", + "GH": "Ghana", + "GR": "Ugiriki", + "GT": "Guatemala", + "HN": "Honduras", + "HU": "Hungaria", + "IS": "Aislandi", + "IN": "India", + "ID": "Indonesia", + "IQ": "Iraki", + "IR": "Iran", + "IE": "Ireland", + "IL": "Israeli", + "IT": "Italia", + "JP": "Japani", + "JO": "Jordan", + "KZ": "Kazakhstan", + "KE": "Kenya", + "KW": "Kuwait", + "KG": "Kirghizstan", + "LV": "Latvia", + "LB": "Lebanon", + "LY": "Libya", + "LT": "Lithuania", + "LU": "Luxembourg", + "MK": "Macedonia Kaskazini", + "MY": "Malaysia", + "MX": "Mexico", + "MD": "Moldova", + "MC": "Monaco", + "MN": "Mongolia", + "MA": "Morocco", + "MZ": "Mozambique", + "NP": "Nepal", + "NL": "Uholanzi", + "NZ": "New Zealand", + "NI": "Nicaragua", + "NG": "Nigeria", + "NO": "Norway", + "PK": "Pakistan", + "PA": "Panama", + "PY": "Paraguay", + "PE": "Peru", + "PH": "Philippines", + "PL": "Poland", + "PT": "Portugal", + "QA": "Qatar", + "RO": "Romania", + "RU": "Urusi", + "SA": "Saudi Arabia", + "SN": "Senegal", + "RS": "Serbia", + "SG": "Singapore", + "SK": "Slovakia", + "SI": "Slovenia", + "ZA": "Afrika Kusini", + "KR": "Korea Kusini", + "LK": "Sri Lanka", + "SE": "Sweden", + "CH": "Switzerland", + "TW": "Taiwan", + "TH": "Thailand", + "TR": "Turkey", + "UG": "Uganda", + "UA": "Ukraine", + "AE": "United Arab Emirates", + "GB": "Uingereza", + "US": "Marekani", + "UY": "Uruguay", + "UZ": "Uzbekistan", + "VE": "Venezuela", + "VN": "Vietnam", + "YE": "Yemen", + "ZM": "Zambia", + "ZW": "Zimbabwe" } } diff --git a/src/environments/.env.ts b/src/environments/.env.ts index eb41e01ac..388dbeff8 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -2,8 +2,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '250703', - 'hash': 'gc4224678' + 'version': '250828', + 'hash': 'b92f79e2' }, 'allow_switching_backend_instance': true }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 5de973f24..705028994 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -60,7 +60,15 @@ export const environment = { vNextApiVersion: window['env']['vNextApiVersion'] || '/v1.0', interbankTransfers: window['env']['interbankTransfers'] || false, - minPasswordLength: loadedEnv['minPasswordLength'] || 12 + minPasswordLength: loadedEnv['minPasswordLength'] || 12, + + OIDC: { + oidcServerEnabled: window['env']['oidcServerEnabled'] || false, + oidcBaseUrl: window['env']['oidcBaseUrl'] || '', + oidcClientId: window['env']['oidcClientId'] || '', + oidcApiUrl: window['env']['oidcApiUrl'] || '', + oidcFrontUrl: window['env']['oidcFrontUrl'] || '' + } }; // Server URL diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 52131ede8..811707f0a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -64,7 +64,15 @@ export const environment = { vNextApiVersion: window.env?.vNextApiVersion || '/v1.0', interbankTransfers: window.env?.interbankTransfers || false, - minPasswordLength: loadedEnv.minPasswordLength || 12 + minPasswordLength: loadedEnv.minPasswordLength || 12, + + OIDC: { + oidcServerEnabled: window['env']['oidcServerEnabled'] || false, + oidcBaseUrl: window['env']['oidcBaseUrl'] || '', + oidcClientId: window['env']['oidcClientId'] || '', + oidcApiUrl: window['env']['oidcApiUrl'] || '', + oidcFrontUrl: window['env']['oidcFrontUrl'] || '' + } }; // Server URL diff --git a/src/index.html b/src/index.html index 8c53ed097..a168e7b28 100644 --- a/src/index.html +++ b/src/index.html @@ -18,6 +18,17 @@ +