diff --git a/console/package.json b/console/package.json index 2c1d38da1b..dab4104385 100644 --- a/console/package.json +++ b/console/package.json @@ -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", diff --git a/console/src/app/app-routing.module.ts b/console/src/app/app-routing.module.ts index 4900c8e424..582f65d8af 100644 --- a/console/src/app/app-routing.module.ts +++ b/console/src/app/app-routing.module.ts @@ -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'], }, diff --git a/console/src/app/app.module.ts b/console/src/app/app.module.ts index 34f9272f3a..d6e7e60bea 100644 --- a/console/src/app/app.module.ts +++ b/console/src/app/app.module.ts @@ -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, diff --git a/console/src/app/guards/auth.guard.ts b/console/src/app/guards/auth.guard.ts index ca996c3312..8f1ebaabde 100644 --- a/console/src/app/guards/auth.guard.ts +++ b/console/src/app/guards/auth.guard.ts @@ -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 | Promise | Promise | boolean { - if (!this.auth.authenticated) { - if (route.queryParams && route.queryParams['login_hint']) { - const hint = route.queryParams['login_hint']; - const configWithPrompt: Partial = { - 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 = { + 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; +}; diff --git a/console/src/app/guards/role-guard.ts b/console/src/app/guards/role-guard.ts new file mode 100644 index 0000000000..887b9e8802 --- /dev/null +++ b/console/src/app/guards/role-guard.ts @@ -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']); +}; diff --git a/console/src/app/guards/role.guard.ts b/console/src/app/guards/role.guard.ts deleted file mode 100644 index 951ffa1b60..0000000000 --- a/console/src/app/guards/role.guard.ts +++ /dev/null @@ -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 { - return this.authService.isAllowed(route.data['roles'], route.data['requiresAll']); - } -} diff --git a/console/src/app/guards/user-guard.ts b/console/src/app/guards/user-guard.ts new file mode 100644 index 0000000000..fc97cf5a2e --- /dev/null +++ b/console/src/app/guards/user-guard.ts @@ -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; + }), + ); +}; diff --git a/console/src/app/guards/user.guard.ts b/console/src/app/guards/user.guard.ts deleted file mode 100644 index b4527753fe..0000000000 --- a/console/src/app/guards/user.guard.ts +++ /dev/null @@ -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 | Promise | 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; - }), - ); - } -} diff --git a/console/src/app/modules/password-complexity-view/password-complexity-view.component.html b/console/src/app/modules/password-complexity-view/password-complexity-view.component.html index 0a5649dabb..169ce8ef1d 100644 --- a/console/src/app/modules/password-complexity-view/password-complexity-view.component.html +++ b/console/src/app/modules/password-complexity-view/password-complexity-view.component.html @@ -1,4 +1,4 @@ -
+
@@ -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" >
diff --git a/console/src/app/modules/password-complexity-view/password-complexity-view.component.ts b/console/src/app/modules/password-complexity-view/password-complexity-view.component.ts index db6ecab89a..2e70a8744c 100644 --- a/console/src/app/modules/password-complexity-view/password-complexity-view.component.ts +++ b/console/src/app/modules/password-complexity-view/password-complexity-view.component.ts @@ -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); + } } diff --git a/console/src/app/pages/instance/instance-routing.module.ts b/console/src/app/pages/instance/instance-routing.module.ts index ca03086e85..db790417b0 100644 --- a/console/src/app/pages/instance/instance-routing.module.ts +++ b/console/src/app/pages/instance/instance-routing.module.ts @@ -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'], diff --git a/console/src/app/pages/org-create/org-create.component.ts b/console/src/app/pages/org-create/org-create.component.ts index a82e04d2b5..fc1fc65ee8 100644 --- a/console/src/app/pages/org-create/org-create.component.ts +++ b/console/src/app/pages/org-create/org-create.component.ts @@ -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); diff --git a/console/src/app/pages/orgs/org-routing.module.ts b/console/src/app/pages/orgs/org-routing.module.ts index 0f79b185e7..1ce6b0049d 100644 --- a/console/src/app/pages/orgs/org-routing.module.ts +++ b/console/src/app/pages/orgs/org-routing.module.ts @@ -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'], diff --git a/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail-routing.module.ts b/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail-routing.module.ts index c6bab59c81..3c1f7dd6e6 100644 --- a/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail-routing.module.ts +++ b/console/src/app/pages/projects/owned-projects/owned-project-detail/owned-project-detail-routing.module.ts @@ -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], }, ]; diff --git a/console/src/app/pages/projects/owned-projects/owned-projects-routing.module.ts b/console/src/app/pages/projects/owned-projects/owned-projects-routing.module.ts index 53481ea3ee..10285cbbf8 100644 --- a/console/src/app/pages/projects/owned-projects/owned-projects-routing.module.ts +++ b/console/src/app/pages/projects/owned-projects/owned-projects-routing.module.ts @@ -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'), }, { diff --git a/console/src/app/pages/projects/projects-routing.module.ts b/console/src/app/pages/projects/projects-routing.module.ts index 057c48e5a5..1efdf6147e 100644 --- a/console/src/app/pages/projects/projects-routing.module.ts +++ b/console/src/app/pages/projects/projects-routing.module.ts @@ -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'], diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.html b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.html new file mode 100644 index 0000000000..cb2857eaf3 --- /dev/null +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.html @@ -0,0 +1,99 @@ + +
+ +
+ +
+ + {{ 'USER.LOGINMETHODS.EMAIL.ISVERIFIED' | translate }} + +
+ + {{ 'USER.PROFILE.FIRSTNAME' | translate }} + + + + {{ 'USER.PROFILE.LASTNAME' | translate }} + + + + {{ 'USER.PROFILE.NICKNAME' | translate }} + + + + {{ 'USER.PROFILE.USERNAME' | translate }} + + + +
+ + {{ 'USER.CREATE.SETUPAUTHENTICATIONLATER' | translate }} + {{ 'USER.CREATE.INVITATION' | translate }} + {{ 'USER.CREATE.INITIALPASSWORD' | translate }} + + + + {{ 'USER.PASSWORD.NEWINITIAL' | translate }} + + + + {{ 'USER.PASSWORD.CONFIRMINITIAL' | translate }} + + + + + +
+ +
+ +
+ +
+
diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss new file mode 100644 index 0000000000..1603cee1f4 --- /dev/null +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.scss @@ -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; +} diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts new file mode 100644 index 0000000000..0ff56a59c6 --- /dev/null +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts @@ -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; +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; + + private readonly passwordComplexityPolicy$: Observable; + protected readonly authenticationFactor$: Observable; + private readonly useLoginV2$: Observable; + + 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, { + 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, + ): Observable { + 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 = { + 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); + } + } +} diff --git a/console/src/app/pages/users/user-create/user-create.component.html b/console/src/app/pages/users/user-create/user-create.component.html index 13fcb95bb3..1b65cc992e 100644 --- a/console/src/app/pages/users/user-create/user-create.component.html +++ b/console/src/app/pages/users/user-create/user-create.component.html @@ -1,4 +1,6 @@ + (1); - @ViewChild('suffix') public set suffix(suffix: ElementRef) { - this.suffix$.next(suffix.nativeElement); + @ViewChild('suffix') public set suffix(suffix: ElementRef | undefined) { + if (suffix?.nativeElement) { + this.suffix$.next(suffix.nativeElement); + } } protected usePassword: boolean = false; protected readonly envSuffix$: Observable; protected readonly userForm: ReturnType; protected readonly pwdForm$: ReturnType; - protected readonly passwordComplexityPolicy$: Observable; + protected readonly passwordComplexityPolicy$: Observable; + protected readonly useV2Api$: Observable; protected readonly suffixPadding$: Observable; 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) { + private buildPwdForm(passwordComplexityPolicy$: Observable) { 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()], diff --git a/console/src/app/pages/users/user-create/user-create.module.ts b/console/src/app/pages/users/user-create/user-create.module.ts index a5483f1802..5bab56f861 100644 --- a/console/src/app/pages/users/user-create/user-create.module.ts +++ b/console/src/app/pages/users/user-create/user-create.module.ts @@ -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 {} diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts index 2752dd9265..2bc2ca67f1 100644 --- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-detail.component.ts @@ -192,7 +192,7 @@ export class AuthUserDetailComponent implements OnInit { } private getMyUser(): Observable { - 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), diff --git a/console/src/app/pages/users/user-detail/password/password.component.ts b/console/src/app/pages/users/user-detail/password/password.component.ts index b37d79c39b..ef18559e09 100644 --- a/console/src/app/pages/users/user-detail/password/password.component.ts +++ b/console/src/app/pages/users/user-detail/password/password.component.ts @@ -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; protected readonly id$: Observable; protected readonly form$: Observable; - protected readonly passwordPolicy$: Observable; - protected readonly user$: Observable; + protected readonly passwordPolicy$: Observable; + protected readonly user$: Observable; 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, user$: Observable): Observable { + 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) { + 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, user$: Observable): Observable { 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, - ): Observable { - 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 { + return defer(() => this.newAuthService.getMyPasswordComplexityPolicy()).pipe( + map((resp) => resp.policy), + catchError((err) => { + this.toast.showError(err); + return of(undefined); }), ); } private getForm$( id$: Observable, - validators$: Observable, + policy$: Observable, ): Observable { + 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 { - 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 { 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 { + public async setPassword(form: UntypedFormGroup, user: User): Promise { 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, diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.ts b/console/src/app/pages/users/user-list/user-table/user-table.component.ts index 79481de63d..29de192ec2 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.ts +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.ts @@ -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; diff --git a/console/src/app/pages/users/users-routing.module.ts b/console/src/app/pages/users/users-routing.module.ts index 240d32638e..73551a8cc1 100644 --- a/console/src/app/pages/users/users-routing.module.ts +++ b/console/src/app/pages/users/users-routing.module.ts @@ -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', diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index 52332eae15..66469b93ee 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -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; public mgmtNew!: ReturnType; public authNew!: ReturnType; + public featureNew!: ReturnType; 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', diff --git a/console/src/app/services/interceptors/auth.interceptor.ts b/console/src/app/services/interceptors/auth.interceptor.ts index 72c1670bce..65b7cbc4bf 100644 --- a/console/src/app/services/interceptors/auth.interceptor.ts +++ b/console/src/app/services/interceptors/auth.interceptor.ts @@ -17,15 +17,15 @@ const accessTokenStorageKey = 'access_token'; @Injectable({ providedIn: 'root' }) export class AuthInterceptorProvider { - public triggerDialog: Subject = new Subject(); + private readonly triggerDialog: Subject = 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 { diff --git a/console/src/app/services/new-auth.service.ts b/console/src/app/services/new-auth.service.ts index 34b12bb90f..b6b2260dad 100644 --- a/console/src/app/services/new-auth.service.ts +++ b/console/src/app/services/new-auth.service.ts @@ -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 { return this.grpcService.authNew.listMyMetadata({}); } + + public getMyPasswordComplexityPolicy(): Promise { + return this.grpcService.authNew.getMyPasswordComplexityPolicy({}); + } } diff --git a/console/src/app/services/new-feature.service.ts b/console/src/app/services/new-feature.service.ts new file mode 100644 index 0000000000..e525e17021 --- /dev/null +++ b/console/src/app/services/new-feature.service.ts @@ -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 { + return this.grpcService.featureNew.getInstanceFeatures({}); + } +} diff --git a/console/src/app/services/new-mgmt.service.ts b/console/src/app/services/new-mgmt.service.ts index 21019f8c73..6798d25f41 100644 --- a/console/src/app/services/new-mgmt.service.ts +++ b/console/src/app/services/new-mgmt.service.ts @@ -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 { return this.grpcService.mgmtNew.removeUserMetadata(create(RemoveUserMetadataRequestSchema, req)); } + + public getPasswordComplexityPolicy(): Promise { + return this.grpcService.mgmtNew.getPasswordComplexityPolicy({}); + } + + public getDefaultPasswordComplexityPolicy(): Promise { + return this.grpcService.mgmtNew.getDefaultPasswordComplexityPolicy({}); + } } diff --git a/console/src/app/services/password-complexity-validator-factory.service.ts b/console/src/app/services/password-complexity-validator-factory.service.ts new file mode 100644 index 0000000000..82c6d5eb58 --- /dev/null +++ b/console/src/app/services/password-complexity-validator-factory.service.ts @@ -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; + } +} diff --git a/console/src/app/services/user.service.ts b/console/src/app/services/user.service.ts index d0388571d3..a5bbd0aaff 100644 --- a/console/src/app/services/user.service.ts +++ b/console/src/app/services/user.service.ts @@ -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; - private user: UserV2 | undefined; + private user$$ = new ReplaySubject>(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): Promise { 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 { - 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 { return this.grpcService.userNew.getUserByID({ userId }); } diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index 1ff5a5ffc1..152797b757 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -788,7 +788,10 @@ "PHONESECTION": "Телефонни номера", "PASSWORDSECTION": "Първоначална парола", "ADDRESSANDPHONESECTION": "Телефонен номер", - "INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. " + "INITMAILDESCRIPTION": "Ако са избрани и двете опции, няма да бъде изпратен имейл за инициализация. ", + "SETUPAUTHENTICATIONLATER": "Настройте удостоверяване по-късно за този потребител.", + "INVITATION": "Изпратете покана по имейл за настройка на удостоверяване и потвърждение на имейл.", + "INITIALPASSWORD": "Задайте начална парола за потребителя." }, "CODEDIALOG": { "TITLE": "Потвърдете телефонния номер", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 6fa8ccd00e..c1848192ce 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -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", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 2b2cbb98d2..bf22866672 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -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", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 3158018199..deeabd09ba 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -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", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index 96a9385ebb..b698ec4b16 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -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", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index 01f2fa2380..e779e66858 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -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", diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json index 28b5e8efcb..127ccdc6ed 100644 --- a/console/src/assets/i18n/hu.json +++ b/console/src/assets/i18n/hu.json @@ -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", diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index e21920dfcf..18340173d5 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -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", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 9213967895..2518ae1e78 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -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", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 6c1507b4ae..ced8cf646c 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -789,7 +789,10 @@ "PHONESECTION": "電話番号", "PASSWORDSECTION": "初期パスワード", "ADDRESSANDPHONESECTION": "電話番号", - "INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。" + "INITMAILDESCRIPTION": "両方のオプションが選択されている場合、初期セットアップ用のメールは送信されません。オプションのいずれかが選択されている場合、データを提供・認証するためのメールが送信されます。", + "SETUPAUTHENTICATIONLATER": "このユーザーの認証を後で設定します。", + "INVITATION": "認証設定とメール確認のための招待メールを送信してください。", + "INITIALPASSWORD": "ユーザーの初期パスワードを設定してください。" }, "CODEDIALOG": { "TITLE": "電話番号の検証", diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json index 5f089a4ebc..491ac69f64 100644 --- a/console/src/assets/i18n/ko.json +++ b/console/src/assets/i18n/ko.json @@ -789,7 +789,10 @@ "PHONESECTION": "전화번호", "PASSWORDSECTION": "초기 비밀번호", "ADDRESSANDPHONESECTION": "전화번호", - "INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다." + "INITMAILDESCRIPTION": "두 옵션이 모두 선택된 경우 초기화 이메일이 전송되지 않습니다. 하나의 옵션만 선택된 경우 데이터 제공/확인을 위한 이메일이 전송됩니다.", + "SETUPAUTHENTICATIONLATER": "이 사용자의 인증을 나중에 설정하세요.", + "INVITATION": "인증 설정 및 이메일 확인을 위한 초대 이메일을 보내세요.", + "INITIALPASSWORD": "사용자에 대한 초기 비밀번호를 설정하세요." }, "CODEDIALOG": { "TITLE": "전화번호 확인", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index 58bc6a02f6..7f197bd1fe 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -789,7 +789,10 @@ "PHONESECTION": "Телефонски броеви", "PASSWORDSECTION": "Почетна лозинка", "ADDRESSANDPHONESECTION": "Телефонски број", - "INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците." + "INITMAILDESCRIPTION": "Ако се изберат двете опции, нема да се испрати е-пошта за иницијализација. Ако се избере само една од опциите, ќе биде испратена е-пошта за обезбедување / верификација на податоците.", + "SETUPAUTHENTICATIONLATER": "Подесете автентикација подоцна за овој корисник.", + "INVITATION": "Испратете покана по е-пошта за поставување на автентикација и потврда на е-поштата.", + "INITIALPASSWORD": "Поставете почетна лозинка за корисникот." }, "CODEDIALOG": { "TITLE": "Верификација на телефонски број", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index b9bb7b3e55..d309a75834 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -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", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 92c35aabb7..8cf66aff1f 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -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", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index fdd3a7fe96..8a0ec5642c 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -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", diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json index 90ecdacd7f..54942c65e2 100644 --- a/console/src/assets/i18n/ro.json +++ b/console/src/assets/i18n/ro.json @@ -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", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index ffc95e1c91..8a94722d9b 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -796,7 +796,10 @@ "PHONESECTION": "Номера телефонов", "PASSWORDSECTION": "Начальный пароль", "ADDRESSANDPHONESECTION": "Номер телефона", - "INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных." + "INITMAILDESCRIPTION": "Если выбраны оба варианта, электронное письмо для инициализации не будет отправлено. Если выбран только один из вариантов, будет отправлено письмо для предоставления/проверки данных.", + "SETUPAUTHENTICATIONLATER": "Настроить аутентификацию позже для этого пользователя.", + "INVITATION": "Отправить приглашение по электронной почте для настройки аутентификации и подтверждения электронной почты.", + "INITIALPASSWORD": "Установите начальный пароль для пользователя." }, "CODEDIALOG": { "TITLE": "Подтвердить номер телефона", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 4e51b83cc7..0ef588d74c 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -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", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 47e4ecad57..0e1a7d6f53 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -789,7 +789,10 @@ "PHONESECTION": "手机号码", "PASSWORDSECTION": "初始密码", "ADDRESSANDPHONESECTION": "手机号码", - "INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。" + "INITMAILDESCRIPTION": "如果选择了这两个选项,则不会发送初始化电子邮件。如果只选择了其中一个选项,将发送一封提供/验证数据的邮件。", + "SETUPAUTHENTICATIONLATER": "稍后为此用户设置身份验证。", + "INVITATION": "发送邀请邮件以进行身份验证设置和电子邮件验证。", + "INITIALPASSWORD": "为用户设置初始密码。" }, "CODEDIALOG": { "TITLE": "验证手机号码", diff --git a/console/yarn.lock b/console/yarn.lock index 67aef4201a..0ee6b0c94f 100644 --- a/console/yarn.lock +++ b/console/yarn.lock @@ -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"