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:
Ramon 2025-03-19 13:27:59 +01:00 committed by GitHub
parent 11c9be3b8d
commit b418ea75bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 863 additions and 375 deletions

View File

@ -33,8 +33,8 @@
"@grpc/grpc-js": "^1.11.2",
"@netlify/framework-info": "^9.8.13",
"@ngx-translate/core": "^15.0.0",
"@zitadel/client": "^1.0.6",
"@zitadel/proto": "^1.0.3",
"@zitadel/client": "^1.0.7",
"@zitadel/proto": "^1.0.4",
"angular-oauth2-oidc": "^15.0.1",
"angularx-qrcode": "^16.0.0",
"buffer": "^6.0.3",

View File

@ -1,8 +1,8 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { RoleGuard } from './guards/role.guard';
import { authGuard } from './guards/auth.guard';
import { roleGuard } from './guards/role-guard';
import { UserGrantContext } from './modules/user-grants/user-grants-datasource';
import { OrgCreateComponent } from './pages/org-create/org-create.component';
@ -10,7 +10,7 @@ const routes: Routes = [
{
path: '',
loadChildren: () => import('./pages/home/home.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['.'],
},
@ -22,7 +22,7 @@ const routes: Routes = [
{
path: 'orgs/create',
component: OrgCreateComponent,
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['(org.create)?(iam.write)?'],
},
@ -31,12 +31,12 @@ const routes: Routes = [
{
path: 'orgs',
loadChildren: () => import('./pages/org-list/org-list.module'),
canActivate: [AuthGuard],
canActivate: [authGuard],
},
{
path: 'granted-projects',
loadChildren: () => import('./pages/projects/granted-projects/granted-projects.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['project.grant.read'],
},
@ -44,20 +44,20 @@ const routes: Routes = [
{
path: 'projects',
loadChildren: () => import('./pages/projects/projects.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['project.read'],
},
},
{
path: 'users',
canActivate: [AuthGuard],
canActivate: [authGuard],
loadChildren: () => import('src/app/pages/users/users.module'),
},
{
path: 'instance',
loadChildren: () => import('./pages/instance/instance.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['iam.read', 'iam.write'],
},
@ -65,7 +65,7 @@ const routes: Routes = [
{
path: 'org',
loadChildren: () => import('./pages/orgs/org.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['org.read'],
},
@ -73,7 +73,7 @@ const routes: Routes = [
{
path: 'actions',
loadChildren: () => import('./pages/actions/actions.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['org.action.read', 'org.flow.read'],
},
@ -81,7 +81,7 @@ const routes: Routes = [
{
path: 'grants',
loadChildren: () => import('./pages/grants/grants.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
context: UserGrantContext.NONE,
roles: ['user.grant.read'],
@ -89,12 +89,12 @@ const routes: Routes = [
},
{
path: 'grant-create',
canActivate: [AuthGuard],
canActivate: [authGuard],
children: [
{
path: 'project/:projectid/grant/:grantid',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
canActivate: [RoleGuard],
canActivate: [roleGuard],
data: {
roles: ['user.grant.write'],
},
@ -102,7 +102,7 @@ const routes: Routes = [
{
path: 'project/:projectid',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
canActivate: [RoleGuard],
canActivate: [roleGuard],
data: {
roles: ['user.grant.write'],
},
@ -110,7 +110,7 @@ const routes: Routes = [
{
path: 'user/:userid',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
canActivate: [RoleGuard],
canActivate: [roleGuard],
data: {
roles: ['user.grant.write'],
},
@ -118,7 +118,7 @@ const routes: Routes = [
{
path: '',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module'),
canActivate: [RoleGuard],
canActivate: [roleGuard],
data: {
roles: ['user.grant.write'],
},
@ -128,7 +128,7 @@ const routes: Routes = [
{
path: 'org-settings',
loadChildren: () => import('./pages/org-settings/org-settings.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['policy.read'],
},

View File

@ -33,9 +33,6 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import * as i18nIsoCountries from 'i18n-iso-countries';
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 { AssetService } from 'src/app/services/asset.service';
import { AppRoutingModule } from './app-routing.module';
@ -173,9 +170,6 @@ const authConfig: AuthConfig = {
ServiceWorkerModule.register('ngsw-worker.js', { enabled: false }),
],
providers: [
AuthGuard,
RoleGuard,
UserGuard,
ThemeService,
EnvironmentService,
ExhaustedService,

View File

@ -1,34 +1,26 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { inject } from '@angular/core';
import { CanActivateFn } from '@angular/router';
import { AuthConfig } from 'angular-oauth2-oidc';
import { Observable } from 'rxjs';
import { AuthenticationService } from '../services/authentication.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard {
constructor(private auth: AuthenticationService) {}
export const authGuard: CanActivateFn = (route) => {
const auth = inject(AuthenticationService);
public canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> | Promise<boolean> | Promise<any> | boolean {
if (!this.auth.authenticated) {
if (route.queryParams && route.queryParams['login_hint']) {
const hint = route.queryParams['login_hint'];
const configWithPrompt: Partial<AuthConfig> = {
customQueryParams: {
login_hint: hint,
},
};
console.log(`authenticate with login_hint: ${hint}`);
this.auth.authenticate(configWithPrompt);
} else {
return this.auth.authenticate();
}
if (!auth.authenticated) {
if (route.queryParams && route.queryParams['login_hint']) {
const hint = route.queryParams['login_hint'];
const configWithPrompt: Partial<AuthConfig> = {
customQueryParams: {
login_hint: hint,
},
};
console.log(`authenticate with login_hint: ${hint}`);
auth.authenticate(configWithPrompt).then();
} else {
return auth.authenticate();
}
return this.auth.authenticated;
}
}
return auth.authenticated;
};

View 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']);
};

View File

@ -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']);
}
}

View 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;
}),
);
};

View File

@ -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;
}),
);
}
}

View File

@ -1,4 +1,4 @@
<div class="validation-col" *ngIf="policy">
<div class="validation-col">
<div class="val" *ngIf="policy.minLength">
<i *ngIf="password?.value?.length === 0; else showSpinner" class="las la-times red"></i>
@ -15,7 +15,7 @@
diameter="20"
[color]="currentError ? 'warn' : 'valid'"
mode="determinate"
[value]="(password?.value?.length / policy.minLength) * 100"
[value]="(password?.value?.length / minLength) * 100"
>
</mat-progress-spinner>
</div>

View File

@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { PasswordComplexityPolicy } from '@zitadel/proto/zitadel/policy_pb';
@Component({
selector: 'cnsl-password-complexity-view',
@ -9,5 +9,9 @@ import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy
})
export class PasswordComplexityViewComponent {
@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);
}
}

View File

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { authGuard } from 'src/app/guards/auth.guard';
import { roleGuard } from 'src/app/guards/role-guard';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { InstanceComponent } from './instance.component';
@ -10,7 +10,7 @@ const routes: Routes = [
{
path: '',
component: InstanceComponent,
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['iam.read'],
},
@ -18,14 +18,14 @@ const routes: Routes = [
{
path: 'members',
loadChildren: () => import('./instance-members/instance-members.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['iam.member.read'],
},
},
{
path: 'provider',
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
loadChildren: () => import('src/app/modules/providers/providers.module'),
data: {
roles: ['iam.idp.read'],
@ -34,7 +34,7 @@ const routes: Routes = [
},
{
path: 'smtpprovider',
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
loadChildren: () => import('src/app/modules/smtp-provider/smtp-provider.module'),
data: {
roles: ['iam.idp.read'],

View File

@ -1,27 +1,21 @@
import { animate, style, transition, trigger } from '@angular/animations';
import { Location } from '@angular/common';
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 { Router } from '@angular/router';
import {
containsLowerCaseValidator,
containsNumberValidator,
containsSymbolValidator,
containsUpperCaseValidator,
minLengthValidator,
passwordConfirmValidator,
requiredValidator,
} from 'src/app/modules/form-field/validators/validators';
import { passwordConfirmValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
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 { AdminService } from 'src/app/services/admin.service';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.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 { 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({
selector: 'cnsl-org-create',
@ -48,20 +42,22 @@ export class OrgCreateComponent {
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
public policy?: PasswordComplexityPolicy.AsObject;
public policy?: PasswordComplexityPolicy;
public usePassword: boolean = false;
public forSelf: boolean = true;
constructor(
private router: Router,
private toast: ToastService,
private adminService: AdminService,
private _location: Location,
private fb: UntypedFormBuilder,
private mgmtService: ManagementService,
private authService: GrpcAuthService,
public langSvc: LanguagesService,
private readonly router: Router,
private readonly toast: ToastService,
private readonly adminService: AdminService,
private readonly _location: Location,
private readonly fb: UntypedFormBuilder,
private readonly mgmtService: ManagementService,
private readonly newMgmtService: NewMgmtService,
private readonly authService: GrpcAuthService,
private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService,
public readonly langSvc: LanguagesService,
breadcrumbService: BreadcrumbService,
) {
const instanceBread = new Breadcrumb({
@ -103,8 +99,8 @@ export class OrgCreateComponent {
this.adminService
.SetUpOrg(createOrgRequest, humanRequest)
.then(() => {
this.authService.revalidateOrgs();
this.router.navigate(['/orgs']);
this.authService.revalidateOrgs().then();
this.router.navigate(['/orgs']).then();
})
.catch((error) => {
this.toast.showError(error);
@ -133,36 +129,12 @@ export class OrgCreateComponent {
}
public initPwdValidators(): void {
const validators: Validators[] = [requiredValidator];
if (this.usePassword) {
this.mgmtService.getDefaultPasswordComplexityPolicy().then((data) => {
if (data.policy) {
this.policy = data.policy;
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],
});
}
this.newMgmtService.getDefaultPasswordComplexityPolicy().then((data) => {
this.pwdForm = this.fb.group({
password: ['', this.passwordComplexityValidatorFactory.buildValidators(data.policy)],
confirmPassword: ['', [requiredValidator, passwordConfirmValidator()]],
});
});
} else {
this.pwdForm = this.fb.group({
@ -194,8 +166,8 @@ export class OrgCreateComponent {
this.mgmtService
.addOrg(this.name.value)
.then(() => {
this.authService.revalidateOrgs();
this.router.navigate(['/orgs']);
this.authService.revalidateOrgs().then();
this.router.navigate(['/orgs']).then();
})
.catch((error) => {
this.toast.showError(error);

View File

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from 'src/app/guards/auth.guard';
import { RoleGuard } from 'src/app/guards/role.guard';
import { authGuard } from 'src/app/guards/auth.guard';
import { roleGuard } from 'src/app/guards/role-guard';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { OrgDetailComponent } from './org-detail/org-detail.component';
@ -13,7 +13,7 @@ const routes: Routes = [
},
{
path: 'provider',
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
loadChildren: () => import('src/app/modules/providers/providers.module'),
data: {
roles: ['org.idp.read'],

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
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';
@ -12,7 +12,7 @@ const routes: Routes = [
animation: 'HomePage',
roles: ['project.read'],
},
canActivate: [RoleGuard],
canActivate: [roleGuard],
},
];

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
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';
const routes: Routes = [
@ -10,7 +10,7 @@ const routes: Routes = [
animation: 'HomePage',
roles: ['project.read'],
},
canActivate: [RoleGuard],
canActivate: [roleGuard],
loadChildren: () => import('./owned-project-detail/owned-project-detail.module'),
},
{
@ -19,7 +19,7 @@ const routes: Routes = [
type: ProjectType.PROJECTTYPE_OWNED,
roles: ['project.member.read'],
},
canActivate: [RoleGuard],
canActivate: [roleGuard],
loadChildren: () => import('src/app/modules/project-members/project-members.module'),
},
{
@ -28,7 +28,7 @@ const routes: Routes = [
animation: 'AddPage',
roles: ['project.app.read'],
},
canActivate: [RoleGuard],
canActivate: [roleGuard],
loadChildren: () => import('src/app/pages/projects/apps/apps.module'),
},
{

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
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';
@ -13,7 +13,7 @@ const routes: Routes = [
{
path: 'create',
loadChildren: () => import('./project-create/project-create.module'),
canActivate: [RoleGuard],
canActivate: [roleGuard],
data: {
animation: 'AddPage',
roles: ['project.create'],
@ -21,7 +21,7 @@ const routes: Routes = [
},
{
path: 'app-create',
canActivate: [RoleGuard],
canActivate: [roleGuard],
data: {
animation: 'AddPage',
roles: ['project.app.write'],

View File

@ -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>

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -1,4 +1,6 @@
<cnsl-user-create-v2 *ngIf="(useV2Api$ | async) === true" />
<cnsl-create-layout
*ngIf="(useV2Api$ | async) === false"
title="{{ 'USER.CREATE.TITLE' | translate }}"
[createSteps]="1"
[currentCreateStep]="1"

View File

@ -1,9 +1,19 @@
import { Location } from '@angular/common';
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 { debounceTime, defer, of, Observable, shareReplay, forkJoin, ObservedValueOf, EMPTY, ReplaySubject } from 'rxjs';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import {
debounceTime,
defer,
of,
Observable,
shareReplay,
forkJoin,
ObservedValueOf,
EMPTY,
ReplaySubject,
TimeoutError,
} from 'rxjs';
import { Gender } from 'src/app/proto/generated/zitadel/user_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.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 { formatPhone } from 'src/app/utils/formatPhone';
import {
containsLowerCaseValidator,
containsNumberValidator,
containsSymbolValidator,
containsUpperCaseValidator,
emailValidator,
minLengthValidator,
passwordConfirmValidator,
@ -23,8 +29,12 @@ import {
} from 'src/app/modules/form-field/validators/validators';
import { LanguagesService } from 'src/app/services/languages.service';
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 { 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({
selector: 'cnsl-user-create',
@ -43,15 +53,18 @@ export class UserCreateComponent implements OnInit {
protected loading = false;
private readonly suffix$ = new ReplaySubject<HTMLSpanElement>(1);
@ViewChild('suffix') public set suffix(suffix: ElementRef<HTMLSpanElement>) {
this.suffix$.next(suffix.nativeElement);
@ViewChild('suffix') public set suffix(suffix: ElementRef<HTMLSpanElement> | undefined) {
if (suffix?.nativeElement) {
this.suffix$.next(suffix.nativeElement);
}
}
protected usePassword: boolean = false;
protected readonly envSuffix$: Observable<string>;
protected readonly userForm: ReturnType<typeof this.buildUserForm>;
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>;
constructor(
@ -59,24 +72,24 @@ export class UserCreateComponent implements OnInit {
private readonly toast: ToastService,
private readonly fb: FormBuilder,
private readonly mgmtService: ManagementService,
private readonly newMgmtService: NewMgmtService,
private readonly destroyRef: DestroyRef,
private readonly breadcrumbService: BreadcrumbService,
protected readonly location: Location,
protected readonly langSvc: LanguagesService,
private readonly featureService: NewFeatureService,
private readonly passwordComplexityValidatorFactory: PasswordComplexityValidatorFactoryService,
countryCallingCodesService: CountryCallingCodesService,
) {
this.envSuffix$ = this.getEnvSuffix();
this.suffixPadding$ = this.getSuffixPadding();
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.pwdForm$ = this.buildPwdForm(this.passwordComplexityPolicy$);
this.countryPhoneCodes = countryCallingCodesService.getCountryCallingCodes();
}
ngOnInit(): void {
this.watchPhoneChanges();
this.breadcrumbService.setBreadcrumb([
new Breadcrumb({
@ -86,6 +99,10 @@ export class UserCreateComponent implements OnInit {
]);
}
ngOnInit(): void {
this.watchPhoneChanges();
}
private getEnvSuffix() {
const domainPolicy$ = defer(() => this.mgmtService.getDomainPolicy());
const orgDomains$ = defer(() => this.mgmtService.listOrgDomains());
@ -112,7 +129,7 @@ export class UserCreateComponent implements OnInit {
}
private getPasswordComplexityPolicy() {
return defer(() => this.mgmtService.getPasswordComplexityPolicy()).pipe(
return defer(() => this.newMgmtService.getPasswordComplexityPolicy()).pipe(
map(({ policy }) => policy),
catchError((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() {
return this.fb.group({
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(
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({
password: new FormControl('', { nonNullable: true, validators }),
password: new FormControl('', {
nonNullable: true,
validators: this.passwordComplexityValidatorFactory.buildValidators(policy),
}),
confirmPassword: new FormControl('', {
nonNullable: true,
validators: [requiredValidator, passwordConfirmValidator()],

View File

@ -19,9 +19,11 @@ import { PasswordComplexityViewModule } from 'src/app/modules/password-complexit
import { CountryCallingCodesService } from 'src/app/services/country-calling-codes.service';
import { UserCreateRoutingModule } from './user-create-routing.module';
import { UserCreateComponent } from './user-create.component';
import { UserCreateV2Component } from './user-create-v2/user-create-v2.component';
import { MatRadioModule } from '@angular/material/radio';
@NgModule({
declarations: [UserCreateComponent],
declarations: [UserCreateComponent, UserCreateV2Component],
providers: [CountryCallingCodesService],
imports: [
UserCreateRoutingModule,
@ -42,6 +44,7 @@ import { UserCreateComponent } from './user-create.component';
DetailLayoutModule,
InputModule,
MatRippleModule,
MatRadioModule,
],
})
export default class UserCreateModule {}

View File

@ -192,7 +192,7 @@ export class AuthUserDetailComponent implements OnInit {
}
private getMyUser(): Observable<UserQuery> {
return defer(() => this.userService.getMyUser()).pipe(
return this.userService.user$.pipe(
map((user) => ({ state: 'success' as const, value: user })),
catchError((error) => of({ state: 'error', error } as const)),
startWith({ state: 'loading' } as const),

View File

@ -1,8 +1,7 @@
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 {
take,
map,
switchMap,
firstValueFrom,
@ -12,24 +11,18 @@ import {
of,
shareReplay,
combineLatestWith,
EMPTY,
} from 'rxjs';
import {
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 { passwordConfirmValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
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 { catchError, filter } from 'rxjs/operators';
import { User } from 'src/app/proto/generated/zitadel/user_pb';
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({
selector: 'cnsl-password',
@ -41,34 +34,53 @@ export class PasswordComponent implements OnInit {
protected readonly username$: Observable<string>;
protected readonly id$: Observable<string | undefined>;
protected readonly form$: Observable<UntypedFormGroup>;
protected readonly passwordPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>;
protected readonly user$: Observable<User.AsObject>;
protected readonly passwordPolicy$: Observable<PasswordComplexityPolicy | undefined>;
protected readonly user$: Observable<User>;
constructor(
activatedRoute: ActivatedRoute,
private readonly activatedRoute: ActivatedRoute,
private readonly fb: UntypedFormBuilder,
private readonly authService: GrpcAuthService,
private readonly userService: UserService,
private readonly newAuthService: NewAuthService,
private readonly toast: ToastService,
private readonly breadcrumbService: BreadcrumbService,
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.user$ = this.authService.user.pipe(take(1), filter(Boolean));
this.username$ = usernameParam$.pipe(mergeWith(this.user$.pipe(map((user) => user.preferredLoginName))));
this.user$ = this.getUser().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.username$ = this.getUsername(this.user$);
this.breadcrumb$ = this.getBreadcrumb$(this.id$, this.user$);
this.passwordPolicy$ = this.getPasswordPolicy$().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
const validators$ = this.getValidators$(this.passwordPolicy$);
this.form$ = this.getForm$(this.id$, validators$);
this.form$ = this.getForm$(this.id$, this.passwordPolicy$);
}
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(
switchMap(async (id) => {
if (id) {
@ -86,7 +98,7 @@ export class PasswordComponent implements OnInit {
return [
new Breadcrumb({
type: BreadcrumbType.AUTHUSER,
name: user.human?.profile?.displayName,
name: (user.type.case === 'human' && user.type.value.profile?.displayName) || undefined,
routerLink: ['/users', 'me'],
}),
];
@ -94,39 +106,22 @@ export class PasswordComponent implements OnInit {
);
}
private getValidators$(
passwordPolicy$: Observable<PasswordComplexityPolicy.AsObject | undefined>,
): Observable<Validators[]> {
return passwordPolicy$.pipe(
map((policy) => {
const validators: Validators[] = [requiredValidator];
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 getPasswordPolicy$(): Observable<PasswordComplexityPolicy | undefined> {
return defer(() => this.newAuthService.getMyPasswordComplexityPolicy()).pipe(
map((resp) => resp.policy),
catchError((err) => {
this.toast.showError(err);
return of(undefined);
}),
);
}
private getForm$(
id$: Observable<string | undefined>,
validators$: Observable<Validators[]>,
policy$: Observable<PasswordComplexityPolicy | undefined>,
): Observable<UntypedFormGroup> {
const validators$ = policy$.pipe(map((policy) => this.passwordComplexityValidatorFactory.buildValidators(policy)));
return id$.pipe(
combineLatestWith(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> {
const password = this.password(form)?.value;
@ -182,7 +164,7 @@ export class PasswordComponent implements OnInit {
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 newPassword = this.newPassword(form);
@ -192,7 +174,7 @@ export class PasswordComponent implements OnInit {
try {
await this.userService.setPassword({
userId: user.id,
userId: user.userId,
newPassword: {
password: newPassword.value,
changeRequired: false,

View File

@ -159,7 +159,7 @@ export class UserTableComponent implements OnInit {
}
private getMyUser() {
return defer(() => this.userService.getMyUser()).pipe(
return this.userService.user$.pipe(
catchError((error) => {
this.toast.showError(error);
return EMPTY;

View File

@ -1,8 +1,8 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
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 { authGuard } from 'src/app/guards/auth.guard';
import { roleGuard } from 'src/app/guards/role-guard';
import { userGuard } from 'src/app/guards/user-guard';
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
import { AuthUserDetailComponent } from './user-detail/auth-user-detail/auth-user-detail.component';
@ -22,7 +22,7 @@ const routes: Routes = [
{
path: 'create',
loadChildren: () => import('./user-create/user-create.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['user.write'],
},
@ -30,7 +30,7 @@ const routes: Routes = [
{
path: 'create-machine',
loadChildren: () => import('./user-create-machine/user-create-machine.module'),
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['user.write'],
},
@ -38,7 +38,7 @@ const routes: Routes = [
{
path: 'me',
component: AuthUserDetailComponent,
canActivate: [AuthGuard],
canActivate: [authGuard],
data: {
animation: 'HomePage',
},
@ -46,13 +46,13 @@ const routes: Routes = [
{
path: 'me/password',
component: PasswordComponent,
canActivate: [AuthGuard],
canActivate: [authGuard],
data: { animation: 'AddPage' },
},
{
path: ':id',
component: UserDetailComponent,
canActivate: [AuthGuard, UserGuard, RoleGuard],
canActivate: [authGuard, userGuard, roleGuard],
data: {
roles: ['user.read'],
animation: 'HomePage',
@ -61,7 +61,7 @@ const routes: Routes = [
{
path: ':id/password',
component: PasswordComponent,
canActivate: [AuthGuard, RoleGuard],
canActivate: [authGuard, roleGuard],
data: {
roles: ['user.write'],
animation: 'AddPage',

View File

@ -18,7 +18,7 @@ import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } f
import { StorageService } from './storage.service';
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
//@ts-ignore
import { createUserServiceClient } from '@zitadel/client/v2';
import { createFeatureServiceClient, createUserServiceClient } from '@zitadel/client/v2';
//@ts-ignore
import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1';
import { createGrpcWebTransport } from '@connectrpc/connect-web';
@ -36,6 +36,7 @@ export class GrpcService {
public userNew!: ReturnType<typeof createUserServiceClient>;
public mgmtNew!: ReturnType<typeof createManagementServiceClient>;
public authNew!: ReturnType<typeof createAuthServiceClient>;
public featureNew!: ReturnType<typeof createFeatureServiceClient>;
constructor(
private readonly envService: EnvironmentService,
@ -114,6 +115,7 @@ export class GrpcService {
this.userNew = createUserServiceClient(transport);
this.mgmtNew = createManagementServiceClient(transportOldAPIs);
this.authNew = createAuthServiceClient(transport);
this.featureNew = createFeatureServiceClient(transport);
const authConfig: AuthConfig = {
scope: 'openid profile email',

View File

@ -17,15 +17,15 @@ const accessTokenStorageKey = 'access_token';
@Injectable({ providedIn: 'root' })
export class AuthInterceptorProvider {
public triggerDialog: Subject<boolean> = new Subject();
private readonly triggerDialog: Subject<boolean> = new Subject();
constructor(
private authenticationService: AuthenticationService,
private storageService: StorageService,
private dialog: MatDialog,
private destroyRef: DestroyRef,
private readonly authenticationService: AuthenticationService,
private readonly storageService: StorageService,
private readonly dialog: MatDialog,
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> {

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
AddMyAuthFactorOTPSMSResponse,
GetMyPasswordComplexityPolicyResponse,
GetMyUserResponse,
ListMyMetadataResponse,
VerifyMyPhoneResponse,
@ -28,4 +29,8 @@ export class NewAuthService {
public listMyMetadata(): Promise<ListMyMetadataResponse> {
return this.grpcService.authNew.listMyMetadata({});
}
public getMyPasswordComplexityPolicy(): Promise<GetMyPasswordComplexityPolicyResponse> {
return this.grpcService.authNew.getMyPasswordComplexityPolicy({});
}
}

View 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({});
}
}

View File

@ -3,8 +3,10 @@ import { GrpcService } from './grpc.service';
import {
GenerateMachineSecretRequestSchema,
GenerateMachineSecretResponse,
GetDefaultPasswordComplexityPolicyResponse,
GetLoginPolicyRequestSchema,
GetLoginPolicyResponse,
GetPasswordComplexityPolicyResponse,
ListUserMetadataRequestSchema,
ListUserMetadataResponse,
RemoveMachineSecretRequestSchema,
@ -89,4 +91,12 @@ export class NewMgmtService {
): Promise<RemoveUserMetadataResponse> {
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({});
}
}

View File

@ -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;
}
}

View File

@ -1,4 +1,4 @@
import { DestroyRef, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
AddHumanUserRequestSchema,
@ -70,57 +70,65 @@ import { ObjectDetails } from '../proto/generated/zitadel/object_pb';
import { Timestamp } from '../proto/generated/google/protobuf/timestamp_pb';
import { HumanPhone, HumanPhoneSchema } from '@zitadel/proto/zitadel/user/v2/phone_pb';
import { OAuthService } from 'angular-oauth2-oidc';
import { firstValueFrom, Observable, shareReplay } from 'rxjs';
import { filter, map, startWith, tap, timeout } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { debounceTime, EMPTY, Observable, of, ReplaySubject, shareReplay, switchAll, switchMap } from 'rxjs';
import { catchError, filter, map, startWith } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class UserService {
private readonly userId$: Observable<string>;
private user: UserV2 | undefined;
private user$$ = new ReplaySubject<Observable<UserV2>>(1);
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(
private readonly grpcService: GrpcService,
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() {
return this.oauthService.events.pipe(
filter((event) => event.type === 'token_received'),
startWith(this.oauthService.getIdToken),
map(() => this.oauthService.getIdToken()),
startWith(this.oauthService.getIdToken()),
filter(Boolean),
// split jwt and get base64 encoded payload
map((token) => token.split('.')[1]),
// decode payload
map(atob),
// parse payload
map((payload) => JSON.parse(payload)),
map((payload: unknown) => {
// check if sub is in payload and is a string
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
return payload.sub;
switchMap((token) => {
// we do this in a try catch so the observable will retry this logic if it fails
try {
// split jwt and get base64 encoded payload
const unparsedPayload = atob(token.split('.')[1]);
// parse payload
const payload: unknown = JSON.parse(unparsedPayload);
// check if sub is in payload and is a string
if (payload && typeof payload === 'object' && 'sub' in payload && typeof payload.sub === 'string') {
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> {
return this.grpcService.userNew.addHumanUser(create(AddHumanUserRequestSchema, req));
}
@ -129,20 +137,6 @@ export class UserService {
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> {
return this.grpcService.userNew.getUserByID({ userId });
}

View File

@ -788,7 +788,10 @@
"PHONESECTION": "Телефонни номера",
"PASSWORDSECTION": "Първоначална парола",
"ADDRESSANDPHONESECTION": "Телефонен номер",
"INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. "
"INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. ",
"SETUPAUTHENTICATIONLATER": "Настройте удостоверяване по-късно за този потребител.",
"INVITATION": "Изпратете покана по имейл за настройка на удостоверяване и потвърждение на имейл.",
"INITIALPASSWORD": "Задайте начална парола за потребителя."
},
"CODEDIALOG": {
"TITLE": "Потвърдете телефонния номер",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Telefonní čísla",
"PASSWORDSECTION": "Prvotní heslo",
"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": {
"TITLE": "Ověření telefonního čísla",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Telefonnummer",
"PASSWORDSECTION": "Setze ein initiales Passwort.",
"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": {
"TITLE": "Telefonnummer verifizieren",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Phone numbers",
"PASSWORDSECTION": "Initial Password",
"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": {
"TITLE": "Verify Phone Number",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Números de teléfono",
"PASSWORDSECTION": "Contraseña inicial",
"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": {
"TITLE": "Verificar número de teléfono",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Numéro de téléphone",
"PASSWORDSECTION": "Mot de passe initial",
"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": {
"TITLE": "Vérifier le numéro de téléphone",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Telefonszámok",
"PASSWORDSECTION": "Kezdeti jelszó",
"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": {
"TITLE": "Telefonszám ellenőrzése",

View File

@ -729,7 +729,10 @@
"PHONESECTION": "Nomor telepon",
"PASSWORDSECTION": "Kata Sandi Awal",
"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": {
"TITLE": "Verifikasi Nomor Telepon",

View File

@ -788,7 +788,10 @@
"PHONESECTION": "Phone numbers",
"PASSWORDSECTION": "Password iniziale",
"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": {
"TITLE": "Verificare il numero di telefono",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "電話番号",
"PASSWORDSECTION": "初期パスワード",
"ADDRESSANDPHONESECTION": "電話番号",
"INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。"
"INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。",
"SETUPAUTHENTICATIONLATER": "このユーザーの認証を後で設定します。",
"INVITATION": "認証設定とメール確認のための招待メールを送信してください。",
"INITIALPASSWORD": "ユーザーの初期パスワードを設定してください。"
},
"CODEDIALOG": {
"TITLE": "電話番号の検証",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "전화번호",
"PASSWORDSECTION": "초기 비밀번호",
"ADDRESSANDPHONESECTION": "전화번호",
"INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다."
"INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다.",
"SETUPAUTHENTICATIONLATER": "이 사용자의 인증을 나중에 설정하세요.",
"INVITATION": "인증 설정 및 이메일 확인을 위한 초대 이메일을 보내세요.",
"INITIALPASSWORD": "사용자에 대한 초기 비밀번호를 설정하세요."
},
"CODEDIALOG": {
"TITLE": "전화번호 확인",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Телефонски броеви",
"PASSWORDSECTION": "Почетна лозинка",
"ADDRESSANDPHONESECTION": "Телефонски број",
"INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците."
"INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците.",
"SETUPAUTHENTICATIONLATER": "Подесете автентикација подоцна за овој корисник.",
"INVITATION": "Испратете покана по е-пошта за поставување на автентикација и потврда на е-поштата.",
"INITIALPASSWORD": "Поставете почетна лозинка за корисникот."
},
"CODEDIALOG": {
"TITLE": "Верификација на телефонски број",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Telefoonnummers",
"PASSWORDSECTION": "Initieel wachtwoord",
"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": {
"TITLE": "Verifieer telefoonnummer",

View File

@ -788,7 +788,10 @@
"PHONESECTION": "Numery telefonów",
"PASSWORDSECTION": "Hasło początkowe",
"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": {
"TITLE": "Weryfikuj numer telefonu",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Números de Telefone",
"PASSWORDSECTION": "Senha Inicial",
"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": {
"TITLE": "Verificar Número de Telefone",

View File

@ -787,7 +787,10 @@
"PHONESECTION": "Numere de telefon",
"PASSWORDSECTION": "Parola inițială",
"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": {
"TITLE": "Verificați numărul de telefon",

View File

@ -796,7 +796,10 @@
"PHONESECTION": "Номера телефонов",
"PASSWORDSECTION": "Начальный пароль",
"ADDRESSANDPHONESECTION": "Номер телефона",
"INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных."
"INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных.",
"SETUPAUTHENTICATIONLATER": "Настроить аутентификацию позже для этого пользователя.",
"INVITATION": "Отправить приглашение по электронной почте для настройки аутентификации и подтверждения электронной почты.",
"INITIALPASSWORD": "Установите начальный пароль для пользователя."
},
"CODEDIALOG": {
"TITLE": "Подтвердить номер телефона",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "Telefonnummer",
"PASSWORDSECTION": "Initialt lösenord",
"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": {
"TITLE": "Verifiera telefonnummer",

View File

@ -789,7 +789,10 @@
"PHONESECTION": "手机号码",
"PASSWORDSECTION": "初始密码",
"ADDRESSANDPHONESECTION": "手机号码",
"INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。"
"INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。",
"SETUPAUTHENTICATIONLATER": "稍后为此用户设置身份验证。",
"INVITATION": "发送邀请邮件以进行身份验证设置和电子邮件验证。",
"INITIALPASSWORD": "为用户设置初始密码。"
},
"CODEDIALOG": {
"TITLE": "验证手机号码",

View File

@ -3516,22 +3516,22 @@
js-yaml "^3.10.0"
tslib "^2.4.0"
"@zitadel/client@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.6.tgz#9fe44ff7c757e8f38fa08d25083dc036afebf5cb"
integrity sha512-MG6RAApoI2Y3QGRfKByISOqGTSFsMr5YtKQYPFDAJhivYK32d7hUiMEv+WzShfGHEI38336FbKz9vg/4M961Lg==
"@zitadel/client@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.7.tgz#39dc8d3d10bfa01e5cf56205ba188f79c39f052d"
integrity sha512-sZG4NEa8vQBt3+4W1AesY+5DstDBuZiqGH2EM+UqbO5D93dlDZInXqZ5oRE7RSl2Bk5ED9mbMFrB7b8DuRw72A==
dependencies:
"@bufbuild/protobuf" "^2.2.2"
"@connectrpc/connect" "^2.0.0"
"@connectrpc/connect-node" "^2.0.0"
"@connectrpc/connect-web" "^2.0.0"
"@zitadel/proto" "1.0.3"
"@zitadel/proto" "1.0.4"
jose "^5.3.0"
"@zitadel/proto@1.0.3", "@zitadel/proto@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.3.tgz#28721710d9e87009adf14f90e0c8cb9bae5275ec"
integrity sha512-95XPGgFgfTwU1A3oQYxTv4p+Qy/9yMO/o21VRtPBfVhPusFFCW0ddg4YoKTKpQl9FbIG7VYMLmRyuJBPuf3r+g==
"@zitadel/proto@1.0.4", "@zitadel/proto@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.4.tgz#e2fe9895f2960643c3619191255aa2f4913ad873"
integrity sha512-s13ZMhuOTe0b+geV+JgJud+kpYdq7TgkuCe7RIY+q4Xs5KC0FHMKfvbAk/jpFbD+TSQHiwo/TBNZlGHdwUR9Ig==
dependencies:
"@bufbuild/protobuf" "^2.2.2"