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==", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true "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": { "@types/glob": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
@ -6353,6 +6358,11 @@
"schema-utils": "^2.6.5" "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": { "file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "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", "@angular/service-worker": "~10.0.2",
"@ngx-translate/core": "^13.0.0", "@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0", "@ngx-translate/http-loader": "^6.0.0",
"@types/file-saver": "^2.0.1",
"@types/google-protobuf": "^3.7.2", "@types/google-protobuf": "^3.7.2",
"@types/uuid": "^8.0.1", "@types/uuid": "^8.0.1",
"angularx-qrcode": "^10.0.6", "angularx-qrcode": "^10.0.6",
"angular-oauth2-oidc": "^10.0.3", "angular-oauth2-oidc": "^10.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"file-saver": "^2.0.2",
"google-proto-files": "^2.2.0", "google-proto-files": "^2.2.0",
"google-protobuf": "^3.12.4", "google-protobuf": "^3.12.4",
"grpc": "^1.24.3", "grpc": "^1.24.3",

View File

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

View File

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

View File

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

View File

@ -15,6 +15,6 @@ export class RoleGuard implements CanActivate {
route: ActivatedRouteSnapshot, route: ActivatedRouteSnapshot,
state: RouterStateSnapshot, state: RouterStateSnapshot,
): Observable<boolean> { ): 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 }}" title="{{projectName}} {{ 'PROJECT.MEMBER.TITLE' | translate }}"
description="{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}"> description="{{ 'PROJECT.MEMBER.DESCRIPTION' | translate }}">
<app-refresh-table *ngIf="project" (refreshed)="changePage()" [dataSize]="dataSource.totalResult" <app-refresh-table *ngIf="project" (refreshed)="changePage()" [dataSize]="dataSource.totalResult"

View File

@ -73,9 +73,17 @@
<ng-container matColumnDef="roles"> <ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
<td class="pointer" [routerLink]="['/user', member.userId]" mat-cell *matCellDef="let member"> <td class="pointer" mat-cell *matCellDef="let member">
<span class="role app-label" *ngFor="let role of member.rolesList; index as i"> <mat-form-field class="form-field" appearance="outline">
{{ 'ROLES.'+role | translate }}</span> <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> </td>
</ng-container> </ng-container>

View File

@ -73,11 +73,6 @@
width: 50px; width: 50px;
max-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 { MatTableModule } from '@angular/material/table';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ProjectMembersComponent } from './project-members.component'; import { IamMembersComponent } from './iam-members.component';
describe('ProjectMembersComponent', () => { describe('IamMembersComponent', () => {
let component: ProjectMembersComponent; let component: IamMembersComponent;
let fixture: ComponentFixture<ProjectMembersComponent>; let fixture: ComponentFixture<IamMembersComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ProjectMembersComponent], declarations: [IamMembersComponent],
imports: [ imports: [
NoopAnimationsModule, NoopAnimationsModule,
MatPaginatorModule, MatPaginatorModule,
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
], ],
}).compileComponents(); }).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ProjectMembersComponent); fixture = TestBed.createComponent(IamMembersComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should compile', () => { it('should compile', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

View File

@ -2,10 +2,11 @@ import { SelectionModel } from '@angular/cdk/collections';
import { AfterViewInit, Component, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; 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 { ProjectMember, ProjectType, User } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service'; import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -25,6 +26,7 @@ export class IamMembersComponent implements AfterViewInit {
public dataSource!: IamMembersDataSource; public dataSource!: IamMembersDataSource;
public selection: SelectionModel<IamMemberView.AsObject> = new SelectionModel<IamMemberView.AsObject>(true, []); 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. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles']; 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 = new IamMembersDataSource(this.adminService);
this.dataSource.loadMembers(0, 25); this.dataSource.loadMembers(0, 25);
this.getRoleOptions();
} }
public ngAfterViewInit(): void { 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 { public removeProjectMemberSelection(): void {
Promise.all(this.selection.selected.map(member => { Promise.all(this.selection.selected.map(member => {
return this.adminService.RemoveIamMember(member.userId).then(() => { 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 { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module'; import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.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 { IamMembersRoutingModule } from './iam-members-routing.module';
import { IamMembersComponent } from './iam-members.component'; import { IamMembersComponent } from './iam-members.component';
@ -39,6 +42,9 @@ import { IamMembersComponent } from './iam-members.component';
MatProgressSpinnerModule, MatProgressSpinnerModule,
FormsModule, FormsModule,
TranslateModule, TranslateModule,
MatFormFieldModule,
MatSelectModule,
HasRolePipeModule,
], ],
}) })
export class IamMembersModule { } export class IamMembersModule { }

View File

@ -8,159 +8,176 @@
{{ createSteps }}</span> {{ createSteps }}</span>
</div> </div>
<ng-container *ngIf="currentCreateStep == 1"> <ng-template appHasRole [appHasRole]="['iam.write']">
<h1>{{'ORG.PAGES.ORGDETAIL_TITLE' | translate}}</h1> <mat-slide-toggle class="example-margin" color="primary" (change)="changeSelf($event)" [(ngModel)]="forSelf">
<form [formGroup]="orgForm" (ngSubmit)="next()"> Use your personal account as organisation owner
<div class="content"> </mat-slide-toggle>
<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 class="btn-container"> <ng-container *ngIf="!forSelf">
<span class="fill-space"></span> <ng-container *ngIf="currentCreateStep == 1">
<button [disabled]="orgForm.invalid" color="primary" mat-raised-button class="big-button" <h1>{{'ORG.PAGES.ORGDETAIL_TITLE' | translate}}</h1>
cdkFocusInitial type="submit"> <form [formGroup]="orgForm" (ngSubmit)="next()">
{{'CONTINUE' | translate}} <div class="content">
</button> <mat-form-field class="formfield" appearance="outline">
</div> <mat-label>{{ 'ORG_DETAIL.DETAIL.NAME' | translate }}</mat-label>
</form> <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> <div class="btn-container">
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION' | translate }}</p> <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"> <div class="user">
<button color="primary" type="submit" <form [formGroup]="userForm" class="form">
mat-stroked-button>{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button> <div class="content">
<p class="section">{{ 'USER.CREATE.NAMEANDEMAILSECTION' | translate }}</p>
<button color="primary" type="submit" mat-raised-button>{{ 'ACTIONS.VERIFY' | translate }}</button> <mat-form-field class="formfield" appearance="outline">
</div> <mat-label>{{ 'USER.PROFILE.USERNAME' | translate }}</mat-label>
<input matInput formControlName="userName" required />
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN_VERIFICATION_SKIP' | translate }}</p> <mat-error *ngIf="userName?.invalid && userName?.errors?.required">
</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">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="formfield" *ngIf="confirmPassword" appearance="outline"> <mat-form-field class="formfield" appearance="outline">
<mat-label>{{ 'USER.PASSWORD.CONFIRM' | translate }}</mat-label> <mat-label>{{ 'USER.PROFILE.EMAIL' | translate }}</mat-label>
<input autocomplete="off" name="confirmPassword" matInput <input matInput formControlName="email" required />
formControlName="confirmPassword" type="password" /> <mat-error *ngIf="email?.invalid && email?.errors?.required">
<mat-error *ngIf="confirmPassword?.errors?.required">
{{ 'USER.VALIDATION.REQUIRED' | translate }} {{ 'USER.VALIDATION.REQUIRED' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="confirmPassword?.errors?.notequal"> </mat-form-field>
{{ 'USER.PASSWORD.NOTEQUAL' | translate }} <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-error>
</mat-form-field> </mat-form-field>
</form> <mat-form-field class="formfield" appearance="outline">
</ng-container> <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>
<div class="btn-container"> </ng-container>
<button color="primary" class="small-button" type="button" (click)="previous()" </ng-container>
mat-stroked-button>{{ 'ACTIONS.BACK' | translate }}</button> </ng-template>
<span class="fill-space"></span> <ng-template appHasRole [appHasRole]="['org.create']">
<button color="primary" class="big-button" (click)="finish()" <div *ngIf="forSelf">
[disabled]="orgForm.invalid || userForm.invalid || ((usePassword && pwdForm) ? pwdForm?.invalid : false)" <ng-container *ngIf="currentCreateStep == 1">
mat-raised-button>{{ 'ACTIONS.FINISH' | translate }}</button> <h1>{{'ORG.PAGES.ORGDETAIL_TITLE' | translate}}</h1>
</div> <form [formGroup]="orgForm" (ngSubmit)="createOrgForSelf()">
</form> <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> </div>
</ng-container> </ng-template>
</div> </div>

View File

@ -2,11 +2,14 @@ import { animate, style, transition, trigger } from '@angular/animations';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { take } from 'rxjs/operators';
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from 'src/app/pages/validators'; import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from 'src/app/pages/validators';
import { CreateOrgRequest, CreateUserRequest, Gender, OrgSetUpResponse } from 'src/app/proto/generated/admin_pb'; import { CreateOrgRequest, CreateUserRequest, Gender, OrgSetUpResponse } from 'src/app/proto/generated/admin_pb';
import { PasswordComplexityPolicy } from 'src/app/proto/generated/auth_pb'; import { PasswordComplexityPolicy } from 'src/app/proto/generated/auth_pb';
import { AdminService } from 'src/app/services/admin.service'; 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 { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -56,6 +59,8 @@ export class OrgCreateComponent {
public policy!: PasswordComplexityPolicy.AsObject; public policy!: PasswordComplexityPolicy.AsObject;
public usePassword: boolean = false; public usePassword: boolean = false;
public forSelf: boolean = true;
constructor( constructor(
private router: Router, private router: Router,
private toast: ToastService, private toast: ToastService,
@ -63,8 +68,13 @@ export class OrgCreateComponent {
private _location: Location, private _location: Location,
private fb: FormBuilder, private fb: FormBuilder,
private orgService: OrgService, 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({ this.orgForm = this.fb.group({
name: ['', [Validators.required]], 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 { public get name(): AbstractControl | null {
return this.orgForm.get('name'); 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 { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { TranslateModule } from '@ngx-translate/core'; 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 { PasswordComplexityViewModule } from 'src/app/modules/password-complexity-view/password-complexity-view.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe.module';
@ -28,8 +30,10 @@ import { OrgCreateComponent } from './org-create.component';
MatSelectModule, MatSelectModule,
HasRolePipeModule, HasRolePipeModule,
TranslateModule, TranslateModule,
HasRoleModule,
MatCheckboxModule, MatCheckboxModule,
PasswordComplexityViewModule, PasswordComplexityViewModule,
MatSlideToggleModule,
], ],
}) })
export class OrgCreateModule { } 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 }}" <app-card title="{{ 'ORG.DOMAINS.TITLE' | translate }}"
description="{{ 'ORG.DOMAINS.DESCRIPTION' | translate }}"> description="{{ 'ORG.DOMAINS.DESCRIPTION' | translate }}">
<div *ngFor="let domain of domains" class="domain"> <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="verified" *ngIf="domain.verified" class="verified las la-check-circle"></i>
<i matTooltip="primary" *ngIf="domain.primary" class="primary las la-star"></i> <i matTooltip="primary" *ngIf="domain.primary" class="primary las la-star"></i>
<span class="fill-space"></span> <span class="fill-space"></span>
<button matTooltip="Remove domain" color="warn" mat-icon-button (click)="removeDomain(domain.domain)"><i <button matTooltip="Remove domain" color="warn" mat-icon-button (click)="removeDomain(domain.domain)"><i
class="las la-trash"></i></button> class="las la-trash"></i></button>
</div> </div>
<p class="new-desc">{{'ORG.PAGES.ORGDOMAIN_VERIFICATION' | translate}}</p> <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> (click)="addNewDomain()">{{'ORG.DOMAINS.NEW' | translate}} </button>
</app-card> </app-card>

View File

@ -16,6 +16,11 @@
.title { .title {
font-size: 16px; font-size: 16px;
margin-right: 1rem; margin-right: 1rem;
cursor: pointer;
&:hover {
text-decoration: underline;
}
} }
.verified, .verified,
@ -24,11 +29,20 @@
margin-right: 1rem; margin-right: 1rem;
} }
.verify-btn {
border-radius: .5rem;
font-size: 13px;
}
.fill-space { .fill-space {
flex: 1; flex: 1;
} }
} }
.add-button {
border-radius: .5rem;
}
.new-desc { .new-desc {
font-size: 14px; font-size: 14px;
color: #818a8a; 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 { ToastService } from 'src/app/services/toast.service';
import { AddDomainDialogComponent } from './add-domain-dialog/add-domain-dialog.component'; import { AddDomainDialogComponent } from './add-domain-dialog/add-domain-dialog.component';
import { DomainVerificationComponent } from './domain-verification/domain-verification.component';
@Component({ @Component({
@ -182,4 +183,18 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
this.router.navigate(['org/members']); 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"> <ng-container matColumnDef="roles">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.MEMBER.ROLES' | translate }} </th>
<td class="pointer" [routerLink]="['/users', member.userId]" mat-cell *matCellDef="let member"> <td class="pointer" mat-cell *matCellDef="let member">
<span class="role app-label" *ngFor="let role of member.rolesList; index as i"> <mat-form-field class="form-field" appearance="outline">
{{ 'ROLES.'+role | translate }}</span> <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> </td>
</ng-container> </ng-container>

View File

@ -73,11 +73,6 @@
width: 50px; width: 50px;
max-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 { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; 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 { OrgService } from 'src/app/services/org.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -25,17 +26,23 @@ export class OrgMembersComponent implements AfterViewInit {
public dataSource!: OrgMembersDataSource; public dataSource!: OrgMembersDataSource;
public selection: SelectionModel<OrgMemberView.AsObject> = new SelectionModel<OrgMemberView.AsObject>(true, []); 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. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles']; public displayedColumns: string[] = ['select', 'firstname', 'lastname', 'username', 'email', 'roles'];
constructor(private orgService: OrgService, constructor(
private orgService: OrgService,
private dialog: MatDialog, private dialog: MatDialog,
private toast: ToastService) { private toast: ToastService,
) {
this.orgService.GetMyOrg().then(org => { this.orgService.GetMyOrg().then(org => {
this.org = org.toObject(); this.org = org.toObject();
this.dataSource = new OrgMembersDataSource(this.orgService); this.dataSource = new OrgMembersDataSource(this.orgService);
this.dataSource.loadMembers(0, 25); this.dataSource.loadMembers(0, 25);
}); });
this.getRoleOptions();
} }
public ngAfterViewInit(): void { public ngAfterViewInit(): void {
@ -44,7 +51,24 @@ export class OrgMembersComponent implements AfterViewInit {
tap(() => this.loadMembersPage()), tap(() => this.loadMembersPage()),
) )
.subscribe(); .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 { private loadMembersPage(): void {

View File

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

View File

@ -13,7 +13,7 @@ const routes: Routes = [
component: OrgCreateComponent, component: OrgCreateComponent,
canActivate: [RoleGuard], canActivate: [RoleGuard],
data: { data: {
roles: ['iam.write'], roles: ['(org.create)?(iam.write)?'],
}, },
loadChildren: () => import('./org-create/org-create.module').then(m => m.OrgCreateModule), 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 { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; 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 { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/member-create-dialog.module'; import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/member-create-dialog.module';
import { CardModule } from 'src/app/modules/card/card.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 { ChangesModule } from '../../modules/changes/changes.module';
import { AddDomainDialogModule } from './org-detail/add-domain-dialog/add-domain-dialog.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 { OrgDetailComponent } from './org-detail/org-detail.component';
import { OrgGridComponent } from './org-grid/org-grid.component'; import { OrgGridComponent } from './org-grid/org-grid.component';
import { OrgsRoutingModule } from './orgs-routing.module'; import { OrgsRoutingModule } from './orgs-routing.module';
import { PolicyGridComponent } from './policy-grid/policy-grid.component'; import { PolicyGridComponent } from './policy-grid/policy-grid.component';
@NgModule({ @NgModule({
declarations: [OrgDetailComponent, OrgGridComponent, PolicyGridComponent], declarations: [OrgDetailComponent, OrgGridComponent, PolicyGridComponent, DomainVerificationComponent],
imports: [ imports: [
CommonModule, CommonModule,
OrgsRoutingModule, OrgsRoutingModule,
@ -53,6 +55,7 @@ import { PolicyGridComponent } from './policy-grid/policy-grid.component';
TranslateModule, TranslateModule,
SharedModule, SharedModule,
ContributorsModule, ContributorsModule,
CopyToClipboardModule,
], ],
}) })
export class OrgsModule { } export class OrgsModule { }

View File

@ -32,7 +32,8 @@
<p class="docs-line" *ngIf="docs?.issuer">Issuer: {{docs.issuer}}</p> <p class="docs-line" *ngIf="docs?.issuer">Issuer: {{docs.issuer}}</p>
</div> </div>
<div class="btn-container"> <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> mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
</div> </div>
</form> </form>

View File

@ -6,12 +6,18 @@
</a> </a>
<h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</h1> <h1>{{ 'PROJECT.PAGES.TITLE' | translate }} {{project?.name}}</h1>
<ng-template appHasRole [appHasRole]="['project.write:'+projectId, 'project.write']"> <ng-template appHasRole [appHasRole]="['project.write:'+projectId, 'project.write']">
<button mat-icon-button (click)="editstate = !editstate" aria-label="Edit project name" <button matTooltip="{{'ACTIONS.EDIT' | translate}}" mat-icon-button (click)="editstate = !editstate"
*ngIf="isZitadel === false"> aria-label="Edit project name" *ngIf="isZitadel === false">
<mat-icon *ngIf="!editstate">edit</mat-icon> <mat-icon *ngIf="!editstate">edit</mat-icon>
<mat-icon *ngIf="editstate">close</mat-icon> <mat-icon *ngIf="editstate">close</mat-icon>
</button> </button>
</ng-template> </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> <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 { public saveProject(): void {
this.projectService.UpdateProject(this.project.projectId, this.project.name).then(() => { this.projectService.UpdateProject(this.project.projectId, this.project.name).then(() => {
this.toast.showInfo('PROJECT.TOAST.UPDATED', true); 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 { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.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 { 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, MatIconModule,
ContributorsModule, ContributorsModule,
WarnDialogModule, WarnDialogModule,
MatTooltipModule,
ProjectRolesModule, ProjectRolesModule,
HasRolePipeModule, HasRolePipeModule,
UserGrantsModule, UserGrantsModule,

View File

@ -1,7 +1,7 @@
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
export function symbolValidator(c: FormControl): any { 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 : { return REGEXP.test(c.value) ? null : {
invalid: true, invalid: true,

View File

@ -7,10 +7,9 @@ import { AuthService } from '../services/auth.service';
name: 'hasRole', name: 'hasRole',
}) })
export class HasRolePipe implements PipeTransform { export class HasRolePipe implements PipeTransform {
constructor(private authService: AuthService) { } constructor(private authService: AuthService) { }
public transform(values: string[], each: boolean = false): Observable<boolean> { public transform(values: string[]): Observable<boolean> {
return this.authService.isAllowed(values, each); 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 { AdminServicePromiseClient } from '../proto/generated/admin_grpc_web_pb';
import { import {
AddIamMemberRequest, AddIamMemberRequest,
ChangeIamMemberRequest,
CreateOrgRequest, CreateOrgRequest,
CreateUserRequest, CreateUserRequest,
IamMember, 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> { public async GetOrgIamPolicy(orgId: string): Promise<OrgIamPolicy> {
const req = new OrgIamPolicyID(); const req = new OrgIamPolicyID();

View File

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

View File

@ -6,15 +6,21 @@ import { ManagementServicePromiseClient } from '../proto/generated/management_gr
import { import {
AddOrgDomainRequest, AddOrgDomainRequest,
AddOrgMemberRequest, AddOrgMemberRequest,
ChangeOrgMemberRequest,
Domain, Domain,
Iam, Iam,
Org, Org,
OrgCreateRequest,
OrgDomain, OrgDomain,
OrgDomainSearchQuery, OrgDomainSearchQuery,
OrgDomainSearchRequest, OrgDomainSearchRequest,
OrgDomainSearchResponse, OrgDomainSearchResponse,
OrgDomainValidationRequest,
OrgDomainValidationResponse,
OrgDomainValidationType,
OrgIamPolicy, OrgIamPolicy,
OrgID, OrgID,
OrgMember,
OrgMemberRoles, OrgMemberRoles,
OrgMemberSearchRequest, OrgMemberSearchRequest,
OrgMemberSearchResponse, OrgMemberSearchResponse,
@ -34,6 +40,7 @@ import {
ProjectGrantCreate, ProjectGrantCreate,
RemoveOrgDomainRequest, RemoveOrgDomainRequest,
RemoveOrgMemberRequest, RemoveOrgMemberRequest,
ValidateOrgDomainRequest,
} from '../proto/generated/management_pb'; } from '../proto/generated/management_pb';
import { GrpcBackendService } from './grpc-backend.service'; import { GrpcBackendService } from './grpc-backend.service';
import { GrpcService, RequestFactory, ResponseMapper } from './grpc.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> { public async SearchMyOrgMembers(limit: number, offset: number): Promise<OrgMemberSearchResponse> {
const req = new OrgMemberSearchRequest(); const req = new OrgMemberSearchRequest();
req.setLimit(limit); 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> { public async AddMyOrgMember(userId: string, rolesList: string[]): Promise<Empty> {
const req = new AddOrgMemberRequest(); const req = new AddOrgMemberRequest();
req.setUserId(userId); 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> { public async RemoveMyOrgMember(userId: string): Promise<Empty> {
const req = new RemoveOrgMemberRequest(); const req = new RemoveOrgMemberRequest();
req.setUserId(userId); req.setUserId(userId);

View File

@ -10,6 +10,7 @@ import {
ApplicationSearchRequest, ApplicationSearchRequest,
ApplicationSearchResponse, ApplicationSearchResponse,
ApplicationUpdate, ApplicationUpdate,
ApplicationView,
GrantedProjectSearchRequest, GrantedProjectSearchRequest,
OIDCApplicationCreate, OIDCApplicationCreate,
OIDCConfig, 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(); const req = new ApplicationID();
req.setProjectId(projectId); req.setProjectId(projectId);
req.setId(applicationId); 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> { public async DeactivateProjectGrant(id: string, projectId: string): Promise<ProjectGrant> {
const req = new ProjectGrantID(); const req = new ProjectGrantID();

View File

@ -56,7 +56,8 @@
"REACTIVATE":"Aktivieren", "REACTIVATE":"Aktivieren",
"DEACTIVATE":"Deaktivieren", "DEACTIVATE":"Deaktivieren",
"REFRESH":"Aktualisieren", "REFRESH":"Aktualisieren",
"LOGIN":"Login" "LOGIN":"Login",
"EDIT":"Bearbeiten"
}, },
"ERRORS": { "ERRORS": {
"REQUIRED": "Bitte fülle alle benötigten Felder aus!", "REQUIRED": "Bitte fülle alle benötigten Felder aus!",
@ -265,9 +266,12 @@
"CREATE":"Organisation erstellen", "CREATE":"Organisation erstellen",
"ORGDETAIL_TITLE":"Gib den Namen und die Domain für die neue Organisation ein.", "ORGDETAIL_TITLE":"Gib den Namen und die Domain für die neue Organisation ein.",
"ORGDOMAIN_TITLE":"Organisations Domain Verifikation", "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!", "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", "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", "DOWNLOAD_FILE":"Datei download",
"SELECTORGTOOLTIP":"Wähle diese Organisation", "SELECTORGTOOLTIP":"Wähle diese Organisation",
"PRIMARYDOMAIN":"Primäre Domain", "PRIMARYDOMAIN":"Primäre Domain",
@ -350,7 +354,8 @@
"DOMAINADDED":"Domain hinzugefügt!", "DOMAINADDED":"Domain hinzugefügt!",
"DOMAINREMOVED":"Domain entfernt!", "DOMAINREMOVED":"Domain entfernt!",
"MEMBERADDED":"Manager hinzugefügt!", "MEMBERADDED":"Manager hinzugefügt!",
"MEMBERREMOVED":"Manager entfernt!" "MEMBERREMOVED":"Manager entfernt!",
"MEMBERCHANGED":"Manager verändert!"
} }
}, },
"ORG_DETAIL": { "ORG_DETAIL": {
@ -408,11 +413,15 @@
"DIALOG": { "DIALOG": {
"REACTIVATE": { "REACTIVATE": {
"TITLE":"Projekt reaktivieren", "TITLE":"Projekt reaktivieren",
"DESCRIPTION":"Wollen Sie das Project wirklich reaktivieren?" "DESCRIPTION":"Wollen Sie das Projekt wirklich reaktivieren?"
}, },
"DEACTIVATE": { "DEACTIVATE": {
"TITLE":"Projekt deaktivieren", "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", "ROLENAMESLIST": "Rollen",
"NOROLES":"Keine Rollen", "NOROLES":"Keine Rollen",
"TOAST":{ "TOAST":{
"PROJECTGRANTUSERGRANTADDED":"Project Berechtigung erstellt!", "PROJECTGRANTUSERGRANTADDED":"Projekt Berechtigung erstellt!",
"PROJECTGRANTADDED":"Projekt Berechtigung erstellt", "PROJECTGRANTADDED":"Projekt Berechtigung erstellt",
"PROJECTGRANTCHANGED":"Projekt Berechtigung geändert!", "PROJECTGRANTCHANGED":"Projekt Berechtigung geändert!",
"PROJECTGRANTMEMBERADDED":"Berechtigungsmanager hinzugefügt!", "PROJECTGRANTMEMBERADDED":"Berechtigungsmanager hinzugefügt!",
@ -533,7 +542,8 @@
"REACTIVATED":"Reaktiviert!", "REACTIVATED":"Reaktiviert!",
"DEACTIVATED":"Deaktiviert!", "DEACTIVATED":"Deaktiviert!",
"UPDATED":"Projekt gespeichert!", "UPDATED":"Projekt gespeichert!",
"GRANTUPDATED":"Berechtigung verändert!" "GRANTUPDATED":"Berechtigung verändert!",
"DELETED":"Projekt gelöscht!"
} }
}, },
"APP": { "APP": {
@ -624,44 +634,44 @@
"ROLES": { "ROLES": {
"ORG_OWNER": "Org. Owner", "ORG_OWNER": "Org. Owner",
"ORG_MEMBER_VIEWER": "Org. Member Viewer", "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_EDITOR":"Org. Editor",
"ORG_VIEWER":"Org. Viewer", "ORG_VIEWER":"Org. Viewer",
"ORG_MEMBER_EDITOR":"Org.. Member Editor", "ORG_MEMBER_EDITOR":"Org.. Member Editor",
"ORG_PROJECT_CREATOR":"Org.. Project Creator", "ORG_PROJECT_CREATOR":"Org.. Projekt Creator",
"ORG_PROJECT_EDITOR":"Org.. Project Editor", "ORG_PROJECT_EDITOR":"Org.. Projekt Editor",
"ORG_PROJECT_VIEWER":"Org.. Project Viewer", "ORG_PROJECT_VIEWER":"Org.. Projekt Viewer",
"ORG_PROJECT_MEMBER_EDITOR":"Org.. Project Member Editor", "ORG_PROJECT_MEMBER_EDITOR":"Org.. Projekt Member Editor",
"ORG_PROJECT_MEMBER_VIEWER":"Org.. Project Member Viewer", "ORG_PROJECT_MEMBER_VIEWER":"Org.. Projekt Member Viewer",
"ORG_PROJECT_ROLE_EDITOR":"Org.. Project Role Editor", "ORG_PROJECT_ROLE_EDITOR":"Org.. Projekt Role Editor",
"ORG_PROJECT_APP_EDITOR":"Org. Project App Editor", "ORG_PROJECT_APP_EDITOR":"Org. Projekt App Editor",
"ORG_PROJECT_APP_VIEWER":"Org. Project App Viewer", "ORG_PROJECT_APP_VIEWER":"Org. Projekt App Viewer",
"ORG_PROJECT_GRANT_EDITOR":"Org. Project Grant Editor" , "ORG_PROJECT_GRANT_EDITOR":"Org. Projekt Grant Editor" ,
"ORG_PROJECT_GRANT_VIEWER":"Org.Project Grant Viewer", "ORG_PROJECT_GRANT_VIEWER":"Org.Projekt Grant Viewer",
"ORG_PROJECT_GRANT_MEMBER_EDITOR":"Org.Project Grant Member Editor", "ORG_PROJECT_GRANT_MEMBER_EDITOR":"Org.Projekt Grant Member Editor",
"ORG_PROJECT_GRANT_MEMBER_VIEWER":"Org.Project Grant Member Viewer", "ORG_PROJECT_GRANT_MEMBER_VIEWER":"Org.Projekt Grant Member Viewer",
"ORG_USER_EDITOR":"Org.User Editor", "ORG_USER_EDITOR":"Org.User Editor",
"ORG_USER_VIEWER":"Org. User Viewer", "ORG_USER_VIEWER":"Org. User Viewer",
"ORG_USER_GRANT_EDITOR":"Org. User Grant Editor", "ORG_USER_GRANT_EDITOR":"Org. User Grant Editor",
"ORG_USER_GRANT_VIEWER":"Org. User Grant Viewer", "ORG_USER_GRANT_VIEWER":"Org. User Grant Viewer",
"ORG_POLICY_EDITOR":"Org. Policy Editor", "ORG_POLICY_EDITOR":"Org. Policy Editor",
"ORG_POLICY_VIEWER":"Org. Policy Viewer", "ORG_POLICY_VIEWER":"Org. Policy Viewer",
"PROJECT_OWNER":"Project Owner", "PROJECT_OWNER":"Projekt Besitzer",
"PROJECT_OWNER_VIEWER":"Project Owner Viewer", "PROJECT_OWNER_VIEWER":"Projekt Besitzer Viewer",
"PROJECT_MEMBER_EDITOR":"Project Member Editor", "PROJECT_MEMBER_EDITOR":"Projekt Manager Editor",
"PROJECT_APP_EDITOR":"Project App Editor", "PROJECT_APP_EDITOR":"Projekt App Editor",
"PROJECT_APP_VIEWER":"Project App Viewer", "PROJECT_APP_VIEWER":"Projekt App Viewer",
"PROJECT_USER_GRANT_EDITOR":"Project User Grant Editor", "PROJECT_USER_GRANT_EDITOR":"Projekt User Grant Editor",
"PROJECT_USER_GRANT_VIEWER":"Project User Grant Viewer", "PROJECT_USER_GRANT_VIEWER":"Projekt User Grant Viewer",
"PROJECT_ROLE_EDITOR": "Project Role Editor", "PROJECT_ROLE_EDITOR": "Projekt Role Editor",
"PROJECT_MEMBER_VIEWER": "Project Member Viewer", "PROJECT_MEMBER_VIEWER": "Projekt Member Viewer",
"PROJECT_GRANT_EDITOR":"Project Grant Editor", "PROJECT_GRANT_EDITOR":"Projekt Grant Editor",
"PROJECT_GRANT_VIEWER":"Project Grant Viewer", "PROJECT_GRANT_VIEWER":"Projekt Grant Viewer",
"PROJECT_GRANT_MEMBER_EDITOR":"Project Grant Member Editor", "PROJECT_GRANT_MEMBER_EDITOR":"Projekt Grant Member Editor",
"PROJECT_GRANT_MEMBER_VIEWER":"Project Grant Member Viewer", "PROJECT_GRANT_MEMBER_VIEWER":"Projekt Grant Member Viewer",
"PROJECT_GRANT_OWNER":"Project Grant Owner", "PROJECT_GRANT_OWNER":"Projekt Grant Owner",
"PROJECT_GRANT_USER_GRANT_EDITOR":"Project Grant User Editor", "PROJECT_GRANT_USER_GRANT_EDITOR":"Projekt Grant User Editor",
"PROJECT_GRANT_USER_GRANT_VIEWER":"Project Grant User Grant Viewer" "PROJECT_GRANT_USER_GRANT_VIEWER":"Projekt Grant User Grant Viewer"
}, },
"GRANTS": { "GRANTS": {
"DELETE":"Grant löschen", "DELETE":"Grant löschen",

View File

@ -56,7 +56,8 @@
"REACTIVATE":"Reactivate", "REACTIVATE":"Reactivate",
"DEACTIVATE":"Deactivate", "DEACTIVATE":"Deactivate",
"REFRESH":"Refresh", "REFRESH":"Refresh",
"LOGIN":"Login" "LOGIN":"Login",
"EDIT":"Edit"
}, },
"ERRORS": { "ERRORS": {
"REQUIRED": "Some required fields are missing!", "REQUIRED": "Some required fields are missing!",
@ -265,9 +266,12 @@
"CREATE":"Create organisation", "CREATE":"Create organisation",
"ORGDETAIL_TITLE":"Enter the name and domain of your new organisation.", "ORGDETAIL_TITLE":"Enter the name and domain of your new organisation.",
"ORGDOMAIN_TITLE":"Organisation domain ownership verification", "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!", "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", "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", "DOWNLOAD_FILE":"Download file",
"SELECTORGTOOLTIP":"Select this organisation", "SELECTORGTOOLTIP":"Select this organisation",
"PRIMARYDOMAIN":"Primary Domain", "PRIMARYDOMAIN":"Primary Domain",
@ -350,7 +354,8 @@
"DOMAINADDED":"Added domain!", "DOMAINADDED":"Added domain!",
"DOMAINREMOVED":"Removed domain!", "DOMAINREMOVED":"Removed domain!",
"MEMBERADDED":"Manager added!", "MEMBERADDED":"Manager added!",
"MEMBERREMOVED":"Manager removed!" "MEMBERREMOVED":"Manager removed!",
"MEMBERCHANGED":"Manager changed!"
} }
}, },
"ORG_DETAIL": { "ORG_DETAIL": {
@ -413,6 +418,10 @@
"DEACTIVATE": { "DEACTIVATE": {
"TITLE":"Deactivate project", "TITLE":"Deactivate project",
"DESCRIPTION":"Do you really want to deactivate your 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!", "REACTIVATED":"Reactivated!",
"DEACTIVATED":"Deactivated!", "DEACTIVATED":"Deactivated!",
"UPDATED":"Project changed!", "UPDATED":"Project changed!",
"GRANTUPDATED":"Grant changed!" "GRANTUPDATED":"Grant changed!",
"DELETED":"Deleted Project!"
} }
}, },
"APP": { "APP": {