mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-12 10:38:33 +00:00
feat: create human user v2 page (#9506)
# Which Problems Are Solved Allows users to be created using the V2 User API # How the Problems Are Solved I added a seperate V2 create user page with the new code using the new apis. # Additional Changes I did some refactorings arround our interceptors as they used an obselete syntax. The password complexity form takes the Buf definitions. # Additional Context - Closes #9430 --------- Co-authored-by: Max Peintner <peintnerm@gmail.com>
This commit is contained in:
parent
11c9be3b8d
commit
b418ea75bb
@ -33,8 +33,8 @@
|
|||||||
"@grpc/grpc-js": "^1.11.2",
|
"@grpc/grpc-js": "^1.11.2",
|
||||||
"@netlify/framework-info": "^9.8.13",
|
"@netlify/framework-info": "^9.8.13",
|
||||||
"@ngx-translate/core": "^15.0.0",
|
"@ngx-translate/core": "^15.0.0",
|
||||||
"@zitadel/client": "^1.0.6",
|
"@zitadel/client": "^1.0.7",
|
||||||
"@zitadel/proto": "^1.0.3",
|
"@zitadel/proto": "^1.0.4",
|
||||||
"angular-oauth2-oidc": "^15.0.1",
|
"angular-oauth2-oidc": "^15.0.1",
|
||||||
"angularx-qrcode": "^16.0.0",
|
"angularx-qrcode": "^16.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
import { AuthGuard } from './guards/auth.guard';
|
import { authGuard } from './guards/auth.guard';
|
||||||
import { RoleGuard } from './guards/role.guard';
|
import { roleGuard } from './guards/role-guard';
|
||||||
import { UserGrantContext } from './modules/user-grants/user-grants-datasource';
|
import { UserGrantContext } from './modules/user-grants/user-grants-datasource';
|
||||||
import { OrgCreateComponent } from './pages/org-create/org-create.component';
|
import { OrgCreateComponent } from './pages/org-create/org-create.component';
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('./pages/home/home.module'),
|
loadChildren: () => import('./pages/home/home.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['.'],
|
roles: ['.'],
|
||||||
},
|
},
|
||||||
@ -22,7 +22,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'orgs/create',
|
path: 'orgs/create',
|
||||||
component: OrgCreateComponent,
|
component: OrgCreateComponent,
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['(org.create)?(iam.write)?'],
|
roles: ['(org.create)?(iam.write)?'],
|
||||||
},
|
},
|
||||||
@ -31,12 +31,12 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'orgs',
|
path: 'orgs',
|
||||||
loadChildren: () => import('./pages/org-list/org-list.module'),
|
loadChildren: () => import('./pages/org-list/org-list.module'),
|
||||||
canActivate: [AuthGuard],
|
canActivate: [authGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'granted-projects',
|
path: 'granted-projects',
|
||||||
loadChildren: () => import('./pages/projects/granted-projects/granted-projects.module'),
|
loadChildren: () => import('./pages/projects/granted-projects/granted-projects.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['project.grant.read'],
|
roles: ['project.grant.read'],
|
||||||
},
|
},
|
||||||
@ -44,20 +44,20 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'projects',
|
path: 'projects',
|
||||||
loadChildren: () => import('./pages/projects/projects.module'),
|
loadChildren: () => import('./pages/projects/projects.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['project.read'],
|
roles: ['project.read'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'users',
|
path: 'users',
|
||||||
canActivate: [AuthGuard],
|
canActivate: [authGuard],
|
||||||
loadChildren: () => import('src/app/pages/users/users.module'),
|
loadChildren: () => import('src/app/pages/users/users.module'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'instance',
|
path: 'instance',
|
||||||
loadChildren: () => import('./pages/instance/instance.module'),
|
loadChildren: () => import('./pages/instance/instance.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['iam.read', 'iam.write'],
|
roles: ['iam.read', 'iam.write'],
|
||||||
},
|
},
|
||||||
@ -65,7 +65,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'org',
|
path: 'org',
|
||||||
loadChildren: () => import('./pages/orgs/org.module'),
|
loadChildren: () => import('./pages/orgs/org.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['org.read'],
|
roles: ['org.read'],
|
||||||
},
|
},
|
||||||
@ -73,7 +73,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'actions',
|
path: 'actions',
|
||||||
loadChildren: () => import('./pages/actions/actions.module'),
|
loadChildren: () => import('./pages/actions/actions.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['org.action.read', 'org.flow.read'],
|
roles: ['org.action.read', 'org.flow.read'],
|
||||||
},
|
},
|
||||||
@ -81,7 +81,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'grants',
|
path: 'grants',
|
||||||
loadChildren: () => import('./pages/grants/grants.module'),
|
loadChildren: () => import('./pages/grants/grants.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
context: UserGrantContext.NONE,
|
context: UserGrantContext.NONE,
|
||||||
roles: ['user.grant.read'],
|
roles: ['user.grant.read'],
|
||||||
@ -89,12 +89,12 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'grant-create',
|
path: 'grant-create',
|
||||||
canActivate: [AuthGuard],
|
canActivate: [authGuard],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'project/:projectid/grant/:grantid',
|
path: 'project/:projectid/grant/:grantid',
|
||||||
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
|
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['user.grant.write'],
|
roles: ['user.grant.write'],
|
||||||
},
|
},
|
||||||
@ -102,7 +102,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'project/:projectid',
|
path: 'project/:projectid',
|
||||||
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
|
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['user.grant.write'],
|
roles: ['user.grant.write'],
|
||||||
},
|
},
|
||||||
@ -110,7 +110,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'user/:userid',
|
path: 'user/:userid',
|
||||||
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
|
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['user.grant.write'],
|
roles: ['user.grant.write'],
|
||||||
},
|
},
|
||||||
@ -118,7 +118,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
|
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['user.grant.write'],
|
roles: ['user.grant.write'],
|
||||||
},
|
},
|
||||||
@ -128,7 +128,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'org-settings',
|
path: 'org-settings',
|
||||||
loadChildren: () => import('./pages/org-settings/org-settings.module'),
|
loadChildren: () => import('./pages/org-settings/org-settings.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['policy.read'],
|
roles: ['policy.read'],
|
||||||
},
|
},
|
||||||
|
@ -33,9 +33,6 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
|||||||
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
||||||
import * as i18nIsoCountries from 'i18n-iso-countries';
|
import * as i18nIsoCountries from 'i18n-iso-countries';
|
||||||
import { from, Observable } from 'rxjs';
|
import { from, Observable } from 'rxjs';
|
||||||
import { AuthGuard } from 'src/app/guards/auth.guard';
|
|
||||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
|
||||||
import { UserGuard } from 'src/app/guards/user.guard';
|
|
||||||
import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module';
|
import { InfoOverlayModule } from 'src/app/modules/info-overlay/info-overlay.module';
|
||||||
import { AssetService } from 'src/app/services/asset.service';
|
import { AssetService } from 'src/app/services/asset.service';
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
@ -173,9 +170,6 @@ const authConfig: AuthConfig = {
|
|||||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }),
|
ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AuthGuard,
|
|
||||||
RoleGuard,
|
|
||||||
UserGuard,
|
|
||||||
ThemeService,
|
ThemeService,
|
||||||
EnvironmentService,
|
EnvironmentService,
|
||||||
ExhaustedService,
|
ExhaustedService,
|
||||||
|
@ -1,34 +1,26 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import { AuthConfig } from 'angular-oauth2-oidc';
|
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthenticationService } from '../services/authentication.service';
|
import { AuthenticationService } from '../services/authentication.service';
|
||||||
|
|
||||||
@Injectable({
|
export const authGuard: CanActivateFn = (route) => {
|
||||||
providedIn: 'root',
|
const auth = inject(AuthenticationService);
|
||||||
})
|
|
||||||
export class AuthGuard {
|
|
||||||
constructor(private auth: AuthenticationService) {}
|
|
||||||
|
|
||||||
public canActivate(
|
if (!auth.authenticated) {
|
||||||
route: ActivatedRouteSnapshot,
|
if (route.queryParams && route.queryParams['login_hint']) {
|
||||||
state: RouterStateSnapshot,
|
const hint = route.queryParams['login_hint'];
|
||||||
): Observable<boolean> | Promise<boolean> | Promise<any> | boolean {
|
const configWithPrompt: Partial<AuthConfig> = {
|
||||||
if (!this.auth.authenticated) {
|
customQueryParams: {
|
||||||
if (route.queryParams && route.queryParams['login_hint']) {
|
login_hint: hint,
|
||||||
const hint = route.queryParams['login_hint'];
|
},
|
||||||
const configWithPrompt: Partial<AuthConfig> = {
|
};
|
||||||
customQueryParams: {
|
console.log(`authenticate with login_hint: ${hint}`);
|
||||||
login_hint: hint,
|
auth.authenticate(configWithPrompt).then();
|
||||||
},
|
} else {
|
||||||
};
|
return auth.authenticate();
|
||||||
console.log(`authenticate with login_hint: ${hint}`);
|
|
||||||
this.auth.authenticate(configWithPrompt);
|
|
||||||
} else {
|
|
||||||
return this.auth.authenticate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return this.auth.authenticated;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return auth.authenticated;
|
||||||
|
};
|
||||||
|
9
console/src/app/guards/role-guard.ts
Normal file
9
console/src/app/guards/role-guard.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
|
||||||
|
import { GrpcAuthService } from '../services/grpc-auth.service';
|
||||||
|
|
||||||
|
export const roleGuard: CanActivateFn = (route) => {
|
||||||
|
const authService = inject(GrpcAuthService);
|
||||||
|
return authService.isAllowed(route.data['roles'], route.data['requiresAll']);
|
||||||
|
};
|
@ -1,16 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { GrpcAuthService } from '../services/grpc-auth.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class RoleGuard {
|
|
||||||
constructor(private authService: GrpcAuthService) {}
|
|
||||||
|
|
||||||
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
|
||||||
return this.authService.isAllowed(route.data['roles'], route.data['requiresAll']);
|
|
||||||
}
|
|
||||||
}
|
|
21
console/src/app/guards/user-guard.ts
Normal file
21
console/src/app/guards/user-guard.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
|
import { map, take } from 'rxjs';
|
||||||
|
|
||||||
|
import { GrpcAuthService } from '../services/grpc-auth.service';
|
||||||
|
|
||||||
|
export const userGuard: CanActivateFn = (route) => {
|
||||||
|
const authService = inject(GrpcAuthService);
|
||||||
|
const router = inject(Router);
|
||||||
|
|
||||||
|
return authService.user.pipe(
|
||||||
|
take(1),
|
||||||
|
map((user) => {
|
||||||
|
const isMe = user?.id === route.params['id'];
|
||||||
|
if (isMe) {
|
||||||
|
router.navigate(['/users', 'me']).then();
|
||||||
|
}
|
||||||
|
return !isMe;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
@ -1,31 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
|
||||||
import { map, Observable, take } from 'rxjs';
|
|
||||||
|
|
||||||
import { GrpcAuthService } from '../services/grpc-auth.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class UserGuard {
|
|
||||||
constructor(
|
|
||||||
private authService: GrpcAuthService,
|
|
||||||
private router: Router,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public canActivate(
|
|
||||||
route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot,
|
|
||||||
): Observable<boolean> | Promise<boolean> | boolean {
|
|
||||||
return this.authService.user.pipe(
|
|
||||||
take(1),
|
|
||||||
map((user) => {
|
|
||||||
const isMe = user?.id === route.params['id'];
|
|
||||||
if (isMe) {
|
|
||||||
this.router.navigate(['/users', 'me']);
|
|
||||||
}
|
|
||||||
return !isMe;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
<div class="validation-col" *ngIf="policy">
|
<div class="validation-col">
|
||||||
<div class="val" *ngIf="policy.minLength">
|
<div class="val" *ngIf="policy.minLength">
|
||||||
<i *ngIf="password?.value?.length === 0; else showSpinner" class="las la-times red"></i>
|
<i *ngIf="password?.value?.length === 0; else showSpinner" class="las la-times red"></i>
|
||||||
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
diameter="20"
|
diameter="20"
|
||||||
[color]="currentError ? 'warn' : 'valid'"
|
[color]="currentError ? 'warn' : 'valid'"
|
||||||
mode="determinate"
|
mode="determinate"
|
||||||
[value]="(password?.value?.length / policy.minLength) * 100"
|
[value]="(password?.value?.length / minLength) * 100"
|
||||||
>
|
>
|
||||||
</mat-progress-spinner>
|
</mat-progress-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { AbstractControl } from '@angular/forms';
|
import { AbstractControl } from '@angular/forms';
|
||||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-password-complexity-view',
|
selector: 'cnsl-password-complexity-view',
|
||||||
@ -9,5 +9,9 @@ import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy
|
|||||||
})
|
})
|
||||||
export class PasswordComplexityViewComponent {
|
export class PasswordComplexityViewComponent {
|
||||||
@Input() public password: AbstractControl | null = null;
|
@Input() public password: AbstractControl | null = null;
|
||||||
@Input() public policy!: PasswordComplexityPolicy.AsObject;
|
@Input({ required: true }) public policy!: PasswordComplexityPolicy;
|
||||||
|
|
||||||
|
protected get minLength() {
|
||||||
|
return Number(this.policy.minLength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { AuthGuard } from 'src/app/guards/auth.guard';
|
import { authGuard } from 'src/app/guards/auth.guard';
|
||||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
import { roleGuard } from 'src/app/guards/role-guard';
|
||||||
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
||||||
|
|
||||||
import { InstanceComponent } from './instance.component';
|
import { InstanceComponent } from './instance.component';
|
||||||
@ -10,7 +10,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: InstanceComponent,
|
component: InstanceComponent,
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['iam.read'],
|
roles: ['iam.read'],
|
||||||
},
|
},
|
||||||
@ -18,14 +18,14 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'members',
|
path: 'members',
|
||||||
loadChildren: () => import('./instance-members/instance-members.module'),
|
loadChildren: () => import('./instance-members/instance-members.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['iam.member.read'],
|
roles: ['iam.member.read'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'provider',
|
path: 'provider',
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
loadChildren: () => import('src/app/modules/providers/providers.module'),
|
loadChildren: () => import('src/app/modules/providers/providers.module'),
|
||||||
data: {
|
data: {
|
||||||
roles: ['iam.idp.read'],
|
roles: ['iam.idp.read'],
|
||||||
@ -34,7 +34,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'smtpprovider',
|
path: 'smtpprovider',
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
loadChildren: () => import('src/app/modules/smtp-provider/smtp-provider.module'),
|
loadChildren: () => import('src/app/modules/smtp-provider/smtp-provider.module'),
|
||||||
data: {
|
data: {
|
||||||
roles: ['iam.idp.read'],
|
roles: ['iam.idp.read'],
|
||||||
|
@ -1,27 +1,21 @@
|
|||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
|
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import {
|
import { passwordConfirmValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||||
containsLowerCaseValidator,
|
|
||||||
containsNumberValidator,
|
|
||||||
containsSymbolValidator,
|
|
||||||
containsUpperCaseValidator,
|
|
||||||
minLengthValidator,
|
|
||||||
passwordConfirmValidator,
|
|
||||||
requiredValidator,
|
|
||||||
} from 'src/app/modules/form-field/validators/validators';
|
|
||||||
import { SetUpOrgRequest } from 'src/app/proto/generated/zitadel/admin_pb';
|
import { SetUpOrgRequest } from 'src/app/proto/generated/zitadel/admin_pb';
|
||||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
|
||||||
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
|
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
import { AdminService } from 'src/app/services/admin.service';
|
import { AdminService } from 'src/app/services/admin.service';
|
||||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { LanguagesService } from '../../services/languages.service';
|
import { LanguagesService } from 'src/app/services/languages.service';
|
||||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||||
|
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||||
|
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||||
|
import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-org-create',
|
selector: 'cnsl-org-create',
|
||||||
@ -48,20 +42,22 @@ export class OrgCreateComponent {
|
|||||||
|
|
||||||
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
|
||||||
|
|
||||||
public policy?: PasswordComplexityPolicy.AsObject;
|
public policy?: PasswordComplexityPolicy;
|
||||||
public usePassword: boolean = false;
|
public usePassword: boolean = false;
|
||||||
|
|
||||||
public forSelf: boolean = true;
|
public forSelf: boolean = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private readonly router: Router,
|
||||||
private toast: ToastService,
|
private readonly toast: ToastService,
|
||||||
private adminService: AdminService,
|
private readonly adminService: AdminService,
|
||||||
private _location: Location,
|
private readonly _location: Location,
|
||||||
private fb: UntypedFormBuilder,
|
private readonly fb: UntypedFormBuilder,
|
||||||
private mgmtService: ManagementService,
|
private readonly mgmtService: ManagementService,
|
||||||
private authService: GrpcAuthService,
|
private readonly newMgmtService: NewMgmtService,
|
||||||
public langSvc: LanguagesService,
|
private readonly authService: GrpcAuthService,
|
||||||
|
private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService,
|
||||||
|
public readonly langSvc: LanguagesService,
|
||||||
breadcrumbService: BreadcrumbService,
|
breadcrumbService: BreadcrumbService,
|
||||||
) {
|
) {
|
||||||
const instanceBread = new Breadcrumb({
|
const instanceBread = new Breadcrumb({
|
||||||
@ -103,8 +99,8 @@ export class OrgCreateComponent {
|
|||||||
this.adminService
|
this.adminService
|
||||||
.SetUpOrg(createOrgRequest, humanRequest)
|
.SetUpOrg(createOrgRequest, humanRequest)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.authService.revalidateOrgs();
|
this.authService.revalidateOrgs().then();
|
||||||
this.router.navigate(['/orgs']);
|
this.router.navigate(['/orgs']).then();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
@ -133,36 +129,12 @@ export class OrgCreateComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public initPwdValidators(): void {
|
public initPwdValidators(): void {
|
||||||
const validators: Validators[] = [requiredValidator];
|
|
||||||
|
|
||||||
if (this.usePassword) {
|
if (this.usePassword) {
|
||||||
this.mgmtService.getDefaultPasswordComplexityPolicy().then((data) => {
|
this.newMgmtService.getDefaultPasswordComplexityPolicy().then((data) => {
|
||||||
if (data.policy) {
|
this.pwdForm = this.fb.group({
|
||||||
this.policy = data.policy;
|
password: ['', this.passwordComplexityValidatorFactory.buildValidators(data.policy)],
|
||||||
|
confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]],
|
||||||
if (this.policy.minLength) {
|
});
|
||||||
validators.push(minLengthValidator(this.policy.minLength));
|
|
||||||
}
|
|
||||||
if (this.policy.hasLowercase) {
|
|
||||||
validators.push(containsLowerCaseValidator);
|
|
||||||
}
|
|
||||||
if (this.policy.hasUppercase) {
|
|
||||||
validators.push(containsUpperCaseValidator);
|
|
||||||
}
|
|
||||||
if (this.policy.hasNumber) {
|
|
||||||
validators.push(containsNumberValidator);
|
|
||||||
}
|
|
||||||
if (this.policy.hasSymbol) {
|
|
||||||
validators.push(containsSymbolValidator);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pwdValidators = [...validators] as ValidatorFn[];
|
|
||||||
const confirmPwdValidators = [requiredValidator, passwordConfirmValidator()] as ValidatorFn[];
|
|
||||||
this.pwdForm = this.fb.group({
|
|
||||||
password: ['', pwdValidators],
|
|
||||||
confirmPassword: ['', confirmPwdValidators],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.pwdForm = this.fb.group({
|
this.pwdForm = this.fb.group({
|
||||||
@ -194,8 +166,8 @@ export class OrgCreateComponent {
|
|||||||
this.mgmtService
|
this.mgmtService
|
||||||
.addOrg(this.name.value)
|
.addOrg(this.name.value)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.authService.revalidateOrgs();
|
this.authService.revalidateOrgs().then();
|
||||||
this.router.navigate(['/orgs']);
|
this.router.navigate(['/orgs']).then();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { AuthGuard } from 'src/app/guards/auth.guard';
|
import { authGuard } from 'src/app/guards/auth.guard';
|
||||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
import { roleGuard } from 'src/app/guards/role-guard';
|
||||||
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
|
||||||
|
|
||||||
import { OrgDetailComponent } from './org-detail/org-detail.component';
|
import { OrgDetailComponent } from './org-detail/org-detail.component';
|
||||||
@ -13,7 +13,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'provider',
|
path: 'provider',
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
loadChildren: () => import('src/app/modules/providers/providers.module'),
|
loadChildren: () => import('src/app/modules/providers/providers.module'),
|
||||||
data: {
|
data: {
|
||||||
roles: ['org.idp.read'],
|
roles: ['org.idp.read'],
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
import { roleGuard } from 'src/app/guards/role-guard';
|
||||||
|
|
||||||
import { OwnedProjectDetailComponent } from './owned-project-detail.component';
|
import { OwnedProjectDetailComponent } from './owned-project-detail.component';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const routes: Routes = [
|
|||||||
animation: 'HomePage',
|
animation: 'HomePage',
|
||||||
roles: ['project.read'],
|
roles: ['project.read'],
|
||||||
},
|
},
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
import { roleGuard } from 'src/app/guards/role-guard';
|
||||||
import { ProjectType } from 'src/app/modules/project-members/project-members-datasource';
|
import { ProjectType } from 'src/app/modules/project-members/project-members-datasource';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@ -10,7 +10,7 @@ const routes: Routes = [
|
|||||||
animation: 'HomePage',
|
animation: 'HomePage',
|
||||||
roles: ['project.read'],
|
roles: ['project.read'],
|
||||||
},
|
},
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
loadChildren: () => import('./owned-project-detail/owned-project-detail.module'),
|
loadChildren: () => import('./owned-project-detail/owned-project-detail.module'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -19,7 +19,7 @@ const routes: Routes = [
|
|||||||
type: ProjectType.PROJECTTYPE_OWNED,
|
type: ProjectType.PROJECTTYPE_OWNED,
|
||||||
roles: ['project.member.read'],
|
roles: ['project.member.read'],
|
||||||
},
|
},
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
loadChildren: () => import('src/app/modules/project-members/project-members.module'),
|
loadChildren: () => import('src/app/modules/project-members/project-members.module'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,7 +28,7 @@ const routes: Routes = [
|
|||||||
animation: 'AddPage',
|
animation: 'AddPage',
|
||||||
roles: ['project.app.read'],
|
roles: ['project.app.read'],
|
||||||
},
|
},
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
loadChildren: () => import('src/app/pages/projects/apps/apps.module'),
|
loadChildren: () => import('src/app/pages/projects/apps/apps.module'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
import { roleGuard } from 'src/app/guards/role-guard';
|
||||||
|
|
||||||
import { ProjectsComponent } from './projects.component';
|
import { ProjectsComponent } from './projects.component';
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'create',
|
path: 'create',
|
||||||
loadChildren: () => import('./project-create/project-create.module'),
|
loadChildren: () => import('./project-create/project-create.module'),
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
data: {
|
data: {
|
||||||
animation: 'AddPage',
|
animation: 'AddPage',
|
||||||
roles: ['project.create'],
|
roles: ['project.create'],
|
||||||
@ -21,7 +21,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'app-create',
|
path: 'app-create',
|
||||||
canActivate: [RoleGuard],
|
canActivate: [roleGuard],
|
||||||
data: {
|
data: {
|
||||||
animation: 'AddPage',
|
animation: 'AddPage',
|
||||||
roles: ['project.app.write'],
|
roles: ['project.app.write'],
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
<cnsl-create-layout
|
||||||
|
title="{{ 'USER.CREATE.TITLE' | translate }}"
|
||||||
|
[createSteps]="1"
|
||||||
|
[currentCreateStep]="1"
|
||||||
|
(closed)="location.back()"
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<mat-progress-bar *ngIf="loading()" color="primary" mode="indeterminate" />
|
||||||
|
<form
|
||||||
|
*ngIf="authenticationFactor$ | async as authenticationFactor"
|
||||||
|
class="form-grid"
|
||||||
|
[formGroup]="userForm"
|
||||||
|
(ngSubmit)="createUserV2(authenticationFactor)"
|
||||||
|
>
|
||||||
|
<cnsl-form-field class="email">
|
||||||
|
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}</cnsl-label>
|
||||||
|
<input class="stretchInput" cnslInput matRipple formControlName="email" required />
|
||||||
|
</cnsl-form-field>
|
||||||
|
<div class="emailVerified">
|
||||||
|
<mat-checkbox [disabled]="authenticationFactor.factor === 'invitation'" formControlName="emailVerified">
|
||||||
|
{{ 'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate }}
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<cnsl-form-field class="givenName">
|
||||||
|
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label>
|
||||||
|
<input cnslInput formControlName="givenName" required />
|
||||||
|
</cnsl-form-field>
|
||||||
|
<cnsl-form-field class="familyName">
|
||||||
|
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</cnsl-label>
|
||||||
|
<input cnslInput formControlName="familyName" required />
|
||||||
|
</cnsl-form-field>
|
||||||
|
<cnsl-form-field class="nickName">
|
||||||
|
<cnsl-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</cnsl-label>
|
||||||
|
<input cnslInput formControlName="nickName" />
|
||||||
|
</cnsl-form-field>
|
||||||
|
<cnsl-form-field class="username">
|
||||||
|
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
|
||||||
|
<input cnslInput formControlName="username" required />
|
||||||
|
</cnsl-form-field>
|
||||||
|
|
||||||
|
<div class="authenticationFactor">
|
||||||
|
<mat-radio-group
|
||||||
|
class="authenticationFactorRadioGroup"
|
||||||
|
aria-label="Select an option"
|
||||||
|
formControlName="authenticationFactor"
|
||||||
|
>
|
||||||
|
<mat-radio-button value="none">{{ 'USER.CREATE.SETUPAUTHENTICATIONLATER' | translate }}</mat-radio-button>
|
||||||
|
<mat-radio-button value="invitation">{{ 'USER.CREATE.INVITATION' | translate }}</mat-radio-button>
|
||||||
|
<mat-radio-button value="initialPassword">{{ 'USER.CREATE.INITIALPASSWORD' | translate }}</mat-radio-button>
|
||||||
|
</mat-radio-group>
|
||||||
|
<form *ngIf="authenticationFactor.factor === 'initialPassword'" [formGroup]="authenticationFactor.form">
|
||||||
|
<cnsl-form-field>
|
||||||
|
<cnsl-label>{{ 'USER.PASSWORD.NEWINITIAL' | translate }}</cnsl-label>
|
||||||
|
<input
|
||||||
|
class="stretchInput"
|
||||||
|
cnslInput
|
||||||
|
autocomplete="off"
|
||||||
|
name="firstpassword"
|
||||||
|
formControlName="password"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</cnsl-form-field>
|
||||||
|
<cnsl-form-field>
|
||||||
|
<cnsl-label>{{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }}</cnsl-label>
|
||||||
|
<input
|
||||||
|
cnslInput
|
||||||
|
autocomplete="off"
|
||||||
|
name="confirmPassword"
|
||||||
|
formControlName="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
class="stretchInput"
|
||||||
|
/>
|
||||||
|
</cnsl-form-field>
|
||||||
|
<cnsl-password-complexity-view
|
||||||
|
class="complexity-view"
|
||||||
|
[policy]="authenticationFactor.policy"
|
||||||
|
[password]="authenticationFactor.form.controls.password"
|
||||||
|
>
|
||||||
|
</cnsl-password-complexity-view>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
data-e2e="create-button"
|
||||||
|
color="primary"
|
||||||
|
[disabled]="
|
||||||
|
userForm.invalid || (authenticationFactor.factor === 'initialPassword' && authenticationFactor.form.invalid)
|
||||||
|
"
|
||||||
|
type="submit"
|
||||||
|
class="create-button"
|
||||||
|
mat-raised-button
|
||||||
|
>
|
||||||
|
{{ 'ACTIONS.CREATE' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</cnsl-create-layout>
|
@ -0,0 +1,61 @@
|
|||||||
|
.content {
|
||||||
|
max-width: 45rem;
|
||||||
|
|
||||||
|
@media only screen and (max-width: 500px) {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-areas:
|
||||||
|
'email email'
|
||||||
|
'emailVerified emailVerified'
|
||||||
|
'givenName familyName'
|
||||||
|
'nickName username'
|
||||||
|
'authenticationFactor authenticationFactor';
|
||||||
|
column-gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email {
|
||||||
|
grid-area: email;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailVerified {
|
||||||
|
grid-area: emailVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
.givenName {
|
||||||
|
grid-area: givenName;
|
||||||
|
}
|
||||||
|
|
||||||
|
.familyName {
|
||||||
|
grid-area: familyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickName {
|
||||||
|
grid-area: nickName;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
grid-area: username;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authenticationFactor {
|
||||||
|
grid-area: authenticationFactor;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authenticationFactorRadioGroup > mat-radio-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authenticationFactorButton {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stretchInput {
|
||||||
|
max-width: unset;
|
||||||
|
}
|
@ -0,0 +1,259 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
import { FormBuilder, FormControl } from '@angular/forms';
|
||||||
|
import { UserService } from 'src/app/services/user.service';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import {
|
||||||
|
emailValidator,
|
||||||
|
minLengthValidator,
|
||||||
|
passwordConfirmValidator,
|
||||||
|
requiredValidator,
|
||||||
|
} from 'src/app/modules/form-field/validators/validators';
|
||||||
|
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||||
|
import {
|
||||||
|
defaultIfEmpty,
|
||||||
|
defer,
|
||||||
|
EMPTY,
|
||||||
|
firstValueFrom,
|
||||||
|
mergeWith,
|
||||||
|
NEVER,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
shareReplay,
|
||||||
|
TimeoutError,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { catchError, filter, map, startWith, timeout } from 'rxjs/operators';
|
||||||
|
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||||
|
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||||
|
import { AddHumanUserRequestSchema } from '@zitadel/proto/zitadel/user/v2/user_service_pb';
|
||||||
|
import { LoginV2FeatureFlag } from '@zitadel/proto/zitadel/feature/v2/feature_pb';
|
||||||
|
import { withLatestFromSynchronousFix } from 'src/app/utils/withLatestFromSynchronousFix';
|
||||||
|
import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service';
|
||||||
|
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
|
type PwdForm = ReturnType<UserCreateV2Component['buildPwdForm']>;
|
||||||
|
type AuthenticationFactor =
|
||||||
|
| { factor: 'none' }
|
||||||
|
| { factor: 'initialPassword'; form: PwdForm; policy: PasswordComplexityPolicy }
|
||||||
|
| { factor: 'invitation' };
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cnsl-user-create-v2',
|
||||||
|
templateUrl: './user-create-v2.component.html',
|
||||||
|
styleUrls: ['./user-create-v2.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class UserCreateV2Component implements OnInit {
|
||||||
|
protected readonly loading = signal(false);
|
||||||
|
|
||||||
|
protected readonly userForm: ReturnType<typeof this.buildUserForm>;
|
||||||
|
|
||||||
|
private readonly passwordComplexityPolicy$: Observable<PasswordComplexityPolicy>;
|
||||||
|
protected readonly authenticationFactor$: Observable<AuthenticationFactor>;
|
||||||
|
private readonly useLoginV2$: Observable<LoginV2FeatureFlag | undefined>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly router: Router,
|
||||||
|
private readonly toast: ToastService,
|
||||||
|
private readonly fb: FormBuilder,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly newMgmtService: NewMgmtService,
|
||||||
|
private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService,
|
||||||
|
private readonly featureService: NewFeatureService,
|
||||||
|
private readonly destroyRef: DestroyRef,
|
||||||
|
private readonly route: ActivatedRoute,
|
||||||
|
protected readonly location: Location,
|
||||||
|
) {
|
||||||
|
this.userForm = this.buildUserForm();
|
||||||
|
|
||||||
|
this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
|
this.authenticationFactor$ = this.getAuthenticationFactor(this.userForm, this.passwordComplexityPolicy$);
|
||||||
|
this.useLoginV2$ = this.getUseLoginV2().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.useLoginV2$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
|
||||||
|
this.authenticationFactor$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async ({ factor }) => {
|
||||||
|
// preserve current factor choice when reloading helpful while developing
|
||||||
|
await this.router.navigate([], {
|
||||||
|
relativeTo: this.route,
|
||||||
|
queryParams: {
|
||||||
|
factor,
|
||||||
|
},
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildUserForm() {
|
||||||
|
const param = this.route.snapshot.queryParamMap.get('factor');
|
||||||
|
const authenticationFactor =
|
||||||
|
param === 'none' ? param : param === 'initialPassword' ? param : param === 'invitation' ? param : 'none';
|
||||||
|
|
||||||
|
return this.fb.group({
|
||||||
|
email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }),
|
||||||
|
username: new FormControl('', { nonNullable: true, validators: [requiredValidator, minLengthValidator(2)] }),
|
||||||
|
givenName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||||
|
familyName: new FormControl('', { nonNullable: true, validators: [requiredValidator] }),
|
||||||
|
nickName: new FormControl('', { nonNullable: true }),
|
||||||
|
emailVerified: new FormControl(false, { nonNullable: true }),
|
||||||
|
authenticationFactor: new FormControl<AuthenticationFactor['factor']>(authenticationFactor, {
|
||||||
|
nonNullable: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPasswordComplexityPolicy() {
|
||||||
|
return defer(() => this.newMgmtService.getPasswordComplexityPolicy()).pipe(
|
||||||
|
map(({ policy }) => policy),
|
||||||
|
filter(Boolean),
|
||||||
|
catchError((error) => {
|
||||||
|
this.toast.showError(error);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthenticationFactor(
|
||||||
|
userForm: typeof this.userForm,
|
||||||
|
passwordComplexityPolicy$: Observable<PasswordComplexityPolicy>,
|
||||||
|
): Observable<AuthenticationFactor> {
|
||||||
|
const pwdForm$ = passwordComplexityPolicy$.pipe(
|
||||||
|
defaultIfEmpty(undefined),
|
||||||
|
map((policy) => this.buildPwdForm(policy)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return userForm.controls.authenticationFactor.valueChanges.pipe(
|
||||||
|
startWith(userForm.controls.authenticationFactor.value),
|
||||||
|
withLatestFromSynchronousFix(pwdForm$, passwordComplexityPolicy$),
|
||||||
|
map(([factor, form, policy]) => {
|
||||||
|
if (factor === 'initialPassword') {
|
||||||
|
return { factor, form, policy };
|
||||||
|
}
|
||||||
|
// reset emailVerified when we switch to invitation
|
||||||
|
if (factor === 'invitation') {
|
||||||
|
userForm.controls.emailVerified.setValue(false);
|
||||||
|
}
|
||||||
|
return { factor };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPwdForm(policy: PasswordComplexityPolicy | undefined) {
|
||||||
|
return this.fb.group({
|
||||||
|
password: new FormControl('', {
|
||||||
|
nonNullable: true,
|
||||||
|
validators: this.passwordComplexityValidatorFactory.buildValidators(policy),
|
||||||
|
}),
|
||||||
|
confirmPassword: new FormControl('', {
|
||||||
|
nonNullable: true,
|
||||||
|
validators: [requiredValidator, passwordConfirmValidator()],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUseLoginV2() {
|
||||||
|
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
||||||
|
map(({ loginV2 }) => loginV2),
|
||||||
|
timeout(1000),
|
||||||
|
catchError((err) => {
|
||||||
|
if (!(err instanceof TimeoutError)) {
|
||||||
|
this.toast.showError(err);
|
||||||
|
}
|
||||||
|
return of(undefined);
|
||||||
|
}),
|
||||||
|
mergeWith(NEVER),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async createUserV2(authenticationFactor: AuthenticationFactor) {
|
||||||
|
try {
|
||||||
|
await this.createUserV2Try(authenticationFactor);
|
||||||
|
} catch (error) {
|
||||||
|
this.toast.showError(error);
|
||||||
|
} finally {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createUserV2Try(authenticationFactor: AuthenticationFactor) {
|
||||||
|
this.loading.set(true);
|
||||||
|
|
||||||
|
const userValues = this.userForm.getRawValue();
|
||||||
|
|
||||||
|
const humanReq: MessageInitShape<typeof AddHumanUserRequestSchema> = {
|
||||||
|
username: userValues.username,
|
||||||
|
profile: {
|
||||||
|
givenName: userValues.givenName,
|
||||||
|
familyName: userValues.familyName,
|
||||||
|
nickName: userValues.nickName,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
email: userValues.email,
|
||||||
|
verification: {
|
||||||
|
case: 'isVerified',
|
||||||
|
value: userValues.emailVerified,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authenticationFactor.factor === 'initialPassword') {
|
||||||
|
const { password } = authenticationFactor.form.getRawValue();
|
||||||
|
humanReq.passwordType = {
|
||||||
|
case: 'password',
|
||||||
|
value: {
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await this.userService.addHumanUser(humanReq);
|
||||||
|
if (authenticationFactor.factor === 'invitation') {
|
||||||
|
const url = await this.getUrlTemplate();
|
||||||
|
await this.userService.createInviteCode({
|
||||||
|
userId: resp.userId,
|
||||||
|
verification: {
|
||||||
|
case: 'sendCode',
|
||||||
|
value: url
|
||||||
|
? {
|
||||||
|
urlTemplate: `${url}verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toast.showInfo('USER.TOAST.CREATED', true);
|
||||||
|
await this.router.navigate(['users', resp.userId], { queryParams: { new: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUrlTemplate() {
|
||||||
|
const useLoginV2 = await firstValueFrom(this.useLoginV2$);
|
||||||
|
if (!useLoginV2?.required) {
|
||||||
|
// loginV2 is not enabled
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { baseUri } = useLoginV2;
|
||||||
|
// if base uri is not set, we use the default for the cloud hosted login v2
|
||||||
|
if (!baseUri) {
|
||||||
|
return new URL(location.origin + '/ui/v2/login/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUriWithTrailingSlash = baseUri.endsWith('/') ? baseUri : `${baseUri}/`;
|
||||||
|
try {
|
||||||
|
// first we try to create a URL directly from the baseUri
|
||||||
|
return new URL(baseUriWithTrailingSlash);
|
||||||
|
} catch (_) {
|
||||||
|
// if this does not work we assume that the baseUri is relative,
|
||||||
|
// and we need to add the location.origin
|
||||||
|
// we make sure the relative url has a slash at the beginning and end
|
||||||
|
const baseUriWithSlashes = baseUriWithTrailingSlash.startsWith('/')
|
||||||
|
? baseUriWithTrailingSlash
|
||||||
|
: `/${baseUriWithTrailingSlash}`;
|
||||||
|
return new URL(location.origin + baseUriWithSlashes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
|
<cnsl-user-create-v2 *ngIf="(useV2Api$ | async) === true" />
|
||||||
<cnsl-create-layout
|
<cnsl-create-layout
|
||||||
|
*ngIf="(useV2Api$ | async) === false"
|
||||||
title="{{ 'USER.CREATE.TITLE' | translate }}"
|
title="{{ 'USER.CREATE.TITLE' | translate }}"
|
||||||
[createSteps]="1"
|
[createSteps]="1"
|
||||||
[currentCreateStep]="1"
|
[currentCreateStep]="1"
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { Component, DestroyRef, ElementRef, OnInit, ViewChild } from '@angular/core';
|
import { Component, DestroyRef, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||||
import { FormBuilder, FormControl, ValidatorFn } from '@angular/forms';
|
import { FormBuilder, FormControl } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { debounceTime, defer, of, Observable, shareReplay, forkJoin, ObservedValueOf, EMPTY, ReplaySubject } from 'rxjs';
|
import {
|
||||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
debounceTime,
|
||||||
|
defer,
|
||||||
|
of,
|
||||||
|
Observable,
|
||||||
|
shareReplay,
|
||||||
|
forkJoin,
|
||||||
|
ObservedValueOf,
|
||||||
|
EMPTY,
|
||||||
|
ReplaySubject,
|
||||||
|
TimeoutError,
|
||||||
|
} from 'rxjs';
|
||||||
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
|
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||||
import { ManagementService } from 'src/app/services/mgmt.service';
|
import { ManagementService } from 'src/app/services/mgmt.service';
|
||||||
@ -11,10 +21,6 @@ import { ToastService } from 'src/app/services/toast.service';
|
|||||||
import { CountryCallingCodesService, CountryPhoneCode } from 'src/app/services/country-calling-codes.service';
|
import { CountryCallingCodesService, CountryPhoneCode } from 'src/app/services/country-calling-codes.service';
|
||||||
import { formatPhone } from 'src/app/utils/formatPhone';
|
import { formatPhone } from 'src/app/utils/formatPhone';
|
||||||
import {
|
import {
|
||||||
containsLowerCaseValidator,
|
|
||||||
containsNumberValidator,
|
|
||||||
containsSymbolValidator,
|
|
||||||
containsUpperCaseValidator,
|
|
||||||
emailValidator,
|
emailValidator,
|
||||||
minLengthValidator,
|
minLengthValidator,
|
||||||
passwordConfirmValidator,
|
passwordConfirmValidator,
|
||||||
@ -23,8 +29,12 @@ import {
|
|||||||
} from 'src/app/modules/form-field/validators/validators';
|
} from 'src/app/modules/form-field/validators/validators';
|
||||||
import { LanguagesService } from 'src/app/services/languages.service';
|
import { LanguagesService } from 'src/app/services/languages.service';
|
||||||
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
import { AddHumanUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
|
||||||
import { catchError, map, startWith } from 'rxjs/operators';
|
import { catchError, map, startWith, timeout } from 'rxjs/operators';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||||
|
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||||
|
import { NewMgmtService } from 'src/app/services/new-mgmt.service';
|
||||||
|
import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-user-create',
|
selector: 'cnsl-user-create',
|
||||||
@ -43,15 +53,18 @@ export class UserCreateComponent implements OnInit {
|
|||||||
protected loading = false;
|
protected loading = false;
|
||||||
|
|
||||||
private readonly suffix$ = new ReplaySubject<HTMLSpanElement>(1);
|
private readonly suffix$ = new ReplaySubject<HTMLSpanElement>(1);
|
||||||
@ViewChild('suffix') public set suffix(suffix: ElementRef<HTMLSpanElement>) {
|
@ViewChild('suffix') public set suffix(suffix: ElementRef<HTMLSpanElement> | undefined) {
|
||||||
this.suffix$.next(suffix.nativeElement);
|
if (suffix?.nativeElement) {
|
||||||
|
this.suffix$.next(suffix.nativeElement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected usePassword: boolean = false;
|
protected usePassword: boolean = false;
|
||||||
protected readonly envSuffix$: Observable<string>;
|
protected readonly envSuffix$: Observable<string>;
|
||||||
protected readonly userForm: ReturnType<typeof this.buildUserForm>;
|
protected readonly userForm: ReturnType<typeof this.buildUserForm>;
|
||||||
protected readonly pwdForm$: ReturnType<typeof this.buildPwdForm>;
|
protected readonly pwdForm$: ReturnType<typeof this.buildPwdForm>;
|
||||||
protected readonly passwordComplexityPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>;
|
protected readonly passwordComplexityPolicy$: Observable<PasswordComplexityPolicy | undefined>;
|
||||||
|
protected readonly useV2Api$: Observable<boolean>;
|
||||||
protected readonly suffixPadding$: Observable<string>;
|
protected readonly suffixPadding$: Observable<string>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -59,24 +72,24 @@ export class UserCreateComponent implements OnInit {
|
|||||||
private readonly toast: ToastService,
|
private readonly toast: ToastService,
|
||||||
private readonly fb: FormBuilder,
|
private readonly fb: FormBuilder,
|
||||||
private readonly mgmtService: ManagementService,
|
private readonly mgmtService: ManagementService,
|
||||||
|
private readonly newMgmtService: NewMgmtService,
|
||||||
private readonly destroyRef: DestroyRef,
|
private readonly destroyRef: DestroyRef,
|
||||||
private readonly breadcrumbService: BreadcrumbService,
|
private readonly breadcrumbService: BreadcrumbService,
|
||||||
protected readonly location: Location,
|
protected readonly location: Location,
|
||||||
protected readonly langSvc: LanguagesService,
|
protected readonly langSvc: LanguagesService,
|
||||||
|
private readonly featureService: NewFeatureService,
|
||||||
|
private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService,
|
||||||
countryCallingCodesService: CountryCallingCodesService,
|
countryCallingCodesService: CountryCallingCodesService,
|
||||||
) {
|
) {
|
||||||
this.envSuffix$ = this.getEnvSuffix();
|
this.envSuffix$ = this.getEnvSuffix();
|
||||||
this.suffixPadding$ = this.getSuffixPadding();
|
this.suffixPadding$ = this.getSuffixPadding();
|
||||||
this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
this.passwordComplexityPolicy$ = this.getPasswordComplexityPolicy().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
|
this.useV2Api$ = this.getUseV2Api().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
|
|
||||||
this.userForm = this.buildUserForm();
|
this.userForm = this.buildUserForm();
|
||||||
this.pwdForm$ = this.buildPwdForm(this.passwordComplexityPolicy$);
|
this.pwdForm$ = this.buildPwdForm(this.passwordComplexityPolicy$);
|
||||||
|
|
||||||
this.countryPhoneCodes = countryCallingCodesService.getCountryCallingCodes();
|
this.countryPhoneCodes = countryCallingCodesService.getCountryCallingCodes();
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.watchPhoneChanges();
|
|
||||||
|
|
||||||
this.breadcrumbService.setBreadcrumb([
|
this.breadcrumbService.setBreadcrumb([
|
||||||
new Breadcrumb({
|
new Breadcrumb({
|
||||||
@ -86,6 +99,10 @@ export class UserCreateComponent implements OnInit {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.watchPhoneChanges();
|
||||||
|
}
|
||||||
|
|
||||||
private getEnvSuffix() {
|
private getEnvSuffix() {
|
||||||
const domainPolicy$ = defer(() => this.mgmtService.getDomainPolicy());
|
const domainPolicy$ = defer(() => this.mgmtService.getDomainPolicy());
|
||||||
const orgDomains$ = defer(() => this.mgmtService.listOrgDomains());
|
const orgDomains$ = defer(() => this.mgmtService.listOrgDomains());
|
||||||
@ -112,7 +129,7 @@ export class UserCreateComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getPasswordComplexityPolicy() {
|
private getPasswordComplexityPolicy() {
|
||||||
return defer(() => this.mgmtService.getPasswordComplexityPolicy()).pipe(
|
return defer(() => this.newMgmtService.getPasswordComplexityPolicy()).pipe(
|
||||||
map(({ policy }) => policy),
|
map(({ policy }) => policy),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
@ -121,6 +138,19 @@ export class UserCreateComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUseV2Api() {
|
||||||
|
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
||||||
|
map((features) => features.consoleUseV2UserApi?.enabled ?? false),
|
||||||
|
timeout(1000),
|
||||||
|
catchError((err) => {
|
||||||
|
if (!(err instanceof TimeoutError)) {
|
||||||
|
this.toast.showError(err);
|
||||||
|
}
|
||||||
|
return of(false);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private buildUserForm() {
|
private buildUserForm() {
|
||||||
return this.fb.group({
|
return this.fb.group({
|
||||||
email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }),
|
email: new FormControl('', { nonNullable: true, validators: [requiredValidator, emailValidator] }),
|
||||||
@ -135,27 +165,14 @@ export class UserCreateComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildPwdForm(passwordComplexityPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>) {
|
private buildPwdForm(passwordComplexityPolicy$: Observable<PasswordComplexityPolicy | undefined>) {
|
||||||
return passwordComplexityPolicy$.pipe(
|
return passwordComplexityPolicy$.pipe(
|
||||||
map((policy) => {
|
map((policy) => {
|
||||||
const validators: ValidatorFn[] = [requiredValidator];
|
|
||||||
if (policy?.minLength) {
|
|
||||||
validators.push(minLengthValidator(policy.minLength));
|
|
||||||
}
|
|
||||||
if (policy?.hasLowercase) {
|
|
||||||
validators.push(containsLowerCaseValidator);
|
|
||||||
}
|
|
||||||
if (policy?.hasUppercase) {
|
|
||||||
validators.push(containsUpperCaseValidator);
|
|
||||||
}
|
|
||||||
if (policy?.hasNumber) {
|
|
||||||
validators.push(containsNumberValidator);
|
|
||||||
}
|
|
||||||
if (policy?.hasSymbol) {
|
|
||||||
validators.push(containsSymbolValidator);
|
|
||||||
}
|
|
||||||
return this.fb.group({
|
return this.fb.group({
|
||||||
password: new FormControl('', { nonNullable: true, validators }),
|
password: new FormControl('', {
|
||||||
|
nonNullable: true,
|
||||||
|
validators: this.passwordComplexityValidatorFactory.buildValidators(policy),
|
||||||
|
}),
|
||||||
confirmPassword: new FormControl('', {
|
confirmPassword: new FormControl('', {
|
||||||
nonNullable: true,
|
nonNullable: true,
|
||||||
validators: [requiredValidator, passwordConfirmValidator()],
|
validators: [requiredValidator, passwordConfirmValidator()],
|
||||||
|
@ -19,9 +19,11 @@ import { PasswordComplexityViewModule } from 'src/app/modules/password-complexit
|
|||||||
import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service';
|
import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service';
|
||||||
import { UserCreateRoutingModule } from './user-create-routing.module';
|
import { UserCreateRoutingModule } from './user-create-routing.module';
|
||||||
import { UserCreateComponent } from './user-create.component';
|
import { UserCreateComponent } from './user-create.component';
|
||||||
|
import { UserCreateV2Component } from './user-create-v2/user-create-v2.component';
|
||||||
|
import { MatRadioModule } from '@angular/material/radio';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [UserCreateComponent],
|
declarations: [UserCreateComponent, UserCreateV2Component],
|
||||||
providers: [CountryCallingCodesService],
|
providers: [CountryCallingCodesService],
|
||||||
imports: [
|
imports: [
|
||||||
UserCreateRoutingModule,
|
UserCreateRoutingModule,
|
||||||
@ -42,6 +44,7 @@ import { UserCreateComponent } from './user-create.component';
|
|||||||
DetailLayoutModule,
|
DetailLayoutModule,
|
||||||
InputModule,
|
InputModule,
|
||||||
MatRippleModule,
|
MatRippleModule,
|
||||||
|
MatRadioModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export default class UserCreateModule {}
|
export default class UserCreateModule {}
|
||||||
|
@ -192,7 +192,7 @@ export class AuthUserDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getMyUser(): Observable<UserQuery> {
|
private getMyUser(): Observable<UserQuery> {
|
||||||
return defer(() => this.userService.getMyUser()).pipe(
|
return this.userService.user$.pipe(
|
||||||
map((user) => ({ state: 'success' as const, value: user })),
|
map((user) => ({ state: 'success' as const, value: user })),
|
||||||
catchError((error) => of({ state: 'error', error } as const)),
|
catchError((error) => of({ state: 'error', error } as const)),
|
||||||
startWith({ state: 'loading' } as const),
|
startWith({ state: 'loading' } as const),
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { Component, DestroyRef, OnInit } from '@angular/core';
|
import { Component, DestroyRef, OnInit } from '@angular/core';
|
||||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
take,
|
|
||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
@ -12,24 +11,18 @@ import {
|
|||||||
of,
|
of,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
combineLatestWith,
|
combineLatestWith,
|
||||||
|
EMPTY,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import { passwordConfirmValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||||
containsLowerCaseValidator,
|
|
||||||
containsNumberValidator,
|
|
||||||
containsSymbolValidator,
|
|
||||||
containsUpperCaseValidator,
|
|
||||||
minLengthValidator,
|
|
||||||
passwordConfirmValidator,
|
|
||||||
requiredValidator,
|
|
||||||
} from 'src/app/modules/form-field/validators/validators';
|
|
||||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
|
|
||||||
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
|
||||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { catchError, filter } from 'rxjs/operators';
|
import { catchError, filter } from 'rxjs/operators';
|
||||||
import { User } from 'src/app/proto/generated/zitadel/user_pb';
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { UserService } from '../../../../services/user.service';
|
import { UserService } from 'src/app/services/user.service';
|
||||||
|
import { User } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||||
|
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||||
|
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||||
|
import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-password',
|
selector: 'cnsl-password',
|
||||||
@ -41,34 +34,53 @@ export class PasswordComponent implements OnInit {
|
|||||||
protected readonly username$: Observable<string>;
|
protected readonly username$: Observable<string>;
|
||||||
protected readonly id$: Observable<string | undefined>;
|
protected readonly id$: Observable<string | undefined>;
|
||||||
protected readonly form$: Observable<UntypedFormGroup>;
|
protected readonly form$: Observable<UntypedFormGroup>;
|
||||||
protected readonly passwordPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>;
|
protected readonly passwordPolicy$: Observable<PasswordComplexityPolicy | undefined>;
|
||||||
protected readonly user$: Observable<User.AsObject>;
|
protected readonly user$: Observable<User>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
activatedRoute: ActivatedRoute,
|
private readonly activatedRoute: ActivatedRoute,
|
||||||
private readonly fb: UntypedFormBuilder,
|
private readonly fb: UntypedFormBuilder,
|
||||||
private readonly authService: GrpcAuthService,
|
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
|
private readonly newAuthService: NewAuthService,
|
||||||
private readonly toast: ToastService,
|
private readonly toast: ToastService,
|
||||||
private readonly breadcrumbService: BreadcrumbService,
|
private readonly breadcrumbService: BreadcrumbService,
|
||||||
private readonly destroyRef: DestroyRef,
|
private readonly destroyRef: DestroyRef,
|
||||||
|
private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService,
|
||||||
) {
|
) {
|
||||||
const usernameParam$ = activatedRoute.queryParamMap.pipe(
|
|
||||||
map((params) => params.get('username')),
|
|
||||||
filter(Boolean),
|
|
||||||
);
|
|
||||||
this.id$ = activatedRoute.paramMap.pipe(map((params) => params.get('id') ?? undefined));
|
this.id$ = activatedRoute.paramMap.pipe(map((params) => params.get('id') ?? undefined));
|
||||||
|
this.user$ = this.getUser().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
this.user$ = this.authService.user.pipe(take(1), filter(Boolean));
|
this.username$ = this.getUsername(this.user$);
|
||||||
this.username$ = usernameParam$.pipe(mergeWith(this.user$.pipe(map((user) => user.preferredLoginName))));
|
|
||||||
|
|
||||||
this.breadcrumb$ = this.getBreadcrumb$(this.id$, this.user$);
|
this.breadcrumb$ = this.getBreadcrumb$(this.id$, this.user$);
|
||||||
this.passwordPolicy$ = this.getPasswordPolicy$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
this.passwordPolicy$ = this.getPasswordPolicy$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
const validators$ = this.getValidators$(this.passwordPolicy$);
|
this.form$ = this.getForm$(this.id$, this.passwordPolicy$);
|
||||||
this.form$ = this.getForm$(this.id$, validators$);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBreadcrumb$(id$: Observable<string | undefined>, user$: Observable<User.AsObject>): Observable<Breadcrumb[]> {
|
ngOnInit() {
|
||||||
|
this.breadcrumb$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((breadcrumbs) => {
|
||||||
|
this.breadcrumbService.setBreadcrumb(breadcrumbs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUser() {
|
||||||
|
return this.userService.user$.pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
this.toast.showError(err);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUsername(user$: Observable<User>) {
|
||||||
|
const prefferedLoginName$ = user$.pipe(map((user) => user.preferredLoginName));
|
||||||
|
|
||||||
|
return this.activatedRoute.queryParamMap.pipe(
|
||||||
|
map((params) => params.get('username')),
|
||||||
|
filter(Boolean),
|
||||||
|
mergeWith(prefferedLoginName$),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBreadcrumb$(id$: Observable<string | undefined>, user$: Observable<User>): Observable<Breadcrumb[]> {
|
||||||
return id$.pipe(
|
return id$.pipe(
|
||||||
switchMap(async (id) => {
|
switchMap(async (id) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -86,7 +98,7 @@ export class PasswordComponent implements OnInit {
|
|||||||
return [
|
return [
|
||||||
new Breadcrumb({
|
new Breadcrumb({
|
||||||
type: BreadcrumbType.AUTHUSER,
|
type: BreadcrumbType.AUTHUSER,
|
||||||
name: user.human?.profile?.displayName,
|
name: (user.type.case === 'human' && user.type.value.profile?.displayName) || undefined,
|
||||||
routerLink: ['/users', 'me'],
|
routerLink: ['/users', 'me'],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@ -94,39 +106,22 @@ export class PasswordComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getValidators$(
|
private getPasswordPolicy$(): Observable<PasswordComplexityPolicy | undefined> {
|
||||||
passwordPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>,
|
return defer(() => this.newAuthService.getMyPasswordComplexityPolicy()).pipe(
|
||||||
): Observable<Validators[]> {
|
map((resp) => resp.policy),
|
||||||
return passwordPolicy$.pipe(
|
catchError((err) => {
|
||||||
map((policy) => {
|
this.toast.showError(err);
|
||||||
const validators: Validators[] = [requiredValidator];
|
return of(undefined);
|
||||||
if (!policy) {
|
|
||||||
return validators;
|
|
||||||
}
|
|
||||||
if (policy.minLength) {
|
|
||||||
validators.push(minLengthValidator(policy.minLength));
|
|
||||||
}
|
|
||||||
if (policy.hasLowercase) {
|
|
||||||
validators.push(containsLowerCaseValidator);
|
|
||||||
}
|
|
||||||
if (policy.hasUppercase) {
|
|
||||||
validators.push(containsUpperCaseValidator);
|
|
||||||
}
|
|
||||||
if (policy.hasNumber) {
|
|
||||||
validators.push(containsNumberValidator);
|
|
||||||
}
|
|
||||||
if (policy.hasSymbol) {
|
|
||||||
validators.push(containsSymbolValidator);
|
|
||||||
}
|
|
||||||
return validators;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getForm$(
|
private getForm$(
|
||||||
id$: Observable<string | undefined>,
|
id$: Observable<string | undefined>,
|
||||||
validators$: Observable<Validators[]>,
|
policy$: Observable<PasswordComplexityPolicy | undefined>,
|
||||||
): Observable<UntypedFormGroup> {
|
): Observable<UntypedFormGroup> {
|
||||||
|
const validators$ = policy$.pipe(map((policy) => this.passwordComplexityValidatorFactory.buildValidators(policy)));
|
||||||
|
|
||||||
return id$.pipe(
|
return id$.pipe(
|
||||||
combineLatestWith(validators$),
|
combineLatestWith(validators$),
|
||||||
map(([id, validators]) => {
|
map(([id, validators]) => {
|
||||||
@ -146,19 +141,6 @@ export class PasswordComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPasswordPolicy$(): Observable<PasswordComplexityPolicy.AsObject | undefined> {
|
|
||||||
return defer(() => this.authService.getMyPasswordComplexityPolicy()).pipe(
|
|
||||||
map((resp) => resp.policy),
|
|
||||||
catchError(() => of(undefined)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.breadcrumb$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((breadcrumbs) => {
|
|
||||||
this.breadcrumbService.setBreadcrumb(breadcrumbs);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setInitialPassword(userId: string, form: UntypedFormGroup): Promise<void> {
|
public async setInitialPassword(userId: string, form: UntypedFormGroup): Promise<void> {
|
||||||
const password = this.password(form)?.value;
|
const password = this.password(form)?.value;
|
||||||
|
|
||||||
@ -182,7 +164,7 @@ export class PasswordComponent implements OnInit {
|
|||||||
window.history.back();
|
window.history.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setPassword(form: UntypedFormGroup, user: User.AsObject): Promise<void> {
|
public async setPassword(form: UntypedFormGroup, user: User): Promise<void> {
|
||||||
const currentPassword = this.currentPassword(form);
|
const currentPassword = this.currentPassword(form);
|
||||||
const newPassword = this.newPassword(form);
|
const newPassword = this.newPassword(form);
|
||||||
|
|
||||||
@ -192,7 +174,7 @@ export class PasswordComponent implements OnInit {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.userService.setPassword({
|
await this.userService.setPassword({
|
||||||
userId: user.id,
|
userId: user.userId,
|
||||||
newPassword: {
|
newPassword: {
|
||||||
password: newPassword.value,
|
password: newPassword.value,
|
||||||
changeRequired: false,
|
changeRequired: false,
|
||||||
|
@ -159,7 +159,7 @@ export class UserTableComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getMyUser() {
|
private getMyUser() {
|
||||||
return defer(() => this.userService.getMyUser()).pipe(
|
return this.userService.user$.pipe(
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
this.toast.showError(error);
|
this.toast.showError(error);
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { AuthGuard } from 'src/app/guards/auth.guard';
|
import { authGuard } from 'src/app/guards/auth.guard';
|
||||||
import { RoleGuard } from 'src/app/guards/role.guard';
|
import { roleGuard } from 'src/app/guards/role-guard';
|
||||||
import { UserGuard } from 'src/app/guards/user.guard';
|
import { userGuard } from 'src/app/guards/user-guard';
|
||||||
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
|
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
|
||||||
|
|
||||||
import { AuthUserDetailComponent } from './user-detail/auth-user-detail/auth-user-detail.component';
|
import { AuthUserDetailComponent } from './user-detail/auth-user-detail/auth-user-detail.component';
|
||||||
@ -22,7 +22,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'create',
|
path: 'create',
|
||||||
loadChildren: () => import('./user-create/user-create.module'),
|
loadChildren: () => import('./user-create/user-create.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['user.write'],
|
roles: ['user.write'],
|
||||||
},
|
},
|
||||||
@ -30,7 +30,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'create-machine',
|
path: 'create-machine',
|
||||||
loadChildren: () => import('./user-create-machine/user-create-machine.module'),
|
loadChildren: () => import('./user-create-machine/user-create-machine.module'),
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['user.write'],
|
roles: ['user.write'],
|
||||||
},
|
},
|
||||||
@ -38,7 +38,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'me',
|
path: 'me',
|
||||||
component: AuthUserDetailComponent,
|
component: AuthUserDetailComponent,
|
||||||
canActivate: [AuthGuard],
|
canActivate: [authGuard],
|
||||||
data: {
|
data: {
|
||||||
animation: 'HomePage',
|
animation: 'HomePage',
|
||||||
},
|
},
|
||||||
@ -46,13 +46,13 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'me/password',
|
path: 'me/password',
|
||||||
component: PasswordComponent,
|
component: PasswordComponent,
|
||||||
canActivate: [AuthGuard],
|
canActivate: [authGuard],
|
||||||
data: { animation: 'AddPage' },
|
data: { animation: 'AddPage' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
component: UserDetailComponent,
|
component: UserDetailComponent,
|
||||||
canActivate: [AuthGuard, UserGuard, RoleGuard],
|
canActivate: [authGuard, userGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['user.read'],
|
roles: ['user.read'],
|
||||||
animation: 'HomePage',
|
animation: 'HomePage',
|
||||||
@ -61,7 +61,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: ':id/password',
|
path: ':id/password',
|
||||||
component: PasswordComponent,
|
component: PasswordComponent,
|
||||||
canActivate: [AuthGuard, RoleGuard],
|
canActivate: [authGuard, roleGuard],
|
||||||
data: {
|
data: {
|
||||||
roles: ['user.write'],
|
roles: ['user.write'],
|
||||||
animation: 'AddPage',
|
animation: 'AddPage',
|
||||||
|
@ -18,7 +18,7 @@ import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } f
|
|||||||
import { StorageService } from './storage.service';
|
import { StorageService } from './storage.service';
|
||||||
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
|
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import { createUserServiceClient } from '@zitadel/client/v2';
|
import { createFeatureServiceClient, createUserServiceClient } from '@zitadel/client/v2';
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
|
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
|
||||||
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
||||||
@ -36,6 +36,7 @@ export class GrpcService {
|
|||||||
public userNew!: ReturnType<typeof createUserServiceClient>;
|
public userNew!: ReturnType<typeof createUserServiceClient>;
|
||||||
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
|
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
|
||||||
public authNew!: ReturnType<typeof createAuthServiceClient>;
|
public authNew!: ReturnType<typeof createAuthServiceClient>;
|
||||||
|
public featureNew!: ReturnType<typeof createFeatureServiceClient>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly envService: EnvironmentService,
|
private readonly envService: EnvironmentService,
|
||||||
@ -114,6 +115,7 @@ export class GrpcService {
|
|||||||
this.userNew = createUserServiceClient(transport);
|
this.userNew = createUserServiceClient(transport);
|
||||||
this.mgmtNew = createManagementServiceClient(transportOldAPIs);
|
this.mgmtNew = createManagementServiceClient(transportOldAPIs);
|
||||||
this.authNew = createAuthServiceClient(transport);
|
this.authNew = createAuthServiceClient(transport);
|
||||||
|
this.featureNew = createFeatureServiceClient(transport);
|
||||||
|
|
||||||
const authConfig: AuthConfig = {
|
const authConfig: AuthConfig = {
|
||||||
scope: 'openid profile email',
|
scope: 'openid profile email',
|
||||||
|
@ -17,15 +17,15 @@ const accessTokenStorageKey = 'access_token';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthInterceptorProvider {
|
export class AuthInterceptorProvider {
|
||||||
public triggerDialog: Subject<boolean> = new Subject();
|
private readonly triggerDialog: Subject<boolean> = new Subject();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authenticationService: AuthenticationService,
|
private readonly authenticationService: AuthenticationService,
|
||||||
private storageService: StorageService,
|
private readonly storageService: StorageService,
|
||||||
private dialog: MatDialog,
|
private readonly dialog: MatDialog,
|
||||||
private destroyRef: DestroyRef,
|
destroyRef: DestroyRef,
|
||||||
) {
|
) {
|
||||||
this.triggerDialog.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef)).subscribe(() => this.openDialog());
|
this.triggerDialog.pipe(debounceTime(1000), takeUntilDestroyed(destroyRef)).subscribe(() => this.openDialog());
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): Observable<string> {
|
getToken(): Observable<string> {
|
||||||
|
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { GrpcService } from './grpc.service';
|
import { GrpcService } from './grpc.service';
|
||||||
import {
|
import {
|
||||||
AddMyAuthFactorOTPSMSResponse,
|
AddMyAuthFactorOTPSMSResponse,
|
||||||
|
GetMyPasswordComplexityPolicyResponse,
|
||||||
GetMyUserResponse,
|
GetMyUserResponse,
|
||||||
ListMyMetadataResponse,
|
ListMyMetadataResponse,
|
||||||
VerifyMyPhoneResponse,
|
VerifyMyPhoneResponse,
|
||||||
@ -28,4 +29,8 @@ export class NewAuthService {
|
|||||||
public listMyMetadata(): Promise<ListMyMetadataResponse> {
|
public listMyMetadata(): Promise<ListMyMetadataResponse> {
|
||||||
return this.grpcService.authNew.listMyMetadata({});
|
return this.grpcService.authNew.listMyMetadata({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getMyPasswordComplexityPolicy(): Promise<GetMyPasswordComplexityPolicyResponse> {
|
||||||
|
return this.grpcService.authNew.getMyPasswordComplexityPolicy({});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
14
console/src/app/services/new-feature.service.ts
Normal file
14
console/src/app/services/new-feature.service.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { GrpcService } from './grpc.service';
|
||||||
|
import { GetInstanceFeaturesResponse } from '@zitadel/proto/zitadel/feature/v2/instance_pb';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class NewFeatureService {
|
||||||
|
constructor(private readonly grpcService: GrpcService) {}
|
||||||
|
|
||||||
|
public getInstanceFeatures(): Promise<GetInstanceFeaturesResponse> {
|
||||||
|
return this.grpcService.featureNew.getInstanceFeatures({});
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,10 @@ import { GrpcService } from './grpc.service';
|
|||||||
import {
|
import {
|
||||||
GenerateMachineSecretRequestSchema,
|
GenerateMachineSecretRequestSchema,
|
||||||
GenerateMachineSecretResponse,
|
GenerateMachineSecretResponse,
|
||||||
|
GetDefaultPasswordComplexityPolicyResponse,
|
||||||
GetLoginPolicyRequestSchema,
|
GetLoginPolicyRequestSchema,
|
||||||
GetLoginPolicyResponse,
|
GetLoginPolicyResponse,
|
||||||
|
GetPasswordComplexityPolicyResponse,
|
||||||
ListUserMetadataRequestSchema,
|
ListUserMetadataRequestSchema,
|
||||||
ListUserMetadataResponse,
|
ListUserMetadataResponse,
|
||||||
RemoveMachineSecretRequestSchema,
|
RemoveMachineSecretRequestSchema,
|
||||||
@ -89,4 +91,12 @@ export class NewMgmtService {
|
|||||||
): Promise<RemoveUserMetadataResponse> {
|
): Promise<RemoveUserMetadataResponse> {
|
||||||
return this.grpcService.mgmtNew.removeUserMetadata(create(RemoveUserMetadataRequestSchema, req));
|
return this.grpcService.mgmtNew.removeUserMetadata(create(RemoveUserMetadataRequestSchema, req));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getPasswordComplexityPolicy(): Promise<GetPasswordComplexityPolicyResponse> {
|
||||||
|
return this.grpcService.mgmtNew.getPasswordComplexityPolicy({});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDefaultPasswordComplexityPolicy(): Promise<GetDefaultPasswordComplexityPolicyResponse> {
|
||||||
|
return this.grpcService.mgmtNew.getDefaultPasswordComplexityPolicy({});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ValidatorFn } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
containsLowerCaseValidator,
|
||||||
|
containsNumberValidator,
|
||||||
|
containsSymbolValidator,
|
||||||
|
containsUpperCaseValidator,
|
||||||
|
minLengthValidator,
|
||||||
|
requiredValidator,
|
||||||
|
} from '../modules/form-field/validators/validators';
|
||||||
|
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class PasswordComplexityValidatorFactoryService {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
buildValidators(policy?: PasswordComplexityPolicy) {
|
||||||
|
const validators: ValidatorFn[] = [requiredValidator];
|
||||||
|
if (policy?.minLength) {
|
||||||
|
validators.push(minLengthValidator(Number(policy.minLength)));
|
||||||
|
}
|
||||||
|
if (policy?.hasLowercase) {
|
||||||
|
validators.push(containsLowerCaseValidator);
|
||||||
|
}
|
||||||
|
if (policy?.hasUppercase) {
|
||||||
|
validators.push(containsUpperCaseValidator);
|
||||||
|
}
|
||||||
|
if (policy?.hasNumber) {
|
||||||
|
validators.push(containsNumberValidator);
|
||||||
|
}
|
||||||
|
if (policy?.hasSymbol) {
|
||||||
|
validators.push(containsSymbolValidator);
|
||||||
|
}
|
||||||
|
return validators;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { DestroyRef, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { GrpcService } from './grpc.service';
|
import { GrpcService } from './grpc.service';
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequestSchema,
|
AddHumanUserRequestSchema,
|
||||||
@ -70,57 +70,65 @@ import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
|
|||||||
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
|
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
|
||||||
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
|
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
|
||||||
import { OAuthService } from 'angular-oauth2-oidc';
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
import { firstValueFrom, Observable, shareReplay } from 'rxjs';
|
import { debounceTime, EMPTY, Observable, of, ReplaySubject, shareReplay, switchAll, switchMap } from 'rxjs';
|
||||||
import { filter, map, startWith, tap, timeout } from 'rxjs/operators';
|
import { catchError, filter, map, startWith } from 'rxjs/operators';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class UserService {
|
export class UserService {
|
||||||
private readonly userId$: Observable<string>;
|
private user$$ = new ReplaySubject<Observable<UserV2>>(1);
|
||||||
private user: UserV2 | undefined;
|
public user$ = this.user$$.pipe(
|
||||||
|
startWith(this.getUser()),
|
||||||
|
// makes sure if many subscribers reset the observable only one wins
|
||||||
|
debounceTime(10),
|
||||||
|
switchAll(),
|
||||||
|
catchError((err) => {
|
||||||
|
// reset user observable on error
|
||||||
|
this.user$$.next(this.getUser());
|
||||||
|
throw err;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly grpcService: GrpcService,
|
private readonly grpcService: GrpcService,
|
||||||
private readonly oauthService: OAuthService,
|
private readonly oauthService: OAuthService,
|
||||||
destroyRef: DestroyRef,
|
) {}
|
||||||
) {
|
|
||||||
this.userId$ = this.getUserId().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
|
||||||
|
|
||||||
// this preloads the userId and deletes the cache everytime the userId changes
|
|
||||||
this.userId$.pipe(takeUntilDestroyed(destroyRef)).subscribe(async () => {
|
|
||||||
this.user = undefined;
|
|
||||||
try {
|
|
||||||
await this.getMyUser();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUserId() {
|
private getUserId() {
|
||||||
return this.oauthService.events.pipe(
|
return this.oauthService.events.pipe(
|
||||||
filter((event) => event.type === 'token_received'),
|
filter((event) => event.type === 'token_received'),
|
||||||
startWith(this.oauthService.getIdToken),
|
|
||||||
map(() => this.oauthService.getIdToken()),
|
map(() => this.oauthService.getIdToken()),
|
||||||
|
startWith(this.oauthService.getIdToken()),
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
// split jwt and get base64 encoded payload
|
switchMap((token) => {
|
||||||
map((token) => token.split('.')[1]),
|
// we do this in a try catch so the observable will retry this logic if it fails
|
||||||
// decode payload
|
try {
|
||||||
map(atob),
|
// split jwt and get base64 encoded payload
|
||||||
// parse payload
|
const unparsedPayload = atob(token.split('.')[1]);
|
||||||
map((payload) => JSON.parse(payload)),
|
// parse payload
|
||||||
map((payload: unknown) => {
|
const payload: unknown = JSON.parse(unparsedPayload);
|
||||||
// check if sub is in payload and is a string
|
// check if sub is in payload and is a string
|
||||||
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
|
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
|
||||||
return payload.sub;
|
return of(payload.sub);
|
||||||
|
}
|
||||||
|
return EMPTY;
|
||||||
|
} catch {
|
||||||
|
return EMPTY;
|
||||||
}
|
}
|
||||||
throw new Error('Invalid payload');
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getUser() {
|
||||||
|
return this.getUserId().pipe(
|
||||||
|
switchMap((id) => this.getUserById(id)),
|
||||||
|
map((resp) => resp.user),
|
||||||
|
filter(Boolean),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
|
public addHumanUser(req: MessageInitShape<typeof AddHumanUserRequestSchema>): Promise<AddHumanUserResponse> {
|
||||||
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
|
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
|
||||||
}
|
}
|
||||||
@ -129,20 +137,6 @@ export class UserService {
|
|||||||
return this.grpcService.userNew.listUsers(req);
|
return this.grpcService.userNew.listUsers(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMyUser(): Promise<UserV2> {
|
|
||||||
const userId = await firstValueFrom(this.userId$.pipe(timeout(2000)));
|
|
||||||
if (this.user) {
|
|
||||||
return this.user;
|
|
||||||
}
|
|
||||||
const resp = await this.getUserById(userId);
|
|
||||||
if (!resp.user) {
|
|
||||||
throw new Error("Couldn't find user");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.user = resp.user;
|
|
||||||
return resp.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getUserById(userId: string): Promise<GetUserByIDResponse> {
|
public getUserById(userId: string): Promise<GetUserByIDResponse> {
|
||||||
return this.grpcService.userNew.getUserByID({ userId });
|
return this.grpcService.userNew.getUserByID({ userId });
|
||||||
}
|
}
|
||||||
|
@ -788,7 +788,10 @@
|
|||||||
"PHONESECTION": "Телефонни номера",
|
"PHONESECTION": "Телефонни номера",
|
||||||
"PASSWORDSECTION": "Първоначална парола",
|
"PASSWORDSECTION": "Първоначална парола",
|
||||||
"ADDRESSANDPHONESECTION": "Телефонен номер",
|
"ADDRESSANDPHONESECTION": "Телефонен номер",
|
||||||
"INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. "
|
"INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. ",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Настройте удостоверяване по-късно за този потребител.",
|
||||||
|
"INVITATION": "Изпратете покана по имейл за настройка на удостоверяване и потвърждение на имейл.",
|
||||||
|
"INITIALPASSWORD": "Задайте начална парола за потребителя."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Потвърдете телефонния номер",
|
"TITLE": "Потвърдете телефонния номер",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Telefonní čísla",
|
"PHONESECTION": "Telefonní čísla",
|
||||||
"PASSWORDSECTION": "Prvotní heslo",
|
"PASSWORDSECTION": "Prvotní heslo",
|
||||||
"ADDRESSANDPHONESECTION": "Telefonní číslo",
|
"ADDRESSANDPHONESECTION": "Telefonní číslo",
|
||||||
"INITMAILDESCRIPTION": "Pokud jsou vybrány obě možnosti, nebude odeslán e-mail pro inicializaci. Pokud je vybrána pouze jedna z možností, bude odeslán e-mail pro poskytnutí/ověření údajů."
|
"INITMAILDESCRIPTION": "Pokud jsou vybrány obě možnosti, nebude odeslán e-mail pro inicializaci. Pokud je vybrána pouze jedna z možností, bude odeslán e-mail pro poskytnutí/ověření údajů.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Nastavte ověřování později pro tohoto uživatele.",
|
||||||
|
"INVITATION": "Odešlete pozvánkový e-mail pro nastavení ověřování a ověření e-mailu.",
|
||||||
|
"INITIALPASSWORD": "Nastavte počáteční heslo pro uživatele."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Ověření telefonního čísla",
|
"TITLE": "Ověření telefonního čísla",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Telefonnummer",
|
"PHONESECTION": "Telefonnummer",
|
||||||
"PASSWORDSECTION": "Setze ein initiales Passwort.",
|
"PASSWORDSECTION": "Setze ein initiales Passwort.",
|
||||||
"ADDRESSANDPHONESECTION": "Telefonnummer",
|
"ADDRESSANDPHONESECTION": "Telefonnummer",
|
||||||
"INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Verifikation der Daten gesendet."
|
"INITMAILDESCRIPTION": "Wenn beide Optionen ausgewählt sind, wird keine E-Mail zur Initialisierung gesendet. Wenn nur eine der Optionen ausgewählt ist, wird eine E-Mail zur Verifikation der Daten gesendet.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Authentifizierung später für diesen Benutzer einrichten.",
|
||||||
|
"INVITATION": "Eine Einladung per E-Mail für die Authentifizierungseinrichtung und E-Mail-Verifizierung senden.",
|
||||||
|
"INITIALPASSWORD": "Setze ein initiales Passwort für den Benutzer."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Telefonnummer verifizieren",
|
"TITLE": "Telefonnummer verifizieren",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Phone numbers",
|
"PHONESECTION": "Phone numbers",
|
||||||
"PASSWORDSECTION": "Initial Password",
|
"PASSWORDSECTION": "Initial Password",
|
||||||
"ADDRESSANDPHONESECTION": "Phone number",
|
"ADDRESSANDPHONESECTION": "Phone number",
|
||||||
"INITMAILDESCRIPTION": "If both options are selected, no email for initialization will be sent. If only one of the options is selected, a mail to provide / verify the data will be sent."
|
"INITMAILDESCRIPTION": "If both options are selected, no email for initialization will be sent. If only one of the options is selected, a mail to provide / verify the data will be sent.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Setup authentication later for this User.",
|
||||||
|
"INVITATION": "Send an invitation E-Mail for authentication setup and E-Mail verification.",
|
||||||
|
"INITIALPASSWORD": "Set an initial password for the User."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Verify Phone Number",
|
"TITLE": "Verify Phone Number",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Números de teléfono",
|
"PHONESECTION": "Números de teléfono",
|
||||||
"PASSWORDSECTION": "Contraseña inicial",
|
"PASSWORDSECTION": "Contraseña inicial",
|
||||||
"ADDRESSANDPHONESECTION": "Número de teléfono",
|
"ADDRESSANDPHONESECTION": "Número de teléfono",
|
||||||
"INITMAILDESCRIPTION": "Si ambas opciones se seleccionan, no se enviará un email para la inicialización. Si solo una de las opciones se selecciona, un email se enviará para proporcionar / verificar los datos."
|
"INITMAILDESCRIPTION": "Si ambas opciones se seleccionan, no se enviará un email para la inicialización. Si solo una de las opciones se selecciona, un email se enviará para proporcionar / verificar los datos.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Configurar la autenticación más tarde para este usuario.",
|
||||||
|
"INVITATION": "Enviar un correo de invitación para la configuración de autenticación y verificación de correo electrónico.",
|
||||||
|
"INITIALPASSWORD": "Establece una contraseña inicial para el usuario."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Verificar número de teléfono",
|
"TITLE": "Verificar número de teléfono",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Numéro de téléphone",
|
"PHONESECTION": "Numéro de téléphone",
|
||||||
"PASSWORDSECTION": "Mot de passe initial",
|
"PASSWORDSECTION": "Mot de passe initial",
|
||||||
"ADDRESSANDPHONESECTION": "Numéro de téléphone",
|
"ADDRESSANDPHONESECTION": "Numéro de téléphone",
|
||||||
"INITMAILDESCRIPTION": "Si les deux options sont sélectionnées, aucun mail d'initialisation ne sera envoyé. Si une seule des options est sélectionnée, un mail pour fournir / vérifier les données sera envoyé."
|
"INITMAILDESCRIPTION": "Si les deux options sont sélectionnées, aucun mail d'initialisation ne sera envoyé. Si une seule des options est sélectionnée, un mail pour fournir / vérifier les données sera envoyé.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Configurer l'authentification plus tard pour cet utilisateur.",
|
||||||
|
"INVITATION": "Envoyer un e-mail d'invitation pour la configuration de l'authentification et la vérification de l'e-mail.",
|
||||||
|
"INITIALPASSWORD": "Définissez un mot de passe initial pour l'utilisateur."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Vérifier le numéro de téléphone",
|
"TITLE": "Vérifier le numéro de téléphone",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Telefonszámok",
|
"PHONESECTION": "Telefonszámok",
|
||||||
"PASSWORDSECTION": "Kezdeti jelszó",
|
"PASSWORDSECTION": "Kezdeti jelszó",
|
||||||
"ADDRESSANDPHONESECTION": "Telefonszám",
|
"ADDRESSANDPHONESECTION": "Telefonszám",
|
||||||
"INITMAILDESCRIPTION": "Ha mindkét opció ki van választva, nem kerül kiküldésre inicializáló e-mail. Ha csak az egyik opció van kiválasztva, egy e-mailt küldünk az adatok megadására / ellenőrzésére."
|
"INITMAILDESCRIPTION": "Ha mindkét opció ki van választva, nem kerül kiküldésre inicializáló e-mail. Ha csak az egyik opció van kiválasztva, egy e-mailt küldünk az adatok megadására / ellenőrzésére.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Állítsa be később az autentikációt ehhez a felhasználóhoz.",
|
||||||
|
"INVITATION": "Küldjön meghívó e-mailt az autentikáció beállításához és az e-mail hitelesítéséhez.",
|
||||||
|
"INITIALPASSWORD": "Állítson be egy kezdeti jelszót a felhasználónak."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Telefonszám ellenőrzése",
|
"TITLE": "Telefonszám ellenőrzése",
|
||||||
|
@ -729,7 +729,10 @@
|
|||||||
"PHONESECTION": "Nomor telepon",
|
"PHONESECTION": "Nomor telepon",
|
||||||
"PASSWORDSECTION": "Kata Sandi Awal",
|
"PASSWORDSECTION": "Kata Sandi Awal",
|
||||||
"ADDRESSANDPHONESECTION": "Nomor telepon",
|
"ADDRESSANDPHONESECTION": "Nomor telepon",
|
||||||
"INITMAILDESCRIPTION": "Jika kedua opsi dipilih, tidak ada email untuk inisialisasi yang akan dikirim. Jika hanya salah satu opsi yang dipilih, email untuk menyediakan/memverifikasi data akan dikirim."
|
"INITMAILDESCRIPTION": "Jika kedua opsi dipilih, tidak ada email untuk inisialisasi yang akan dikirim. Jika hanya salah satu opsi yang dipilih, email untuk menyediakan/memverifikasi data akan dikirim.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Atur autentikasi nanti untuk pengguna ini.",
|
||||||
|
"INVITATION": "Kirim Email undangan untuk pengaturan autentikasi dan verifikasi Email.",
|
||||||
|
"INITIALPASSWORD": "Tetapkan kata sandi awal untuk pengguna."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Verifikasi Nomor Telepon",
|
"TITLE": "Verifikasi Nomor Telepon",
|
||||||
|
@ -788,7 +788,10 @@
|
|||||||
"PHONESECTION": "Phone numbers",
|
"PHONESECTION": "Phone numbers",
|
||||||
"PASSWORDSECTION": "Password iniziale",
|
"PASSWORDSECTION": "Password iniziale",
|
||||||
"ADDRESSANDPHONESECTION": "Numero di telefono",
|
"ADDRESSANDPHONESECTION": "Numero di telefono",
|
||||||
"INITMAILDESCRIPTION": "Se vengono selezionate entrambe le opzioni, non verrà inviata alcuna e-mail per l'inizializzazione. Se solo una delle opzioni viene selezionata, verrà inviata una mail per fornire/verificare i dati."
|
"INITMAILDESCRIPTION": "Se vengono selezionate entrambe le opzioni, non verrà inviata alcuna e-mail per l'inizializzazione. Se solo una delle opzioni viene selezionata, verrà inviata una mail per fornire/verificare i dati.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Configura l'autenticazione più tardi per questo utente.",
|
||||||
|
"INVITATION": "Invia un'e-mail di invito per la configurazione dell'autenticazione e la verifica dell'e-mail.",
|
||||||
|
"INITIALPASSWORD": "Imposta una password iniziale per l'utente."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Verificare il numero di telefono",
|
"TITLE": "Verificare il numero di telefono",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "電話番号",
|
"PHONESECTION": "電話番号",
|
||||||
"PASSWORDSECTION": "初期パスワード",
|
"PASSWORDSECTION": "初期パスワード",
|
||||||
"ADDRESSANDPHONESECTION": "電話番号",
|
"ADDRESSANDPHONESECTION": "電話番号",
|
||||||
"INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。"
|
"INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "このユーザーの認証を後で設定します。",
|
||||||
|
"INVITATION": "認証設定とメール確認のための招待メールを送信してください。",
|
||||||
|
"INITIALPASSWORD": "ユーザーの初期パスワードを設定してください。"
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "電話番号の検証",
|
"TITLE": "電話番号の検証",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "전화번호",
|
"PHONESECTION": "전화번호",
|
||||||
"PASSWORDSECTION": "초기 비밀번호",
|
"PASSWORDSECTION": "초기 비밀번호",
|
||||||
"ADDRESSANDPHONESECTION": "전화번호",
|
"ADDRESSANDPHONESECTION": "전화번호",
|
||||||
"INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다."
|
"INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "이 사용자의 인증을 나중에 설정하세요.",
|
||||||
|
"INVITATION": "인증 설정 및 이메일 확인을 위한 초대 이메일을 보내세요.",
|
||||||
|
"INITIALPASSWORD": "사용자에 대한 초기 비밀번호를 설정하세요."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "전화번호 확인",
|
"TITLE": "전화번호 확인",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Телефонски броеви",
|
"PHONESECTION": "Телефонски броеви",
|
||||||
"PASSWORDSECTION": "Почетна лозинка",
|
"PASSWORDSECTION": "Почетна лозинка",
|
||||||
"ADDRESSANDPHONESECTION": "Телефонски број",
|
"ADDRESSANDPHONESECTION": "Телефонски број",
|
||||||
"INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците."
|
"INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Подесете автентикација подоцна за овој корисник.",
|
||||||
|
"INVITATION": "Испратете покана по е-пошта за поставување на автентикација и потврда на е-поштата.",
|
||||||
|
"INITIALPASSWORD": "Поставете почетна лозинка за корисникот."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Верификација на телефонски број",
|
"TITLE": "Верификација на телефонски број",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Telefoonnummers",
|
"PHONESECTION": "Telefoonnummers",
|
||||||
"PASSWORDSECTION": "Initieel wachtwoord",
|
"PASSWORDSECTION": "Initieel wachtwoord",
|
||||||
"ADDRESSANDPHONESECTION": "Telefoonnummer",
|
"ADDRESSANDPHONESECTION": "Telefoonnummer",
|
||||||
"INITMAILDESCRIPTION": "Als beide opties geselecteerd zijn, wordt er geen e-mail voor initialisatie verzonden. Als slechts een van de opties is geselecteerd, wordt een e-mail gestuurd om de gegevens te verstrekken / te verifiëren."
|
"INITMAILDESCRIPTION": "Als beide opties geselecteerd zijn, wordt er geen e-mail voor initialisatie verzonden. Als slechts een van de opties is geselecteerd, wordt een e-mail gestuurd om de gegevens te verstrekken / te verifiëren.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Authenticatie later instellen voor deze gebruiker.",
|
||||||
|
"INVITATION": "Stuur een uitnodigingsmail voor het instellen van authenticatie en e-mailverificatie.",
|
||||||
|
"INITIALPASSWORD": "Stel een initieel wachtwoord in voor de gebruiker."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Verifieer telefoonnummer",
|
"TITLE": "Verifieer telefoonnummer",
|
||||||
|
@ -788,7 +788,10 @@
|
|||||||
"PHONESECTION": "Numery telefonów",
|
"PHONESECTION": "Numery telefonów",
|
||||||
"PASSWORDSECTION": "Hasło początkowe",
|
"PASSWORDSECTION": "Hasło początkowe",
|
||||||
"ADDRESSANDPHONESECTION": "Numer telefonu",
|
"ADDRESSANDPHONESECTION": "Numer telefonu",
|
||||||
"INITMAILDESCRIPTION": "Jeśli zaznaczone są obie opcje, nie zostanie wysłany żaden e-mail inicjujący. Jeśli zaznaczona jest tylko jedna opcja, zostanie wysłany e-mail, aby udostępnić/zweryfikować dane."
|
"INITMAILDESCRIPTION": "Jeśli zaznaczone są obie opcje, nie zostanie wysłany żaden e-mail inicjujący. Jeśli zaznaczona jest tylko jedna opcja, zostanie wysłany e-mail, aby udostępnić/zweryfikować dane.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Skonfiguruj uwierzytelnianie później dla tego użytkownika.",
|
||||||
|
"INVITATION": "Wyślij e-mail zaproszeniowy do konfiguracji uwierzytelniania i weryfikacji e-maila.",
|
||||||
|
"INITIALPASSWORD": "Ustaw początkowe hasło dla użytkownika."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Weryfikuj numer telefonu",
|
"TITLE": "Weryfikuj numer telefonu",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Números de Telefone",
|
"PHONESECTION": "Números de Telefone",
|
||||||
"PASSWORDSECTION": "Senha Inicial",
|
"PASSWORDSECTION": "Senha Inicial",
|
||||||
"ADDRESSANDPHONESECTION": "Número de telefone",
|
"ADDRESSANDPHONESECTION": "Número de telefone",
|
||||||
"INITMAILDESCRIPTION": "Se ambas as opções forem selecionadas, nenhum e-mail de inicialização será enviado. Se apenas uma das opções for selecionada, um e-mail para fornecer/verificar os dados será enviado."
|
"INITMAILDESCRIPTION": "Se ambas as opções forem selecionadas, nenhum e-mail de inicialização será enviado. Se apenas uma das opções for selecionada, um e-mail para fornecer/verificar os dados será enviado.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Configurar autenticação mais tarde para este usuário.",
|
||||||
|
"INVITATION": "Enviar um E-mail de convite para configuração de autenticação e verificação de E-mail.",
|
||||||
|
"INITIALPASSWORD": "Defina uma senha inicial para o usuário."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Verificar Número de Telefone",
|
"TITLE": "Verificar Número de Telefone",
|
||||||
|
@ -787,7 +787,10 @@
|
|||||||
"PHONESECTION": "Numere de telefon",
|
"PHONESECTION": "Numere de telefon",
|
||||||
"PASSWORDSECTION": "Parola inițială",
|
"PASSWORDSECTION": "Parola inițială",
|
||||||
"ADDRESSANDPHONESECTION": "Număr de telefon",
|
"ADDRESSANDPHONESECTION": "Număr de telefon",
|
||||||
"INITMAILDESCRIPTION": "Dacă ambele opțiuni sunt selectate, nu va fi trimis niciun e-mail pentru inițializare. Dacă este selectată doar una dintre opțiuni, va fi trimis un e-mail pentru a furniza / verifica datele."
|
"INITMAILDESCRIPTION": "Dacă ambele opțiuni sunt selectate, nu va fi trimis niciun e-mail pentru inițializare. Dacă este selectată doar una dintre opțiuni, va fi trimis un e-mail pentru a furniza / verifica datele.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Configurați autentificarea mai târziu pentru acest utilizator.",
|
||||||
|
"INVITATION": "Trimiteți un e-mail de invitație pentru configurarea autentificării și verificarea e-mailului.",
|
||||||
|
"INITIALPASSWORD": "Setați o parolă inițială pentru utilizator."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Verificați numărul de telefon",
|
"TITLE": "Verificați numărul de telefon",
|
||||||
|
@ -796,7 +796,10 @@
|
|||||||
"PHONESECTION": "Номера телефонов",
|
"PHONESECTION": "Номера телефонов",
|
||||||
"PASSWORDSECTION": "Начальный пароль",
|
"PASSWORDSECTION": "Начальный пароль",
|
||||||
"ADDRESSANDPHONESECTION": "Номер телефона",
|
"ADDRESSANDPHONESECTION": "Номер телефона",
|
||||||
"INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных."
|
"INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Настроить аутентификацию позже для этого пользователя.",
|
||||||
|
"INVITATION": "Отправить приглашение по электронной почте для настройки аутентификации и подтверждения электронной почты.",
|
||||||
|
"INITIALPASSWORD": "Установите начальный пароль для пользователя."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Подтвердить номер телефона",
|
"TITLE": "Подтвердить номер телефона",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "Telefonnummer",
|
"PHONESECTION": "Telefonnummer",
|
||||||
"PASSWORDSECTION": "Initialt lösenord",
|
"PASSWORDSECTION": "Initialt lösenord",
|
||||||
"ADDRESSANDPHONESECTION": "Telefonnummer",
|
"ADDRESSANDPHONESECTION": "Telefonnummer",
|
||||||
"INITMAILDESCRIPTION": "Om båda alternativen är valda kommer inget e-postmeddelande för initialisering att skickas. Om endast ett av alternativen är valt kommer ett e-postmeddelande för att tillhandahålla/verifiera uppgifterna att skickas."
|
"INITMAILDESCRIPTION": "Om båda alternativen är valda kommer inget e-postmeddelande för initialisering att skickas. Om endast ett av alternativen är valt kommer ett e-postmeddelande för att tillhandahålla/verifiera uppgifterna att skickas.",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "Ställ in autentisering senare för den här användaren.",
|
||||||
|
"INVITATION": "Skicka en inbjudningsmail för autentiseringsinställning och e-postverifiering.",
|
||||||
|
"INITIALPASSWORD": "Ställ in ett initialt lösenord för användaren."
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "Verifiera telefonnummer",
|
"TITLE": "Verifiera telefonnummer",
|
||||||
|
@ -789,7 +789,10 @@
|
|||||||
"PHONESECTION": "手机号码",
|
"PHONESECTION": "手机号码",
|
||||||
"PASSWORDSECTION": "初始密码",
|
"PASSWORDSECTION": "初始密码",
|
||||||
"ADDRESSANDPHONESECTION": "手机号码",
|
"ADDRESSANDPHONESECTION": "手机号码",
|
||||||
"INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。"
|
"INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。",
|
||||||
|
"SETUPAUTHENTICATIONLATER": "稍后为此用户设置身份验证。",
|
||||||
|
"INVITATION": "发送邀请邮件以进行身份验证设置和电子邮件验证。",
|
||||||
|
"INITIALPASSWORD": "为用户设置初始密码。"
|
||||||
},
|
},
|
||||||
"CODEDIALOG": {
|
"CODEDIALOG": {
|
||||||
"TITLE": "验证手机号码",
|
"TITLE": "验证手机号码",
|
||||||
|
@ -3516,22 +3516,22 @@
|
|||||||
js-yaml "^3.10.0"
|
js-yaml "^3.10.0"
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@zitadel/client@^1.0.6":
|
"@zitadel/client@^1.0.7":
|
||||||
version "1.0.6"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.6.tgz#9fe44ff7c757e8f38fa08d25083dc036afebf5cb"
|
resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.7.tgz#39dc8d3d10bfa01e5cf56205ba188f79c39f052d"
|
||||||
integrity sha512-MG6RAApoI2Y3QGRfKByISOqGTSFsMr5YtKQYPFDAJhivYK32d7hUiMEv+WzShfGHEI38336FbKz9vg/4M961Lg==
|
integrity sha512-sZG4NEa8vQBt3+4W1AesY+5DstDBuZiqGH2EM+UqbO5D93dlDZInXqZ5oRE7RSl2Bk5ED9mbMFrB7b8DuRw72A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@bufbuild/protobuf" "^2.2.2"
|
"@bufbuild/protobuf" "^2.2.2"
|
||||||
"@connectrpc/connect" "^2.0.0"
|
"@connectrpc/connect" "^2.0.0"
|
||||||
"@connectrpc/connect-node" "^2.0.0"
|
"@connectrpc/connect-node" "^2.0.0"
|
||||||
"@connectrpc/connect-web" "^2.0.0"
|
"@connectrpc/connect-web" "^2.0.0"
|
||||||
"@zitadel/proto" "1.0.3"
|
"@zitadel/proto" "1.0.4"
|
||||||
jose "^5.3.0"
|
jose "^5.3.0"
|
||||||
|
|
||||||
"@zitadel/proto@1.0.3", "@zitadel/proto@^1.0.3":
|
"@zitadel/proto@1.0.4", "@zitadel/proto@^1.0.4":
|
||||||
version "1.0.3"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.3.tgz#28721710d9e87009adf14f90e0c8cb9bae5275ec"
|
resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.4.tgz#e2fe9895f2960643c3619191255aa2f4913ad873"
|
||||||
integrity sha512-95XPGgFgfTwU1A3oQYxTv4p+Qy/9yMO/o21VRtPBfVhPusFFCW0ddg4YoKTKpQl9FbIG7VYMLmRyuJBPuf3r+g==
|
integrity sha512-s13ZMhuOTe0b+geV+JgJud+kpYdq7TgkuCe7RIY+q4Xs5KC0FHMKfvbAk/jpFbD+TSQHiwo/TBNZlGHdwUR9Ig==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@bufbuild/protobuf" "^2.2.2"
|
"@bufbuild/protobuf" "^2.2.2"
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user