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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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'],

View File

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

View File

@ -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'],

View File

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

View File

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

View File

@ -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'],

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

View File

@ -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()],

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 { 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 {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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