mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-10 16:15:12 +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 <[email protected]>
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+6
-2
@@ -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'],
|
||||
|
||||
+2
-2
@@ -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 {}
|
||||
|
||||
+1
-1
@@ -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({});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user