WEB-233: Improve the OIDC integration with 2FA (#2616)

This commit is contained in:
AllsVeam
2025-09-01 13:55:35 -06:00
committed by GitHub
parent 801dd3fdfe
commit 6ee9f4d4d2
66 changed files with 4425 additions and 135 deletions
+5
View File
@@ -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}
+5
View File
@@ -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
+22 -3
View File
@@ -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 {}
@@ -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.
+1 -1
View File
@@ -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.
@@ -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();
}
}
/**
@@ -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();
}
}
/**
@@ -1,55 +1,72 @@
<form class="layout-column" [formGroup]="loginForm" (ngSubmit)="login()" id="login-form">
<mat-form-field class="login-input flex-align-center">
<span matPrefix class="m-r-10">
<fa-icon icon="user-circle" size="lg"></fa-icon>
</span>
<mat-label>{{ 'labels.inputs.Username' | translate }}</mat-label>
<input matInput type="text" autocomplete="off" formControlName="username" />
<mat-error *ngIf="loginForm.controls.username.hasError('required')">
{{ 'labels.inputs.Username' | translate }} <strong>{{ 'labels.commons.is required' | translate }}</strong>
</mat-error>
</mat-form-field>
<div *ngIf="!oidcServerEnabled">
<form class="layout-column" [formGroup]="loginForm" (ngSubmit)="login()" id="login-form">
<mat-form-field class="login-input flex-align-center">
<span matPrefix class="m-r-10">
<fa-icon icon="user-circle" size="lg"></fa-icon>
</span>
<mat-label>{{ 'labels.inputs.Username' | translate }}</mat-label>
<input matInput type="text" autocomplete="off" formControlName="username" />
<mat-error *ngIf="loginForm.controls.username.hasError('required')">
{{ 'labels.inputs.Username' | translate }} <strong>{{ 'labels.commons.is required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="login-input flex-align-center">
<span matPrefix class="m-r-10">
<fa-icon icon="lock" size="lg"></fa-icon>
</span>
<mat-label>{{ 'labels.inputs.Password' | translate }}</mat-label>
<input matInput type="{{ passwordInputType }}" formControlName="password" />
<button
type="button"
matSuffix
*ngIf="loginForm.controls.password.value && !loading"
mat-icon-button
(mousedown)="togglePasswordVisibility()"
(mouseup)="togglePasswordVisibility()"
>
<fa-icon *ngIf="passwordInputType === 'password'" icon="eye"></fa-icon>
<fa-icon *ngIf="passwordInputType === 'text'" icon="eye-slash"></fa-icon>
</button>
<mat-error *ngIf="loginForm.controls.password.hasError('required')">
{{ 'labels.inputs.Password' | translate }} <strong>{{ 'labels.commons.pass is required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="remember" class="m-t-10 flex-align-center">{{
'labels.inputs.Remember me' | translate
}}</mat-checkbox>
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<button mat-raised-button color="primary" class="login-button flex-align-center" [disabled]="!loginForm.valid">
{{ 'labels.buttons.Login' | translate }}
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
</button>
<mat-form-field class="login-input flex-align-center">
<span matPrefix class="m-r-10">
<fa-icon icon="lock" size="lg"></fa-icon>
</span>
<mat-label>{{ 'labels.inputs.Password' | translate }}</mat-label>
<input matInput type="{{ passwordInputType }}" formControlName="password" />
<button
type="button"
matSuffix
*ngIf="loginForm.controls.password.value && !loading"
mat-icon-button
(mousedown)="togglePasswordVisibility()"
(mouseup)="togglePasswordVisibility()"
mat-button
class="login-button flex-align-center"
(click)="forgotPassword()"
[disabled]="loading"
>
<fa-icon *ngIf="passwordInputType === 'password'" icon="eye"></fa-icon>
<fa-icon *ngIf="passwordInputType === 'text'" icon="eye-slash"></fa-icon>
{{ 'labels.links.Forgot Password?' | translate }}
</button>
<mat-error *ngIf="loginForm.controls.password.hasError('required')">
{{ 'labels.inputs.Password' | translate }} <strong>{{ 'labels.commons.pass is required' | translate }}</strong>
</mat-error>
</mat-form-field>
</form>
</div>
<mat-checkbox formControlName="remember" class="m-t-10 flex-align-center">{{
'labels.inputs.Remember me' | translate
}}</mat-checkbox>
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
<button mat-raised-button color="primary" class="login-button flex-align-center" [disabled]="!loginForm.valid">
{{ 'labels.buttons.Login' | translate }}
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
</button>
<button
type="button"
mat-button
class="login-button flex-align-center"
(click)="forgotPassword()"
[disabled]="loading"
>
{{ 'labels.links.Forgot Password?' | translate }}
</button>
</form>
<div *ngIf="oidcServerEnabled" class="container container-margin">
<div *ngIf="!isLoggedIn">
<button mat-raised-button color="primary" class="login-button-z" (click)="loginOIDC()">
{{ 'labels.buttons.Login' | translate }}
<mat-spinner [diameter]="20" *ngIf="loading"></mat-spinner>
</button>
</div>
<div *ngIf="isLoggedIn">
<p>Welcome! You are logged in.</p>
<button (click)="logout()">Logout</button>
<button (click)="loadProtectedData()">Load Protected Data</button>
<pre>{{ protectedData | json }}</pre>
</div>
</div>
@@ -18,3 +18,8 @@
::ng-deep .mat-mdc-form-field-infix {
width: 144px !important;
}
.container-margin {
margin-top: 8rem;
margin-bottom: 8rem;
}
@@ -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.
*
@@ -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 });
});
}
@@ -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 });
});
}
@@ -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 {
+99 -41
View File
@@ -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 {}
+12 -1
View File
@@ -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 {}
+23 -9
View File
@@ -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);
});
}
}
+10
View File
@@ -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;
};
+381
View File
@@ -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<string> {
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<void> {
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);
}
}
@@ -0,0 +1,6 @@
<div class="loader-wrapper">
<div class="bottom triangle"></div>
<div class="top triangle"></div>
<div class="left triangle"></div>
<div class="right triangle"></div>
</div>
@@ -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);
}
}
}
+117
View File
@@ -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' }
];
+9
View File
@@ -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' }
];
@@ -0,0 +1,35 @@
<h1 mat-dialog-title>{{ 'labels.heading.Change Password' | translate }}</h1>
<div mat-dialog-content>
<form [formGroup]="changePasswordForm" class="layout-column" (ngSubmit)="onSubmit()">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labels.inputs.Password Current' | translate }}</mat-label>
<input matInput type="password" formControlName="currentPassword" required />
<mat-error *ngIf="changePasswordForm.get('currentPassword')?.hasError('required')">
{{ 'labels.inputs.Password Current Required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labels.inputs.Password New' | translate }}</mat-label>
<input matInput type="password" formControlName="password" required />
<mat-error *ngIf="changePasswordForm.get('password')?.errors">
{{ 'labels.commons.Password validation' | translate: { minchar: minPasswordLength } }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ 'labels.inputs.Repeat Password' | translate }}</mat-label>
<input matInput type="password" formControlName="repeatPassword" required />
<mat-error *ngIf="changePasswordForm.get('repeatPassword')?.hasError('notequal')">
{{ 'inputs.passwordsDoNotMatch' | translate }}
</mat-error>
</mat-form-field>
<mat-dialog-actions align="center">
<button mat-raised-button color="warn" type="button" (click)="dialogRef.close()">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" type="submit" [disabled]="!changePasswordForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-dialog-actions>
</form>
</div>
@@ -0,0 +1,8 @@
.error {
color: red;
}
mat-dialog-content {
min-width: 240px;
width: 240px;
}
@@ -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<ChangePasswordDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ChangePasswordDialogComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChangePasswordDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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<ChangePasswordDialogComponent>,
@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'));
}
}
});
}
}
+34
View File
@@ -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 {}
+64
View File
@@ -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<any>, next: HttpHandler): Observable<HttpEvent<any>> {
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<any>, next: HttpHandler): Promise<HttpEvent<any>> {
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<HttpEvent<any>>;
} else {
throw new Error('No new access token obtained after refresh');
}
} catch (e) {
console.error('Error in handle401Error, forcing logout');
throw e;
}
}
}
@@ -0,0 +1,205 @@
<div class="container">
<mat-card>
<form [formGroup]="userForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-row-wrap layout-lt-md-column gap-2percent">
<!-- Username -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Username' | translate }}</mat-label>
<input matInput required formControlName="username" />
<mat-error *ngIf="userForm.controls.username.hasError('required')">
{{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<!-- Email -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Email' | translate }}</mat-label>
<input matInput required formControlName="email" />
<mat-error *ngIf="userForm.controls.email.hasError('email')">
{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.invalid' | translate }}</strong>
</mat-error>
<mat-error *ngIf="userForm.controls.email.hasError('required')">
{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<!-- First Name -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
<input matInput formControlName="firstName" required />
</mat-form-field>
<!-- Last Name -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
<input matInput formControlName="lastName" required />
</mat-form-field>
<!-- Preferred Language -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Language' | translate }}</mat-label>
<mat-select formControlName="preferredLanguage" required>
<mat-option *ngFor="let lang of languageCodes" [value]="lang.code">
{{ 'languages.' + lang.key | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<!-- Gender -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Gender' | translate }}</mat-label>
<mat-select formControlName="gender" required>
<mat-option value="GENDER_MALE">{{ 'labels.catalogs.male' | translate }}</mat-option>
<mat-option value="GENDER_FEMALE">{{ 'labels.catalogs.female' | translate }}</mat-option>
<mat-option value="GENDER_OTHER">{{ 'labels.catalogs.other' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<!-- Country code and phone -->
<div class="flex items-center gap-2">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Code' | translate }}</mat-label>
<mat-select formControlName="countryCode" required>
<mat-option *ngFor="let country of countryCodes" [value]="country.code">
{{ 'countries.' + country.key | translate }} ({{ country.code }})
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Phone Number' | translate }}</mat-label>
<input matInput formControlName="phoneNumber" required maxlength="12" minlength="4" />
</mat-form-field>
</div>
<mat-form-field class="flex-20">
<mat-label>{{ 'labels.inputs.Office' | translate }}</mat-label>
<mat-select required formControlName="officeId">
<mat-option *ngFor="let office of officesData" [value]="office.id">
{{ office.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="userForm.controls.officeId.hasError('required')">
{{ 'labels.inputs.Office' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-20">
<mat-label>{{ 'labels.inputs.Staff' | translate }}</mat-label>
<mat-select formControlName="staffId">
<mat-option *ngFor="let staff of staffData" [value]="staff.id">
{{ staff.displayName }}
</mat-option>
</mat-select>
</mat-form-field>
<!-- Password -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Password' | translate }}</mat-label>
<input matInput type="password" formControlName="password" required />
<mat-error *ngIf="userForm.get('password')?.hasError('required')">
{{ 'labels.inputs.Password' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
<mat-error *ngIf="userForm.get('password')?.hasError('minlength')">
{{ 'labels.commons.Password should be at least' | translate }}
<strong>{{ 'labels.commons.12 characters long' | translate }}</strong>
</mat-error>
<mat-error *ngIf="userForm.get('password')?.hasError('maxlength')">
{{ 'labels.commons.Password should not be more than' | translate }}
<strong>{{ 'labels.commons.50 characters long' | translate }}</strong>
</mat-error>
<mat-error
*ngIf="
userForm.get('password')?.hasError('pattern') &&
!userForm.get('password')?.hasError('minlength') &&
!userForm.get('password')?.hasError('maxlength')
"
>
{{ 'labels.commons.Password should include a' | translate }}
<strong>{{ 'labels.commons.numeral' | translate }}</strong>
{{ 'labels.commons.and at' | translate }}
<strong>{{ 'labels.commons.least one uppercase' | translate }}</strong
>,
<strong>{{ 'labels.commons.lowercase and special character' | translate }}</strong>
</mat-error>
</mat-form-field>
<!-- Repeat password -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Repeat Password' | translate }}</mat-label>
<input matInput type="password" formControlName="repeatPassword" required />
<mat-error *ngIf="userForm.get('repeatPassword')?.hasError('required')">
{{ 'labels.inputs.Password' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<!-- Passwords do not match -->
<p
class="flex-98 reset-password-error"
*ngIf="
userForm.errors?.passwordsDoNotMatch &&
!userForm.get('password')?.hasError('required') &&
!userForm.get('repeatPassword')?.hasError('required')
"
>
{{ 'labels.inputs.Passwords' | translate }}
<strong>{{ 'labels.commons.do not match' | translate }}</strong>
</p>
<!-- Roles -->
<mat-form-field class="flex-20">
<mat-label>{{ 'labels.inputs.Roles' | translate }}</mat-label>
<mat-select formControlName="roles" required multiple>
<mat-option *ngFor="let role of rolesData" [value]="role.id">
{{ role.name }}
</mat-option>
</mat-select>
<mat-error *ngIf="userForm.controls.roles.hasError('required')">
{{ 'labels.commons.At least one role' | translate }}
<strong>{{ 'labels.commons.must be selected' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="userForm.invalid">
{{ 'labels.buttons.Submit' | translate }}
</button>
</mat-card-actions>
</form>
</mat-card>
</div>
<ng-template #templateUserFormRef let-popover="popover">
<h2>{{ 'labels.heading.Create User' | translate }}</h2>
<p class="mw400">
{{ 'labels.text.Filling Details' | translate }}:
<a href="https://mifosforge.jira.com/wiki/spaces/docs/pages/67141740/Users" target="_blank">{{
'labels.inputs.Users' | translate
}}</a>
</p>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
</button>
<button mat-raised-button color="primary" (click)="popover.close()">
{{ 'labels.buttons.Create User' | translate }}
</button>
<button mat-raised-button color="primary" (click)="popover.close(); previousStep()">
{{ 'labels.buttons.Back' | translate }}
</button>
<button mat-raised-button color="primary" (click)="popover.close(); nextStep()">
{{ 'labels.buttons.Next' | translate }}
</button>
</div>
</ng-template>
@@ -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;
}
@@ -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<CreateUserComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CreateUserComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CreateUserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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<any>;
/* Template for popover on create user form */
@ViewChild('templateUserFormRef') templateUserFormRef: TemplateRef<any>;
/**
* 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<any>.
* @param target HTMLElement | ElementRef<any>.
* @param position String.
* @param backdrop Boolean.
*/
showPopover(
template: TemplateRef<any>,
target: HTMLElement | ElementRef<any>,
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']);
}
});
}
}
@@ -0,0 +1,125 @@
<div class="container">
<mat-card>
<form *ngIf="editUserForm" [formGroup]="editUserForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-row-wrap gap-2px responsive-column">
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Username' | translate }}</mat-label>
<input matInput required formControlName="username" readonline />
<mat-error *ngIf="editUserForm.controls.username.hasError('required')">
{{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Email' | translate }}</mat-label>
<input matInput required formControlName="email" />
<mat-error *ngIf="editUserForm.controls.email.hasError('email')">
{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.invalid' | translate }}</strong>
</mat-error>
<mat-error *ngIf="editUserForm.controls.email.hasError('required')">
{{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
<input matInput required formControlName="firstname" />
<mat-error *ngIf="editUserForm.controls.firstname.hasError('pattern')">
{{ 'labels.inputs.First Name' | translate }}
<strong>{{ 'labels.commons.cannot begin with a special character or number' | translate }}</strong>
</mat-error>
<mat-error *ngIf="editUserForm.controls.firstname.hasError('required')">
{{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
<input matInput required formControlName="lastname" />
<mat-error *ngIf="editUserForm.controls.lastname.hasError('pattern')">
{{ 'labels.inputs.Last Name' | translate }}
<strong>{{ 'labels.commons.cannot begin with a special character or number' | translate }}</strong>
</mat-error>
<mat-error *ngIf="editUserForm.controls.lastname.hasError('required')">
{{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-20">
<mat-label>{{ 'labels.inputs.Code' | translate }}</mat-label>
<mat-select formControlName="countryCode" required>
<mat-option *ngFor="let country of countryCodes" [value]="country.code">
{{ 'countries.' + country.key | translate }} ({{ country.code }})
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-75">
<mat-label>{{ 'labels.inputs.Phone Number' | translate }}</mat-label>
<input matInput formControlName="phoneNumber" required maxlength="12" minlength="4" />
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Gender' | translate }}</mat-label>
<mat-select formControlName="gender" required>
<mat-option value="GENDER_MALE">{{ 'labels.catalogs.male' | translate }}</mat-option>
<mat-option value="GENDER_FEMALE">{{ 'labels.catalogs.female' | translate }}</mat-option>
<mat-option value="GENDER_OTHER">{{ 'labels.catalogs.other' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Language' | translate }}</mat-label>
<mat-select formControlName="preferredLanguage" required>
<mat-option *ngFor="let lang of languageCodes" [value]="lang.code">
{{ 'languages.' + lang.key | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Office' | translate }}</mat-label>
<mat-select required formControlName="officeId" (selectionChange)="officeChanged($event.value)">
<mat-option *ngFor="let office of officesData" [value]="office.id">
{{ office.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Staff' | translate }}</mat-label>
<mat-select formControlName="staffId">
<mat-option *ngFor="let staff of staffData" [value]="staff.id">
{{ staff.displayName }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Roles' | translate }}</mat-label>
<mat-select formControlName="roles" multiple>
<mat-option *ngFor="let rol of rolesData" [value]="rol.id">
{{ rol.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary">
{{ 'labels.buttons.Submit' | translate }}
</button>
</mat-card-actions>
</div>
</mat-card-content>
</form>
</mat-card>
</div>
@@ -0,0 +1,3 @@
.password-never-expires-wrapper {
padding: 17.5px 0;
}
@@ -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<EditUserComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [EditUserComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EditUserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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']);
});
}
}
@@ -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<any>}
*/
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const userId = route.paramMap.get('id');
return this.usersServiceZitadel.getUser(userId);
}
}
@@ -0,0 +1,47 @@
<div class="container m-b-20 layout-row align-end gap-20px">
<div #buttonCreateUser class="in-block">
<button mat-raised-button color="primary" [routerLink]="['create']">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Create User' | translate }}
</button>
</div>
</div>
<div class="container container-margin">
<div #filter class="layout-row gap-20px">
<mat-form-field class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
</div>
</div>
<div class="container mat-elevation-z8 container-margin">
<h3>{{ 'labels.heading.Users' | translate }}</h3>
<table mat-table [dataSource]="dataSource" matSort #sortUsers="matSort">
<ng-container matColumnDef="firstname">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.First Name' | translate }}</th>
<td mat-cell *matCellDef="let user">{{ user.firstname }}</td>
</ng-container>
<ng-container matColumnDef="lastname">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Last Name' | translate }}</th>
<td mat-cell *matCellDef="let user">{{ user.lastname }}</td>
</ng-container>
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Email' | translate }}</th>
<td mat-cell *matCellDef="let user">{{ user.email }}</td>
</ng-container>
<ng-container matColumnDef="officeName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Office' | translate }}</th>
<td mat-cell *matCellDef="let user">{{ user.officeName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="select-row"></tr>
</table>
<mat-paginator #paginatorUsers [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
@@ -0,0 +1,11 @@
table {
width: 100%;
.select-row:hover {
cursor: pointer;
}
}
.container-margin {
margin-top: 1rem;
}
+126
View File
@@ -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<any>();
dataSourceZitadel = new MatTableDataSource<any>();
/** 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<any>;
@ViewChild('templateButtonCreateUser') templateButtonCreateUser!: TemplateRef<any>;
@ViewChild('usersTable') usersTable!: ElementRef<any>;
@ViewChild('templateUsersTable') templateUsersTable!: TemplateRef<any>;
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<any>, target: HTMLElement | ElementRef<any>, 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']);
}
}
@@ -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<any>}
*/
resolve(): Observable<any> {
return this.usersServiceZitadel.getUsersTemplate();
}
}
@@ -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<any>}
*/
resolve(): Observable<any> {
return this.usersServiceZitadel.getUsers();
}
}
@@ -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<any>}
*/
createUser(user: any): Observable<any> {
return this.http.post(this.api + 'authentication/user', user);
}
getExtraUserData(userId: string): Observable<any> {
return this.http.get(`${this.api}authentication/user/db/${userId}`);
}
assignRolesToUser(userId: string, roleKeys: string[]): Observable<any> {
const payload = {
userId: userId,
roleKeys: roleKeys.map(String)
};
return this.http.post(this.api + 'authentication/user/role', payload);
}
createUserBd(user: any): Observable<any> {
return this.http.post(this.api + 'authentication/user/db', user);
}
/**
* @returns {Observable<any>} Users template data
*/
getUsersTemplate(): Observable<any> {
return this.http.get('/users/template');
}
/**
* @returns {Observable<any>} Users data
*/
getUsers(): Observable<any[]> {
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<any>} User.
*/
getUser(userId: string): Observable<any> {
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<any> {
return this.http.put(this.api + 'authentication/user', user);
}
editRoles(roles: any): Observable<any> {
return this.http.put(this.api + 'authentication/user/role', roles);
}
editOffice(office: any): Observable<any> {
return this.http.put(this.api + 'authentication/user/office', office);
}
getRoles() {
return this.http.get(this.api + 'authentication/role');
}
}
@@ -0,0 +1,96 @@
<div class="container m-b-20 layout-row layout-lt-md-column align-end gap-2percent">
<button mat-raised-button color="primary" [routerLink]="['edit']">
<fa-icon icon="edit" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Edit' | translate }}
</button>
<button mat-raised-button color="warn" (click)="delete()">
<fa-icon icon="trash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Delete' | translate }}
</button>
<div
*ngIf="userData.isSelfServiceUser === 'USER_STATE_ACTIVE' || userData.isSelfServiceUser === 'USER_STATE_INACTIVE'"
>
<button mat-raised-button color="primary" (click)="changeUserPassword(userData.id)">
<fa-icon icon="cog" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Change Password' | translate }}
</button>
</div>
<div [ngSwitch]="userData.isSelfServiceUser">
<button *ngSwitchCase="'USER_STATE_ACTIVE'" mat-raised-button color="warn" (click)="desactiveUser(userData.id)">
<fa-icon icon="ban" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Disable' | translate }}
</button>
<button *ngSwitchCase="'USER_STATE_INACTIVE'" mat-raised-button color="primary" (click)="activeUser(userData.id)">
<fa-icon icon="check" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Reactivate' | translate }}
</button>
<button *ngSwitchDefault mat-raised-button color="primary" disabled>
<fa-icon icon="ban" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Validate' | translate }}
</button>
</div>
</div>
<div class="container">
<mat-card>
<mat-card-content>
<div class="layout-row-wrap">
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Login Name' | translate }}
</div>
<div class="flex-50">
{{ userData.username }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.First Name' | translate }}
</div>
<div class="flex-50">
{{ userData.firstname }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Last Name' | translate }}
</div>
<div class="flex-50">
{{ userData.lastname }}
</div>
<div class="flex-50 mat-body-strong" *ngIf="userData.email">
{{ 'labels.inputs.Email' | translate }}
</div>
<div class="flex-50" *ngIf="userData.email">
{{ userData.email }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Office' | translate }}
</div>
<div class="flex-50">
{{ userData.officeName }}
</div>
<div class="flex-50 mat-body-strong">
{{ 'labels.inputs.Roles' | translate }}
</div>
<div class="flex-50">
<span *ngFor="let role of userData.selectedRoles">
{{ role.name }}
</span>
</div>
</div>
<div class="layout-row layout-align-center gap-2percent column-on-mobile">
<button type="button" mat-raised-button color="primary" [routerLink]="['../../']">
{{ 'labels.buttons.Back' | translate }}
</button>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,14 @@
.container {
max-width: 37rem;
.content {
div {
margin: 1rem 0;
word-wrap: break-word;
span {
display: block;
}
}
}
}
@@ -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<ViewUserZitadelComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ViewUserZitadelComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ViewUserZitadelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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);
}
}
+10 -3
View File
@@ -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);
+8 -1
View File
@@ -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);
+123
View File
@@ -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"
}
}
+123
View File
@@ -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"
}
}
+123
View File
@@ -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"
}
}
+123
View File
@@ -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"
}
}
+123
View File
@@ -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"
}
}
+123
View File
@@ -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"
}
}
+123
View File
@@ -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"
}
}
+123
View File
@@ -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": "짐바브웨"
}
}
+123
View File
@@ -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ė"
}
}
+123
View File
@@ -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"
}
}
+123
View File
@@ -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": "जिम्बावे"
}
}
+123
View File
@@ -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"
}
}
+123
View File
@@ -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"
}
}
+2 -2
View File
@@ -2,8 +2,8 @@
/* tslint:disable */
export default {
'mifos_x': {
'version': '250703',
'hash': 'gc4224678'
'version': '250828',
'hash': 'b92f79e2'
},
'allow_switching_backend_instance': true
};
+9 -1
View File
@@ -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
+9 -1
View File
@@ -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
+11
View File
@@ -18,6 +18,17 @@
<!-- Load environment variables -->
</head>
<body class="h-full">
<script>
function getQueryParam(param) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
}
const code = getQueryParam('code');
if (code) {
localStorage.setItem('auth_code', code);
window.location.href = '/#/callback';
}
</script>
<script src="assets/env.js"></script>
<!--[if lt IE 10]>
<p>