mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-11 08:35:13 +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:
@@ -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