feat(console): domain verification, create org, regexp route guards, change org, iam member (#573)

* verification dialog, service

* i18n, verification dialog

* file saver

* savefile

* verify trigger

* delete project, i18n

* org create dialog

* org-create-self

* stylelint

* fix signout redirect

* rm unused dialog component, import

* project i18n de

* regexp roles

* use regex to check permissions

* border radius

* change validation flow

* update org member

* iam member change

* lint

* rm unused css

* Update console/src/assets/i18n/de.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* Update console/src/assets/i18n/de.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* change guard

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
Max Peintner 2020-08-12 08:47:53 +02:00 committed by GitHub
parent 29831111ae
commit 65058ed17c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 757 additions and 255 deletions

View File

@ -2181,6 +2181,11 @@
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/file-saver": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.1.tgz",
"integrity": "sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw=="
},
"@types/glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
@ -6353,6 +6358,11 @@
"schema-utils": "^2.6.5"
}
},
"file-saver": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz",
"integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",

View File

@ -24,11 +24,13 @@
"@angular/service-worker": "~10.0.2",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@types/file-saver": "^2.0.1",
"@types/google-protobuf": "^3.7.2",
"@types/uuid": "^8.0.1",
"angularx-qrcode": "^10.0.6",
"angular-oauth2-oidc": "^10.0.3",
"cors": "^2.8.5",
"file-saver": "^2.0.2",
"google-proto-files": "^2.2.0",
"google-protobuf": "^3.12.4",
"grpc": "^1.24.3",

View File

@ -28,7 +28,7 @@
{{temporg?.name ? temporg.name : 'NO NAME'}}
</button>
<ng-template appHasRole [appHasRole]="['iam.write']">
<ng-template appHasRole [appHasRole]="['(org.create)?(iam.write)?']">
<button mat-menu-item [routerLink]="[ '/org/create' ]">
<mat-icon class="avatar">add</mat-icon>
{{'MENU.NEWORG' | translate}}
@ -79,7 +79,7 @@
</a>
</ng-template>
<ng-template appHasRole [appHasRole]="['project.read']">
<ng-template appHasRole [appHasRole]="['project.read(:[0-9]*)?']">
<div @navitem class="divider">
<div class="line"></div>
<span>{{'MENU.PROJECTSSECTION' | translate}}</span>
@ -108,7 +108,7 @@
</a>
</ng-template>
<ng-template appHasRole [appHasRole]="['user.read']">
<ng-template appHasRole [appHasRole]="['user.read(:[0-9]*)?']">
<div @navitem class="divider">
<div class="line"></div>
<span class="label">

View File

@ -20,6 +20,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { QuicklinkModule } from 'ngx-quicklink';
import { RegExpPipeModule } from 'src/app/pipes/regexp-pipe.module';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
@ -109,6 +110,7 @@ const authConfig: AuthConfig = {
AvatarModule,
WarnDialogModule,
MatDialogModule,
RegExpPipeModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
],
providers: [

View File

@ -8,7 +8,7 @@ import { AuthService } from 'src/app/services/auth.service';
export class HasRoleDirective {
private hasView: boolean = false;
@Input() public set appHasRole(roles: string[]) {
@Input() public set appHasRole(roles: string[] | RegExp[]) {
if (roles && roles.length > 0) {
this.authService.isAllowed(roles).subscribe(isAllowed => {
if (isAllowed && !this.hasView) {

View File

@ -15,6 +15,6 @@ export class RoleGuard implements CanActivate {
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> {
return this.authService.isAllowed(route.data['roles'], true);
return this.authService.isAllowed(route.data['roles']);
}
}

View File

@ -1,4 +1,4 @@
<app-detail-layout [backRouterLink]="[ '/projects', project.projectId]"
<app-detail-layout *ngIf="project" [backRouterLink]="[ '/projects', project?.projectId]"
title="{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}">
<app-refresh-table *ngIf="project" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"

View File

@ -73,9 +73,17 @@
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member">
<span class="role app-label" *ngFor="let role of member.rolesList; index as i">
{{ 'ROLES.'+role | translate }}</span>
<td class="pointer" mat-cell *matCellDef="let member">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.TITLE' | translate }}</mat-label>
<mat-select [(ngModel)]="member.rolesList" multiple
[disabled]="(['org.member.write'] | hasRole | async) == false"
(selectionChange)="updateRoles(member, $event)">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ 'ROLES.'+role | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</td>
</ng-container>

View File

@ -73,11 +73,6 @@
width: 50px;
max-width: 50px;
}
.role {
display: inline-block;
margin: .25rem;
}
}
}

View File

@ -4,31 +4,31 @@ import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ProjectMembersComponent } from './project-members.component';
import { IamMembersComponent } from './iam-members.component';
describe('ProjectMembersComponent', () => {
let component: ProjectMembersComponent;
let fixture: ComponentFixture<ProjectMembersComponent>;
describe('IamMembersComponent', () => {
let component: IamMembersComponent;
let fixture: ComponentFixture<IamMembersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectMembersComponent],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
],
}).compileComponents();
}));
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IamMembersComponent],
imports: [
NoopAnimationsModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectMembersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(IamMembersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
it('should compile', () => {
expect(component).toBeTruthy();
});
});

View File

@ -2,10 +2,11 @@ import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { IamMemberView } from 'src/app/proto/generated/admin_pb';
import { IamMember, IamMemberView } from 'src/app/proto/generated/admin_pb';
import { ProjectMember, ProjectType, User } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
@ -25,6 +26,7 @@ export class IamMembersComponent implements AfterViewInit {
public dataSource!: IamMembersDataSource;
public selection: SelectionModel<IamMemberView.AsObject> = new SelectionModel<IamMemberView.AsObject>(true, []);
public memberRoleOptions: string[] = [];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];
@ -34,6 +36,7 @@ export class IamMembersComponent implements AfterViewInit {
this.dataSource = new IamMembersDataSource(this.adminService);
this.dataSource.loadMembers(0, 25);
this.getRoleOptions();
}
public ngAfterViewInit(): void {
@ -51,6 +54,25 @@ export class IamMembersComponent implements AfterViewInit {
);
}
public getRoleOptions(): void {
this.adminService.GetIamMemberRoles().then(resp => {
this.memberRoleOptions = resp.toObject().rolesList;
}).catch(error => {
this.toast.showError(error);
});
}
updateRoles(member: IamMemberView.AsObject, selectionChange: MatSelectChange): void {
console.log(member.userId, selectionChange.value);
this.adminService.ChangeIamMember(member.userId, selectionChange.value)
.then((newmember: IamMember) => {
this.toast.showInfo('ORG.TOAST.MEMBERCHANGED', true);
}).catch(error => {
this.toast.showError(error);
});
}
public removeProjectMemberSelection(): void {
Promise.all(this.selection.selected.map(member => {
return this.adminService.RemoveIamMember(member.userId).then(() => {

View File

@ -5,15 +5,18 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
import { IamMembersRoutingModule } from './iam-members-routing.module';
import { IamMembersComponent } from './iam-members.component';
@ -39,6 +42,9 @@ import { IamMembersComponent } from './iam-members.component';
MatProgressSpinnerModule,
FormsModule,
TranslateModule,
MatFormFieldModule,
MatSelectModule,
HasRolePipeModule,
],
})
export class IamMembersModule { }

View File

@ -8,159 +8,176 @@
{{ createSteps }}</span>
</div>
<ng-container *ngIf="currentCreateStep == 1">
<h1>{{'ORG.PAGES.ORGDETAIL_TITLE' | translate}}</h1>
<form [formGroup]="orgForm" (ngSubmit)="next()">
<div class="content">
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'ORG_DETAIL.DETAIL.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'ORG_DETAIL.DETAIL.DOMAIN' | translate }}</mat-label>
<input matInput formControlName="domain" />
</mat-form-field>
</div>
<ng-template appHasRole [appHasRole]="['iam.write']">
<mat-slide-toggle class="example-margin" color="primary" (change)="changeSelf($event)" [(ngModel)]="forSelf">
Use your personal account as organisation owner
</mat-slide-toggle>
<div class="btn-container">
<span class="fill-space"></span>
<button [disabled]="orgForm.invalid" color="primary" mat-raised-button class="big-button"
cdkFocusInitial type="submit">
{{'CONTINUE' | translate}}
</button>
</div>
</form>
<ng-container *ngIf="!forSelf">
<ng-container *ngIf="currentCreateStep == 1">
<h1>{{'ORG.PAGES.ORGDETAIL_TITLE' | translate}}</h1>
<form [formGroup]="orgForm" (ngSubmit)="next()">
<div class="content">
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'ORG_DETAIL.DETAIL.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'ORG_DETAIL.DETAIL.DOMAIN' | translate }}</mat-label>
<input matInput formControlName="domain" />
</mat-form-field>
</div>
<!-- <div *ngIf="name?.touched" @openClose>
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION' | translate }}</p>
<div class="btn-container">
<span class="fill-space"></span>
<button [disabled]="orgForm.invalid" color="primary" mat-raised-button class="big-button"
cdkFocusInitial type="submit">
{{'CONTINUE' | translate}}
</button>
</div>
</form>
</ng-container>
<p>{{domain?.value}}/.well-known/caos-developer-domain-association.txt</p>
<ng-container *ngIf="currentCreateStep == createSteps">
<h1>{{'ORG.PAGES.ORGDETAILUSER_TITLE' | translate}}</h1>
<div class="btn-container">
<button color="primary" type="submit"
mat-stroked-button>{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button>
<button color="primary" type="submit" mat-raised-button>{{ 'ACTIONS.VERIFY' | translate }}</button>
</div>
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION_SKIP' | translate }}</p>
</div> -->
</ng-container>
<ng-container *ngIf="currentCreateStep == createSteps">
<h1>{{'ORG.PAGES.ORGDETAILUSER_TITLE' | translate}}</h1>
<div class="user">
<form [formGroup]="userForm" class="form">
<div class="content">
<p class="section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.USERNAME' | translate }}</mat-label>
<input matInput formControlName="userName" required />
<mat-error *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.EMAIL' | translate }}</mat-label>
<input matInput formControlName="email" required />
<mat-error *ngIf="email?.invalid && email?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</mat-label>
<input matInput formControlName="firstName" required />
<mat-error *ngIf="firstName?.invalid && firstName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</mat-label>
<input matInput formControlName="lastName" required />
<mat-error *ngIf="lastName?.invalid && lastName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</mat-label>
<input matInput formControlName="nickName" />
<mat-error *ngIf="nickName?.invalid && nickName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.GENDER' | translate }}</mat-label>
<mat-select formControlName="gender">
<mat-option *ngFor="let gender of genders" [value]="gender">
{{ 'GENDERS.'+gender | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="gender?.invalid && gender?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</mat-label>
<mat-select formControlName="preferredLanguage">
<mat-option *ngFor="let language of languages" [value]="language">
{{ 'LANGUAGES.'+language | translate }}
</mat-option>
<mat-error *ngIf="preferredLanguage?.invalid && preferredLanguage?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-select>
</mat-form-field>
<mat-checkbox class="checkbox" [(ngModel)]="usePassword" [ngModelOptions]="{standalone: true}"
(change)="initPwdValidators()">
{{'ORG.PAGES.USEPASSWORD' | translate}}</mat-checkbox>
<ng-container *ngIf="usePassword && pwdForm">
<p class="section">{{ 'USER.CREATE.PASSWORDSECTION' | translate }}</p>
<app-password-complexity-view class="complexity-view" [policy]="this.policy"
[password]="password">
</app-password-complexity-view>
<form [formGroup]="pwdForm" class="form">
<mat-form-field class="formfield" *ngIf="password" appearance="outline">
<mat-label>{{ 'USER.PASSWORD.NEW' | translate }}</mat-label>
<input autocomplete="off" name="firstpassword" matInput formControlName="password"
type="password" />
<mat-error *ngIf="password?.errors?.required">
<div class="user">
<form [formGroup]="userForm" class="form">
<div class="content">
<p class="section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.USERNAME' | translate }}</mat-label>
<input matInput formControlName="userName" required />
<mat-error *ngIf="userName?.invalid && userName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" *ngIf="confirmPassword" appearance="outline">
<mat-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</mat-label>
<input autocomplete="off" name="confirmPassword" matInput
formControlName="confirmPassword" type="password" />
<mat-error *ngIf="confirmPassword?.errors?.required">
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.EMAIL' | translate }}</mat-label>
<input matInput formControlName="email" required />
<mat-error *ngIf="email?.invalid && email?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
<mat-error *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</mat-label>
<input matInput formControlName="firstName" required />
<mat-error *ngIf="firstName?.invalid && firstName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
</form>
</ng-container>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</mat-label>
<input matInput formControlName="lastName" required />
<mat-error *ngIf="lastName?.invalid && lastName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.NICKNAME' | translate }}</mat-label>
<input matInput formControlName="nickName" />
<mat-error *ngIf="nickName?.invalid && nickName?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<p class="section">{{ 'USER.CREATE.GENDERLANGSECTION' | translate }}</p>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.GENDER' | translate }}</mat-label>
<mat-select formControlName="gender">
<mat-option *ngFor="let gender of genders" [value]="gender">
{{ 'GENDERS.'+gender | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="gender?.invalid && gender?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PROFILE.PREFERRED_LANGUAGE' | translate }}</mat-label>
<mat-select formControlName="preferredLanguage">
<mat-option *ngFor="let language of languages" [value]="language">
{{ 'LANGUAGES.'+language | translate }}
</mat-option>
<mat-error
*ngIf="preferredLanguage?.invalid && preferredLanguage?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-select>
</mat-form-field>
<mat-checkbox class="checkbox" [(ngModel)]="usePassword"
[ngModelOptions]="{standalone: true}" (change)="initPwdValidators()">
{{'ORG.PAGES.USEPASSWORD' | translate}}</mat-checkbox>
<ng-container *ngIf="usePassword && pwdForm">
<p class="section">{{ 'USER.CREATE.PASSWORDSECTION' | translate }}</p>
<app-password-complexity-view class="complexity-view" [policy]="this.policy"
[password]="password">
</app-password-complexity-view>
<form [formGroup]="pwdForm" class="form">
<mat-form-field class="formfield" *ngIf="password" appearance="outline">
<mat-label>{{ 'USER.PASSWORD.NEW' | translate }}</mat-label>
<input autocomplete="off" name="firstpassword" matInput
formControlName="password" type="password" />
<mat-error *ngIf="password?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="formfield" *ngIf="confirmPassword" appearance="outline">
<mat-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</mat-label>
<input autocomplete="off" name="confirmPassword" matInput
formControlName="confirmPassword" type="password" />
<mat-error *ngIf="confirmPassword?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error>
<mat-error *ngIf="confirmPassword?.errors?.notequal">
{{ 'USER.PASSWORD.NOTEQUAL' | translate }}
</mat-error>
</mat-form-field>
</form>
</ng-container>
</div>
<div class="btn-container">
<button color="primary" class="small-button" type="button" (click)="previous()"
mat-stroked-button>{{ 'ACTIONS.BACK' | translate }}</button>
<span class="fill-space"></span>
<button color="primary" class="big-button" (click)="finish()"
[disabled]="orgForm.invalid || userForm.invalid || ((usePassword && pwdForm) ? pwdForm?.invalid : false)"
mat-raised-button>{{ 'ACTIONS.FINISH' | translate }}</button>
</div>
</form>
</div>
<div class="btn-container">
<button color="primary" class="small-button" type="button" (click)="previous()"
mat-stroked-button>{{ 'ACTIONS.BACK' | translate }}</button>
<span class="fill-space"></span>
<button color="primary" class="big-button" (click)="finish()"
[disabled]="orgForm.invalid || userForm.invalid || ((usePassword && pwdForm) ? pwdForm?.invalid : false)"
mat-raised-button>{{ 'ACTIONS.FINISH' | translate }}</button>
</div>
</form>
</ng-container>
</ng-container>
</ng-template>
<ng-template appHasRole [appHasRole]="['org.create']">
<div *ngIf="forSelf">
<ng-container *ngIf="currentCreateStep == 1">
<h1>{{'ORG.PAGES.ORGDETAIL_TITLE' | translate}}</h1>
<form [formGroup]="orgForm" (ngSubmit)="createOrgForSelf()">
<div class="content">
<mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'ORG_DETAIL.DETAIL.NAME' | translate }}</mat-label>
<input matInput formControlName="name" />
</mat-form-field>
</div>
<div class="btn-container">
<span class="fill-space"></span>
<button [disabled]="orgForm.invalid" color="primary" mat-raised-button class="big-button"
cdkFocusInitial type="submit">
{{'CREATE' | translate}}
</button>
</div>
</form>
</ng-container>
</div>
</ng-container>
</ng-template>
</div>

View File

@ -2,11 +2,14 @@ import { animate, style, transition, trigger } from '@angular/animations';
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Router } from '@angular/router';
import { take } from 'rxjs/operators';
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from 'src/app/pages/validators';
import { CreateOrgRequest, CreateUserRequest, Gender, OrgSetUpResponse } from 'src/app/proto/generated/admin_pb';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/auth_pb';
import { AdminService } from 'src/app/services/admin.service';
import { AuthService } from 'src/app/services/auth.service';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
@ -56,6 +59,8 @@ export class OrgCreateComponent {
public policy!: PasswordComplexityPolicy.AsObject;
public usePassword: boolean = false;
public forSelf: boolean = true;
constructor(
private router: Router,
private toast: ToastService,
@ -63,8 +68,13 @@ export class OrgCreateComponent {
private _location: Location,
private fb: FormBuilder,
private orgService: OrgService,
private authService: AuthService,
) {
const validators: Validators[] = [];
this.authService.isAllowed(['iam.write']).pipe(take(1)).subscribe((allowed) => {
if (allowed) {
this.forSelf = false;
}
});
this.orgForm = this.fb.group({
name: ['', [Validators.required]],
@ -165,6 +175,36 @@ export class OrgCreateComponent {
}
}
public changeSelf(change: MatSlideToggleChange): void {
console.log(change.checked);
if (change.checked) {
this.createSteps = 1;
this.orgForm = this.fb.group({
name: ['', [Validators.required]],
});
} else {
this.createSteps = 2;
this.orgForm = this.fb.group({
name: ['', [Validators.required]],
domain: [''],
});
}
}
public createOrgForSelf(): void {
console.log('create for self');
if (this.name && this.name.value) {
this.orgService.CreateOrg(this.name.value).then((org) => {
this.router.navigate(['orgs', org.toObject().id]);
}).catch(error => {
this.toast.showError(error);
});
}
}
public get name(): AbstractControl | null {
return this.orgForm.get('name');
}

View File

@ -7,7 +7,9 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { PasswordComplexityViewModule } from 'src/app/modules/password-complexity-view/password-complexity-view.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
@ -28,8 +30,10 @@ import { OrgCreateComponent } from './org-create.component';
MatSelectModule,
HasRolePipeModule,
TranslateModule,
HasRoleModule,
MatCheckboxModule,
PasswordComplexityViewModule,
MatSlideToggleModule,
],
})
export class OrgCreateModule { }

View File

@ -0,0 +1,47 @@
<span class="title" mat-dialog-title>{{domain.domain}} {{'ORG.PAGES.ORGDOMAIN_TITLE' | translate}} </span>
<div mat-dialog-content>
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION' | translate }}</p>
<p class="desc warn">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION_VALIDATION_DESC' | translate }}</p>
<div class="btn-container">
<button color="primary" type="submit" mat-raised-button
(click)="validate()">{{ 'ACTIONS.VERIFY' | translate }}</button>
</div>
<p>{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION_NEWTOKEN_TITLE' | translate }}</p>
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION_NEWTOKEN_DESC' | translate }}</p>
<div class="btn-container" *ngIf="!(http || dns)">
<button color="primary" mat-raised-button (click)="loadHttpToken()">HTTP</button>
<button color="primary" mat-raised-button (click)="loadDnsToken()">DNS</button>
</div>
<div *ngIf="http">
<p>HTTP TOKEN</p>
<p class="entry">{{http?.url}}.txt</p>
<div class="btn-container">
<button mat-stroked-button (click)="saveFile()"
color="primary">{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button>
</div>
</div>
<div *ngIf="dns">
<p>DNS TOKEN</p>
<div class="line" *ngIf="dns?.token">
<p class="entry">{{dns?.token}}</p>
<button color="primary" [disabled]="copied == data.clientSecret" matTooltip="copy to clipboard"
appCopyToClipboard [valueToCopy]="dns.token" (copiedValue)="copied = $event" mat-icon-button>
<i *ngIf="copied != dns.token" class="las la-clipboard"></i>
<i *ngIf="copied == dns.token" class="las la-clipboard-check"></i>
</button>
</div>
<p class="entry">{{dns?.url}}</p>
</div>
</div>
<div mat-dialog-actions class="action">
<button mat-button class="ok-button" (click)="closeDialog()">
{{'ACTIONS.CLOSE' | translate}}
</button>
</div>

View File

@ -0,0 +1,41 @@
.btn-container {
display: flex;
margin: -.5rem;
button {
margin: 1rem .5rem;
border-radius: .5rem;
display: block;
}
}
.desc {
color: #8795a1;
font-size: .9rem;
&.warn {
color: rgb(201, 51, 71);
}
}
.entry {
margin: .5rem 0;
}
.line {
display: flex;
align-items: center;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: .5rem;
}
button {
border-radius: .5rem;
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DomainVerificationComponent } from './domain-verification.component';
describe('DomainVerificationComponent', () => {
let component: DomainVerificationComponent;
let fixture: ComponentFixture<DomainVerificationComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DomainVerificationComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DomainVerificationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,59 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { saveAs } from 'file-saver';
import { OrgDomainValidationResponse, OrgDomainValidationType, OrgDomainView } from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-domain-verification',
templateUrl: './domain-verification.component.html',
styleUrls: ['./domain-verification.component.scss'],
})
export class DomainVerificationComponent {
public domain!: OrgDomainView.AsObject;
public OrgDomainValidationType: any = OrgDomainValidationType;
public http!: OrgDomainValidationResponse.AsObject;
public dns!: OrgDomainValidationResponse.AsObject;
public copied: string = '';
constructor(
private toast: ToastService,
public dialogRef: MatDialogRef<DomainVerificationComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private orgService: OrgService,
) {
this.domain = data.domain;
}
async loadHttpToken(): Promise<void> {
this.http = (await this.orgService.GenerateMyOrgDomainValidation(
this.domain.domain,
OrgDomainValidationType.ORGDOMAINVALIDATIONTYPE_HTTP)).toObject();
}
async loadDnsToken(): Promise<void> {
this.dns = (await this.orgService.GenerateMyOrgDomainValidation(
this.domain.domain,
OrgDomainValidationType.ORGDOMAINVALIDATIONTYPE_DNS)).toObject();
}
public closeDialog(): void {
this.dialogRef.close(false);
}
public validate(): void {
this.orgService.ValidateMyOrgDomain(this.domain.domain).then(() => {
this.dialogRef.close(false);
}).catch((error) => {
this.toast.showError(error);
});
}
public saveFile(): void {
const blob = new Blob([this.http.token], { type: 'text/plain;charset=utf-8' });
saveAs(blob, this.http.token + '.txt');
}
}

View File

@ -5,16 +5,17 @@
<app-card title="{{ 'ORG.DOMAINS.TITLE' | translate }}"
description="{{ 'ORG.DOMAINS.DESCRIPTION' | translate }}">
<div *ngFor="let domain of domains" class="domain">
<span class="title">{{domain.domain}}</span>
<span (click)="verifyDomain(domain)" class="title">{{domain.domain}}</span>
<i matTooltip="verified" *ngIf="domain.verified" class="verified las la-check-circle"></i>
<i matTooltip="primary" *ngIf="domain.primary" class="primary las la-star"></i>
<span class="fill-space"></span>
<button matTooltip="Remove domain" color="warn" mat-icon-button (click)="removeDomain(domain.domain)"><i
class="las la-trash"></i></button>
</div>
<p class="new-desc">{{'ORG.PAGES.ORGDOMAIN_VERIFICATION' | translate}}</p>
<button matTooltip="Add domain" mat-raised-button color="primary"
<button class="add-button" matTooltip="Add domain" mat-raised-button color="primary"
(click)="addNewDomain()">{{'ORG.DOMAINS.NEW' | translate}} </button>
</app-card>

View File

@ -16,6 +16,11 @@
.title {
font-size: 16px;
margin-right: 1rem;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.verified,
@ -24,11 +29,20 @@
margin-right: 1rem;
}
.verify-btn {
border-radius: .5rem;
font-size: 13px;
}
.fill-space {
flex: 1;
}
}
.add-button {
border-radius: .5rem;
}
.new-desc {
font-size: 14px;
color: #818a8a;

View File

@ -23,6 +23,7 @@ import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
import { AddDomainDialogComponent } from './add-domain-dialog/add-domain-dialog.component';
import { DomainVerificationComponent } from './domain-verification/domain-verification.component';
@Component({
@ -182,4 +183,18 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
this.router.navigate(['org/members']);
}
}
public verifyDomain(domain: OrgDomainView.AsObject): void {
const dialogRef = this.dialog.open(DomainVerificationComponent, {
data: {
domain: domain,
},
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
console.log(resp);
}
});
}
}

View File

@ -73,9 +73,17 @@
<ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member">
<span class="role app-label" *ngFor="let role of member.rolesList; index as i">
{{ 'ROLES.'+role | translate }}</span>
<td class="pointer" mat-cell *matCellDef="let member">
<mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.TITLE' | translate }}</mat-label>
<mat-select [(ngModel)]="member.rolesList" multiple
[disabled]="(['org.member.write'] | hasRole | async) == false"
(selectionChange)="updateRoles(member, $event)">
<mat-option *ngFor="let role of memberRoleOptions" [value]="role">
{{ 'ROLES.'+role | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</td>
</ng-container>

View File

@ -73,11 +73,6 @@
width: 50px;
max-width: 50px;
}
.role {
display: inline-block;
margin: .25rem;
}
}
}

View File

@ -2,10 +2,11 @@ import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
import { Org, OrgMemberView, ProjectMember, ProjectType, User } from 'src/app/proto/generated/management_pb';
import { Org, OrgMember, OrgMemberView, ProjectMember, ProjectType, User } from 'src/app/proto/generated/management_pb';
import { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service';
@ -25,17 +26,23 @@ export class OrgMembersComponent implements AfterViewInit {
public dataSource!: OrgMembersDataSource;
public selection: SelectionModel<OrgMemberView.AsObject> = new SelectionModel<OrgMemberView.AsObject>(true, []);
public memberRoleOptions: string[] = [];
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];
constructor(private orgService: OrgService,
constructor(
private orgService: OrgService,
private dialog: MatDialog,
private toast: ToastService) {
private toast: ToastService,
) {
this.orgService.GetMyOrg().then(org => {
this.org = org.toObject();
this.dataSource = new OrgMembersDataSource(this.orgService);
this.dataSource.loadMembers(0, 25);
});
this.getRoleOptions();
}
public ngAfterViewInit(): void {
@ -44,7 +51,24 @@ export class OrgMembersComponent implements AfterViewInit {
tap(() => this.loadMembersPage()),
)
.subscribe();
}
public getRoleOptions(): void {
this.orgService.GetOrgMemberRoles().then(resp => {
this.memberRoleOptions = resp.toObject().rolesList;
}).catch(error => {
this.toast.showError(error);
});
}
updateRoles(member: OrgMemberView.AsObject, selectionChange: MatSelectChange): void {
console.log(member.userId, selectionChange.value);
this.orgService.ChangeMyOrgMember(member.userId, selectionChange.value)
.then((newmember: OrgMember) => {
this.toast.showInfo('ORG.TOAST.MEMBERCHANGED', true);
}).catch(error => {
this.toast.showError(error);
});
}
private loadMembersPage(): void {

View File

@ -5,15 +5,18 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
import { OrgMembersRoutingModule } from './org-members-routing.module';
import { OrgMembersComponent } from './org-members.component';
@ -39,6 +42,9 @@ import { OrgMembersComponent } from './org-members.component';
FormsModule,
TranslateModule,
DetailLayoutModule,
MatFormFieldModule,
MatSelectModule,
HasRolePipeModule,
],
})
export class OrgMembersModule { }

View File

@ -13,7 +13,7 @@ const routes: Routes = [
component: OrgCreateComponent,
canActivate: [RoleGuard],
data: {
roles: ['iam.write'],
roles: ['(org.create)?(iam.write)?'],
},
loadChildren: () => import('./org-create/org-create.module').then(m => m.OrgCreateModule),
},

View File

@ -11,6 +11,7 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/member-create-dialog.module';
import { CardModule } from 'src/app/modules/card/card.module';
@ -21,13 +22,14 @@ import { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module
import { ChangesModule } from '../../modules/changes/changes.module';
import { AddDomainDialogModule } from './org-detail/add-domain-dialog/add-domain-dialog.module';
import { DomainVerificationComponent } from './org-detail/domain-verification/domain-verification.component';
import { OrgDetailComponent } from './org-detail/org-detail.component';
import { OrgGridComponent } from './org-grid/org-grid.component';
import { OrgsRoutingModule } from './orgs-routing.module';
import { PolicyGridComponent } from './policy-grid/policy-grid.component';
@NgModule({
declarations: [OrgDetailComponent, OrgGridComponent, PolicyGridComponent],
declarations: [OrgDetailComponent, OrgGridComponent, PolicyGridComponent, DomainVerificationComponent],
imports: [
CommonModule,
OrgsRoutingModule,
@ -53,6 +55,7 @@ import { PolicyGridComponent } from './policy-grid/policy-grid.component';
TranslateModule,
SharedModule,
ContributorsModule,
CopyToClipboardModule,
],
})
export class OrgsModule { }

View File

@ -32,7 +32,8 @@
<p class="docs-line" *ngIf="docs?.issuer">Issuer: {{docs.issuer}}</p>
</div>
<div class="btn-container">
<button type="submit" color="primary" [disabled]="appNameForm.invalid || name?.disabled"
<button class="submit-button" type="submit" color="primary"
[disabled]="appNameForm.invalid || name?.disabled"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div>
</form>

View File

@ -6,12 +6,18 @@
</a>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</h1>
<ng-template appHasRole [appHasRole]="['project.write:'+projectId, 'project.write']">
<button mat-icon-button (click)="editstate = !editstate" aria-label="Edit project name"
*ngIf="isZitadel === false">
<button matTooltip="{{'ACTIONS.EDIT' | translate}}" mat-icon-button (click)="editstate = !editstate"
aria-label="Edit project name" *ngIf="isZitadel === false">
<mat-icon *ngIf="!editstate">edit</mat-icon>
<mat-icon *ngIf="editstate">close</mat-icon>
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['project.delete:'+projectId, 'project.delete']">
<button matTooltip="{{'ACTIONS.DELETE' | translate}}" color="warn" mat-icon-button
(click)="deleteProject()" aria-label="Edit project name" *ngIf="isZitadel === false">
<i class="las la-trash"></i>
</button>
</ng-template>
<span class="fill-space"></span>

View File

@ -166,6 +166,28 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
}
}
public deleteProject(): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'PROJECT.PAGES.DIALOG.DELETE.TITLE',
descriptionKey: 'PROJECT.PAGES.DIALOG.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.projectService.RemoveProject(this.projectId).then(() => {
this.toast.showInfo('PROJECT.TOAST.DELETED', true);
this.router.navigate(['/projects']);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
public saveProject(): void {
this.projectService.UpdateProject(this.project.projectId, this.project.name).then(() => {
this.toast.showInfo('PROJECT.TOAST.UPDATED', true);

View File

@ -11,6 +11,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/member-create-dialog.module';
@ -50,6 +51,7 @@ import { ProjectGrantsComponent } from './project-grants/project-grants.componen
MatIconModule,
ContributorsModule,
WarnDialogModule,
MatTooltipModule,
ProjectRolesModule,
HasRolePipeModule,
UserGrantsModule,

View File

@ -1,7 +1,7 @@
import { FormControl } from '@angular/forms';
export function symbolValidator(c: FormControl): any {
const REGEXP = /[^a-z0-9]/gi;
const REGEXP: RegExp = /[^a-z0-9]/gi;
return REGEXP.test(c.value) ? null : {
invalid: true,

View File

@ -7,10 +7,9 @@ import { AuthService } from '../services/auth.service';
name: 'hasRole',
})
export class HasRolePipe implements PipeTransform {
constructor(private authService: AuthService) { }
public transform(values: string[], each: boolean = false): Observable<boolean> {
return this.authService.isAllowed(values, each);
public transform(values: string[]): Observable<boolean> {
return this.authService.isAllowed(values);
}
}

View File

@ -0,0 +1,18 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RegexpPipe } from './regexp.pipe';
@NgModule({
declarations: [
RegexpPipe,
],
imports: [
CommonModule,
],
exports: [
RegexpPipe,
],
})
export class RegExpPipeModule { }

View File

@ -0,0 +1,10 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'regexp',
})
export class RegexpPipe implements PipeTransform {
public transform(value: string): RegExp {
return new RegExp(value);
}
}

View File

@ -5,6 +5,7 @@ import { Metadata } from 'grpc-web';
import { AdminServicePromiseClient } from '../proto/generated/admin_grpc_web_pb';
import {
AddIamMemberRequest,
ChangeIamMemberRequest,
CreateOrgRequest,
CreateUserRequest,
IamMember,
@ -134,6 +135,20 @@ export class AdminService {
);
}
public async ChangeIamMember(
userId: string,
rolesList: string[],
): Promise<IamMember> {
const req = new ChangeIamMemberRequest();
req.setUserId(userId);
req.setRolesList(rolesList);
return await this.request(
c => c.changeIamMember,
req,
f => f,
);
}
public async GetOrgIamPolicy(orgId: string): Promise<OrgIamPolicy> {
const req = new OrgIamPolicyID();

View File

@ -1,5 +1,4 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, finalize, first, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators';
@ -31,7 +30,6 @@ export class AuthService {
private userService: AuthUserService,
private storage: StorageService,
private statehandler: StatehandlerService,
private router: Router,
) {
this.user = merge(
of(this.oauthService.getAccessToken()).pipe(
@ -66,25 +64,28 @@ export class AuthService {
first(),
switchMap(() => from(this.userService.GetMyzitadelPermissions())),
map(rolesResp => rolesResp.toObject().permissionsList),
).subscribe(roles => this.zitadelPermissions.next(roles));
).subscribe(roles => {
console.log(roles);
this.zitadelPermissions.next(roles);
});
}
public isAllowed(roles: string[], each: boolean = false): Observable<boolean> {
public isAllowed(roles: string[] | RegExp[]): Observable<boolean> {
if (roles && roles.length > 0) {
return this.zitadelPermissions.pipe(switchMap(zroles => {
return of(this.hasRoles(zroles, roles, each));
return of(this.hasRoles(zroles, roles));
}));
} else {
return of(false);
}
}
public hasRoles(userRoles: string[], requestedRoles: string[], each: boolean = false): boolean {
return each ?
requestedRoles.every(role => userRoles.includes(role)) :
requestedRoles.findIndex(role => {
return userRoles.findIndex(i => i.includes(role)) > -1;
public hasRoles(userRoles: string[], requestedRoles: string[] | RegExp[]): boolean {
return requestedRoles.findIndex((regexp: any) => {
return userRoles.findIndex(role => {
return (new RegExp(regexp)).test(role);
}) > -1;
}) > -1;
}
public get authenticated(): boolean {
@ -129,7 +130,6 @@ export class AuthService {
this.oauthService.logOut();
this._authenticated = false;
this._authenticationChanged.next(false);
this.router.navigate(['/']);
}
public get activeOrgChanged(): Observable<Org.AsObject> {

View File

@ -6,15 +6,21 @@ import { ManagementServicePromiseClient } from '../proto/generated/management_gr
import {
AddOrgDomainRequest,
AddOrgMemberRequest,
ChangeOrgMemberRequest,
Domain,
Iam,
Org,
OrgCreateRequest,
OrgDomain,
OrgDomainSearchQuery,
OrgDomainSearchRequest,
OrgDomainSearchResponse,
OrgDomainValidationRequest,
OrgDomainValidationResponse,
OrgDomainValidationType,
OrgIamPolicy,
OrgID,
OrgMember,
OrgMemberRoles,
OrgMemberSearchRequest,
OrgMemberSearchResponse,
@ -34,6 +40,7 @@ import {
ProjectGrantCreate,
RemoveOrgDomainRequest,
RemoveOrgMemberRequest,
ValidateOrgDomainRequest,
} from '../proto/generated/management_pb';
import { GrpcBackendService } from './grpc-backend.service';
import { GrpcService, RequestFactory, ResponseMapper } from './grpc.service';
@ -121,6 +128,31 @@ export class OrgService {
);
}
public async GenerateMyOrgDomainValidation(domain: string, type: OrgDomainValidationType):
Promise<OrgDomainValidationResponse> {
const req: OrgDomainValidationRequest = new OrgDomainValidationRequest();
req.setDomain(domain);
req.setType(type);
return await this.request(
c => c.generateMyOrgDomainValidation,
req,
f => f,
);
}
public async ValidateMyOrgDomain(domain: string):
Promise<Empty> {
const req: ValidateOrgDomainRequest = new ValidateOrgDomainRequest();
req.setDomain(domain);
return await this.request(
c => c.validateMyOrgDomain,
req,
f => f,
);
}
public async SearchMyOrgMembers(limit: number, offset: number): Promise<OrgMemberSearchResponse> {
const req = new OrgMemberSearchRequest();
req.setLimit(limit);
@ -142,6 +174,16 @@ export class OrgService {
);
}
public async CreateOrg(name: string): Promise<Org> {
const req = new OrgCreateRequest();
req.setName(name);
return await this.request(
c => c.createOrg,
req,
f => f,
);
}
public async AddMyOrgMember(userId: string, rolesList: string[]): Promise<Empty> {
const req = new AddOrgMemberRequest();
req.setUserId(userId);
@ -155,6 +197,18 @@ export class OrgService {
);
}
public async ChangeMyOrgMember(userId: string, rolesList: string[]): Promise<OrgMember> {
const req = new ChangeOrgMemberRequest();
req.setUserId(userId);
req.setRolesList(rolesList);
return await this.request(
c => c.changeMyOrgMember,
req,
f => f,
);
}
public async RemoveMyOrgMember(userId: string): Promise<Empty> {
const req = new RemoveOrgMemberRequest();
req.setUserId(userId);

View File

@ -10,6 +10,7 @@ import {
ApplicationSearchRequest,
ApplicationSearchResponse,
ApplicationUpdate,
ApplicationView,
GrantedProjectSearchRequest,
OIDCApplicationCreate,
OIDCConfig,
@ -488,7 +489,7 @@ export class ProjectService {
);
}
public async GetApplicationById(projectId: string, applicationId: string): Promise<Application> {
public async GetApplicationById(projectId: string, applicationId: string): Promise<ApplicationView> {
const req = new ApplicationID();
req.setProjectId(projectId);
req.setId(applicationId);
@ -519,6 +520,16 @@ export class ProjectService {
);
}
public async RemoveProject(id: string): Promise<Empty> {
const req = new ProjectID();
req.setId(id);
return await this.request(
c => c.removeProject,
req,
f => f,
);
}
public async DeactivateProjectGrant(id: string, projectId: string): Promise<ProjectGrant> {
const req = new ProjectGrantID();

View File

@ -56,7 +56,8 @@
"REACTIVATE":"Aktivieren",
"DEACTIVATE":"Deaktivieren",
"REFRESH":"Aktualisieren",
"LOGIN":"Login"
"LOGIN":"Login",
"EDIT":"Bearbeiten"
},
"ERRORS": {
"REQUIRED": "Bitte fülle alle benötigten Felder aus!",
@ -265,9 +266,12 @@
"CREATE":"Organisation erstellen",
"ORGDETAIL_TITLE":"Gib den Namen und die Domain für die neue Organisation ein.",
"ORGDOMAIN_TITLE":"Organisations Domain Verifikation",
"ORGDOMAIN_VERIFICATION":"Stelle deine Domain bereit und überprüfe deren Besitz indem du eine Bestätigungsdatei herunterladen und unter der unten angegebenen URL hochladen. Klicken zum Abschluss auf die Schaltfläche, um diese zu überprüfen.",
"ORGDOMAIN_VERIFICATION":"Überprüfen Sie den Besitz ihrer Domain indem Sie eine Bestätigungsdatei herunterladen und unter der angegebenen URL hochladen oder Sie sie mit einem DNS Eintrag verifizieren.",
"ORGDOMAIN_VERIFICATION_SKIP":"Du kannst die Überprüfung vorerst überspringen und deine Organisation weiter erstellen. Um deine Organisation jedoch verwenden zu können, muss dieser Schritt abgeschlossen sein!",
"ORGDETAILUSER_TITLE":"Organisationsbesitzer hinzufügen",
"ORGDOMAIN_VERIFICATION_VALIDATION_DESC":"Die Tokens werden regelmäßig überprüft, um sicherzustellen, dass sie weiterhin Besitzer der Domain sind.",
"ORGDOMAIN_VERIFICATION_NEWTOKEN_TITLE":"Neues Token anfordern",
"ORGDOMAIN_VERIFICATION_NEWTOKEN_DESC":"Wenn Sie ein neues Token anfordern wollen, klicken Sie auf die gewünschte Methode. Wenn Sie ein vorhandenes Token validieren möchten Klicken Sie auf Validieren.",
"DOWNLOAD_FILE":"Datei download",
"SELECTORGTOOLTIP":"Wähle diese Organisation",
"PRIMARYDOMAIN":"Primäre Domain",
@ -350,7 +354,8 @@
"DOMAINADDED":"Domain hinzugefügt!",
"DOMAINREMOVED":"Domain entfernt!",
"MEMBERADDED":"Manager hinzugefügt!",
"MEMBERREMOVED":"Manager entfernt!"
"MEMBERREMOVED":"Manager entfernt!",
"MEMBERCHANGED":"Manager verändert!"
}
},
"ORG_DETAIL": {
@ -408,11 +413,15 @@
"DIALOG": {
"REACTIVATE": {
"TITLE":"Projekt reaktivieren",
"DESCRIPTION":"Wollen Sie das Project wirklich reaktivieren?"
"DESCRIPTION":"Wollen Sie das Projekt wirklich reaktivieren?"
},
"DEACTIVATE": {
"TITLE":"Projekt deaktivieren",
"DESCRIPTION":"Wollen Sie das Project wirklich deaktivieren?"
"DESCRIPTION":"Wollen Sie das Projekt wirklich deaktivieren?"
},
"DELETE": {
"TITLE":"Projekt löschen",
"DESCRIPTION":"Wollen Sie das Projekt wirklich löschen?"
}
}
},
@ -479,7 +488,7 @@
"ROLENAMESLIST": "Rollen",
"NOROLES":"Keine Rollen",
"TOAST":{
"PROJECTGRANTUSERGRANTADDED":"Project Berechtigung erstellt!",
"PROJECTGRANTUSERGRANTADDED":"Projekt Berechtigung erstellt!",
"PROJECTGRANTADDED":"Projekt Berechtigung erstellt",
"PROJECTGRANTCHANGED":"Projekt Berechtigung geändert!",
"PROJECTGRANTMEMBERADDED":"Berechtigungsmanager hinzugefügt!",
@ -533,7 +542,8 @@
"REACTIVATED":"Reaktiviert!",
"DEACTIVATED":"Deaktiviert!",
"UPDATED":"Projekt gespeichert!",
"GRANTUPDATED":"Berechtigung verändert!"
"GRANTUPDATED":"Berechtigung verändert!",
"DELETED":"Projekt gelöscht!"
}
},
"APP": {
@ -624,44 +634,44 @@
"ROLES": {
"ORG_OWNER": "Org. Owner",
"ORG_MEMBER_VIEWER": "Org. Member Viewer",
"ORG_PROJECT_ROLE_VIEWER": "Org. Project Role Viewer",
"ORG_PROJECT_ROLE_VIEWER": "Org. Projekt Role Viewer",
"ORG_EDITOR":"Org. Editor",
"ORG_VIEWER":"Org. Viewer",
"ORG_MEMBER_EDITOR":"Org.. Member Editor",
"ORG_PROJECT_CREATOR":"Org.. Project Creator",
"ORG_PROJECT_EDITOR":"Org.. Project Editor",
"ORG_PROJECT_VIEWER":"Org.. Project Viewer",
"ORG_PROJECT_MEMBER_EDITOR":"Org.. Project Member Editor",
"ORG_PROJECT_MEMBER_VIEWER":"Org.. Project Member Viewer",
"ORG_PROJECT_ROLE_EDITOR":"Org.. Project Role Editor",
"ORG_PROJECT_APP_EDITOR":"Org. Project App Editor",
"ORG_PROJECT_APP_VIEWER":"Org. Project App Viewer",
"ORG_PROJECT_GRANT_EDITOR":"Org. Project Grant Editor" ,
"ORG_PROJECT_GRANT_VIEWER":"Org.Project Grant Viewer",
"ORG_PROJECT_GRANT_MEMBER_EDITOR":"Org.Project Grant Member Editor",
"ORG_PROJECT_GRANT_MEMBER_VIEWER":"Org.Project Grant Member Viewer",
"ORG_PROJECT_CREATOR":"Org.. Projekt Creator",
"ORG_PROJECT_EDITOR":"Org.. Projekt Editor",
"ORG_PROJECT_VIEWER":"Org.. Projekt Viewer",
"ORG_PROJECT_MEMBER_EDITOR":"Org.. Projekt Member Editor",
"ORG_PROJECT_MEMBER_VIEWER":"Org.. Projekt Member Viewer",
"ORG_PROJECT_ROLE_EDITOR":"Org.. Projekt Role Editor",
"ORG_PROJECT_APP_EDITOR":"Org. Projekt App Editor",
"ORG_PROJECT_APP_VIEWER":"Org. Projekt App Viewer",
"ORG_PROJECT_GRANT_EDITOR":"Org. Projekt Grant Editor" ,
"ORG_PROJECT_GRANT_VIEWER":"Org.Projekt Grant Viewer",
"ORG_PROJECT_GRANT_MEMBER_EDITOR":"Org.Projekt Grant Member Editor",
"ORG_PROJECT_GRANT_MEMBER_VIEWER":"Org.Projekt Grant Member Viewer",
"ORG_USER_EDITOR":"Org.User Editor",
"ORG_USER_VIEWER":"Org. User Viewer",
"ORG_USER_GRANT_EDITOR":"Org. User Grant Editor",
"ORG_USER_GRANT_VIEWER":"Org. User Grant Viewer",
"ORG_POLICY_EDITOR":"Org. Policy Editor",
"ORG_POLICY_VIEWER":"Org. Policy Viewer",
"PROJECT_OWNER":"Project Owner",
"PROJECT_OWNER_VIEWER":"Project Owner Viewer",
"PROJECT_MEMBER_EDITOR":"Project Member Editor",
"PROJECT_APP_EDITOR":"Project App Editor",
"PROJECT_APP_VIEWER":"Project App Viewer",
"PROJECT_USER_GRANT_EDITOR":"Project User Grant Editor",
"PROJECT_USER_GRANT_VIEWER":"Project User Grant Viewer",
"PROJECT_ROLE_EDITOR": "Project Role Editor",
"PROJECT_MEMBER_VIEWER": "Project Member Viewer",
"PROJECT_GRANT_EDITOR":"Project Grant Editor",
"PROJECT_GRANT_VIEWER":"Project Grant Viewer",
"PROJECT_GRANT_MEMBER_EDITOR":"Project Grant Member Editor",
"PROJECT_GRANT_MEMBER_VIEWER":"Project Grant Member Viewer",
"PROJECT_GRANT_OWNER":"Project Grant Owner",
"PROJECT_GRANT_USER_GRANT_EDITOR":"Project Grant User Editor",
"PROJECT_GRANT_USER_GRANT_VIEWER":"Project Grant User Grant Viewer"
"PROJECT_OWNER":"Projekt Besitzer",
"PROJECT_OWNER_VIEWER":"Projekt Besitzer Viewer",
"PROJECT_MEMBER_EDITOR":"Projekt Manager Editor",
"PROJECT_APP_EDITOR":"Projekt App Editor",
"PROJECT_APP_VIEWER":"Projekt App Viewer",
"PROJECT_USER_GRANT_EDITOR":"Projekt User Grant Editor",
"PROJECT_USER_GRANT_VIEWER":"Projekt User Grant Viewer",
"PROJECT_ROLE_EDITOR": "Projekt Role Editor",
"PROJECT_MEMBER_VIEWER": "Projekt Member Viewer",
"PROJECT_GRANT_EDITOR":"Projekt Grant Editor",
"PROJECT_GRANT_VIEWER":"Projekt Grant Viewer",
"PROJECT_GRANT_MEMBER_EDITOR":"Projekt Grant Member Editor",
"PROJECT_GRANT_MEMBER_VIEWER":"Projekt Grant Member Viewer",
"PROJECT_GRANT_OWNER":"Projekt Grant Owner",
"PROJECT_GRANT_USER_GRANT_EDITOR":"Projekt Grant User Editor",
"PROJECT_GRANT_USER_GRANT_VIEWER":"Projekt Grant User Grant Viewer"
},
"GRANTS": {
"DELETE":"Grant löschen",

View File

@ -56,7 +56,8 @@
"REACTIVATE":"Reactivate",
"DEACTIVATE":"Deactivate",
"REFRESH":"Refresh",
"LOGIN":"Login"
"LOGIN":"Login",
"EDIT":"Edit"
},
"ERRORS": {
"REQUIRED": "Some required fields are missing!",
@ -265,9 +266,12 @@
"CREATE":"Create organisation",
"ORGDETAIL_TITLE":"Enter the name and domain of your new organisation.",
"ORGDOMAIN_TITLE":"Organisation domain ownership verification",
"ORGDOMAIN_VERIFICATION":"Provide your web domain and verify their ownership. You need to download a verification file and upload it at the provided URL listed below. To complete, click the button to verify.",
"ORGDOMAIN_VERIFICATION":"Verify ownership of your domain. You need to download a verification file and upload it at the provided URL listed below or place a TXT Record at the provided Url. To complete, click the button to verify.",
"ORGDOMAIN_VERIFICATION_SKIP":"You can skip verification for now and continue to create your organisation, but in order to use your organisation this step has to be completed!",
"ORGDETAILUSER_TITLE":"Configure Organisation Owner",
"ORGDOMAIN_VERIFICATION_VALIDATION_DESC":"The tokens are checked regularly to ensure you are still owner of the domain.",
"ORGDOMAIN_VERIFICATION_NEWTOKEN_TITLE":"Request new token",
"ORGDOMAIN_VERIFICATION_NEWTOKEN_DESC":"If you want to request a new token, select you preferred method, if you want to validate a persisting token, click on the button above.",
"DOWNLOAD_FILE":"Download file",
"SELECTORGTOOLTIP":"Select this organisation",
"PRIMARYDOMAIN":"Primary Domain",
@ -350,7 +354,8 @@
"DOMAINADDED":"Added domain!",
"DOMAINREMOVED":"Removed domain!",
"MEMBERADDED":"Manager added!",
"MEMBERREMOVED":"Manager removed!"
"MEMBERREMOVED":"Manager removed!",
"MEMBERCHANGED":"Manager changed!"
}
},
"ORG_DETAIL": {
@ -413,6 +418,10 @@
"DEACTIVATE": {
"TITLE":"Deactivate project",
"DESCRIPTION":"Do you really want to deactivate your project?"
},
"DELETE": {
"TITLE":"Projekt löschen",
"DESCRIPTION":"Wollen Sie das Project wirklich löschen?"
}
}
},
@ -533,7 +542,8 @@
"REACTIVATED":"Reactivated!",
"DEACTIVATED":"Deactivated!",
"UPDATED":"Project changed!",
"GRANTUPDATED":"Grant changed!"
"GRANTUPDATED":"Grant changed!",
"DELETED":"Deleted Project!"
}
},
"APP": {