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