fix(console): move org domains into settings page of the organization (#6612)

* fix: hide domains settings for unauthorized users

* refine sidenav object mapping

* move domains to settings

* change docs

* set anchor to list element

* remove canwrite check in ngif

---------

Co-authored-by: Miguel A. C <[email protected]>
This commit is contained in:
Max Peintner
2023-09-29 09:31:35 +02:00
committed by GitHub
co-authored by Miguel A. C
parent eb31c2a3be
commit d01f4d229f
42 changed files with 161 additions and 202 deletions
@@ -0,0 +1,18 @@
<span class="title" mat-dialog-title>{{ 'ORG.DOMAINS.ADD.TITLE' | translate }}</span>
<div mat-dialog-content>
<p class="desc cnsl-secondary-text">{{ 'ORG.DOMAINS.ADD.DESCRIPTION' | translate }}</p>
<cnsl-form-field label="Domain" required="true" class="form-field">
<cnsl-label>Domain</cnsl-label>
<input cnslInput [(ngModel)]="newdomain" />
</cnsl-form-field>
</div>
<div mat-dialog-actions class="action">
<button mat-stroked-button (click)="closeDialog()">
{{ 'ACTIONS.CANCEL' | translate }}
</button>
<button color="primary" mat-raised-button class="ok-button" [disabled]="!newdomain" (click)="closeDialogWithSuccess()">
{{ 'ACTIONS.ADD' | translate }}
</button>
</div>
@@ -0,0 +1,21 @@
.title {
font-size: 1.2rem;
margin-top: 0;
}
.desc {
font-size: 0.9rem;
}
.form-field {
width: 100%;
}
.action {
display: flex;
justify-content: space-between;
.ok-button {
margin-left: 0.5rem;
}
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AddDomainDialogComponent } from './add-domain-dialog.component';
describe('WarnDialogComponent', () => {
let component: AddDomainDialogComponent;
let fixture: ComponentFixture<AddDomainDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AddDomainDialogComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddDomainDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,26 @@
import { Component, Inject } from '@angular/core';
import {
MatLegacyDialogRef as MatDialogRef,
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
} from '@angular/material/legacy-dialog';
@Component({
selector: 'cnsl-add-domain-dialog',
templateUrl: './add-domain-dialog.component.html',
styleUrls: ['./add-domain-dialog.component.scss'],
})
export class AddDomainDialogComponent {
public newdomain: string = '';
constructor(
public dialogRef: MatDialogRef<AddDomainDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {}
public closeDialog(): void {
this.dialogRef.close(false);
}
public closeDialogWithSuccess(): void {
this.dialogRef.close(this.newdomain);
}
}
@@ -0,0 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from 'src/app/modules/input/input.module';
import { AddDomainDialogComponent } from './add-domain-dialog.component';
@NgModule({
declarations: [AddDomainDialogComponent],
imports: [CommonModule, TranslateModule, MatButtonModule, InputModule, FormsModule],
})
export class AddDomainDialogModule {}
@@ -0,0 +1,87 @@
<span class="title" mat-dialog-title>{{ 'ORG.PAGES.ORGDOMAIN.TITLE' | translate }} {{ domain?.domainName }}</span>
<div mat-dialog-content>
<p class="desc">{{ 'ORG.PAGES.ORGDOMAIN.VERIFICATION' | translate }}</p>
<cnsl-info-section [type]="InfoSectionType.WARN" class="desc">{{
'ORG.PAGES.ORGDOMAIN.VERIFICATION_VALIDATION_DESC' | translate
}}</cnsl-info-section>
<p
*ngIf="domain?.validationType !== DomainValidationType.DOMAIN_VALIDATION_TYPE_UNSPECIFIED && !(dns || http)"
class="desc"
>
{{ 'ORG.PAGES.ORGDOMAIN.VERIFICATION_VALIDATION_ONGOING' | translate: domain }}
{{ 'ORG.PAGES.ORGDOMAIN.VERIFICATION_VALIDATION_ONGOING_TYPE' | translate }}
{{ 'ORG.PAGES.ORGDOMAIN.TYPES.' + domain?.validationType | translate }}
</p>
<div class="btn-container">
<button
[disabled]="domain?.validationType === DomainValidationType.DOMAIN_VALIDATION_TYPE_UNSPECIFIED"
color="primary"
type="submit"
mat-raised-button
*ngIf="!(dns || http)"
(click)="validate()"
>
{{ 'ACTIONS.VERIFY' | translate }}
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
<button *ngIf="!showNew" mat-stroked-button color="primary" (click)="showNew = true">
{{ 'ORG.PAGES.ORGDOMAIN.REQUESTNEWTOKEN' | translate }}
</button>
</div>
<ng-container *ngIf="showNew">
<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 }}</p>
<div class="btn-container">
<button mat-stroked-button (click)="saveFile()" color="primary">{{ 'ORG.PAGES.DOWNLOAD_FILE' | translate }}</button>
<button color="primary" class="verify-button" type="submit" mat-raised-button (click)="validate()">
<span>{{ 'ACTIONS.VERIFY' | translate }}</span>
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
</div>
</div>
<div *ngIf="dns">
<p>DNS TOKEN</p>
<div class="domain-line" *ngIf="dns.token">
<p class="entry">{{ dns.token }}</p>
<button
color="primary"
[disabled]="copied === data.clientSecret"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[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>
<button color="primary" type="submit" mat-raised-button class="verify-button" (click)="validate()">
{{ 'ACTIONS.VERIFY' | translate }}
</button>
<mat-spinner class="spinner" *ngIf="validating" diameter="20" mode="indeterminate"></mat-spinner>
</div>
<p class="entry">{{ dns.url }}</p>
</div>
</ng-container>
</div>
<div mat-dialog-actions class="action">
<button mat-stroked-button (click)="closeDialog()">
{{ 'ACTIONS.CLOSE' | translate }}
</button>
</div>
@@ -0,0 +1,28 @@
.btn-container {
display: flex;
margin: -0.5rem;
align-items: center;
button {
margin: 1rem 0.5rem;
}
}
.desc {
font-size: 0.9rem;
}
.entry {
margin: 0.5rem 0;
display: inline-block;
}
.domain-line {
display: flex;
align-items: center;
}
.action {
display: flex;
justify-content: flex-start;
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DomainVerificationComponent } from './domain-verification.component';
describe('DomainVerificationComponent', () => {
let component: DomainVerificationComponent;
let fixture: ComponentFixture<DomainVerificationComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [DomainVerificationComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DomainVerificationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,93 @@
import { Component, Inject } from '@angular/core';
import {
MatLegacyDialogRef as MatDialogRef,
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
} from '@angular/material/legacy-dialog';
import { saveAs } from 'file-saver';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { GenerateOrgDomainValidationResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { Domain, DomainValidationType } from 'src/app/proto/generated/zitadel/org_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'cnsl-domain-verification',
templateUrl: './domain-verification.component.html',
styleUrls: ['./domain-verification.component.scss'],
})
export class DomainVerificationComponent {
public domain?: Domain.AsObject;
public DomainValidationType: any = DomainValidationType;
public http?: GenerateOrgDomainValidationResponse.AsObject;
public dns?: GenerateOrgDomainValidationResponse.AsObject;
public copied: string = '';
public showNew: boolean = false;
public validating: boolean = false;
public InfoSectionType: any = InfoSectionType;
constructor(
private toast: ToastService,
public dialogRef: MatDialogRef<DomainVerificationComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private mgmtService: ManagementService,
) {
this.domain = data.domain;
if (this.domain?.validationType === DomainValidationType.DOMAIN_VALIDATION_TYPE_UNSPECIFIED) {
this.showNew = true;
}
}
async loadHttpToken(): Promise<void> {
if (this.domain) {
this.mgmtService
.generateOrgDomainValidation(this.domain.domainName, DomainValidationType.DOMAIN_VALIDATION_TYPE_HTTP)
.then((http) => {
this.http = http;
});
}
}
async loadDnsToken(): Promise<void> {
if (this.domain) {
this.mgmtService
.generateOrgDomainValidation(this.domain.domainName, DomainValidationType.DOMAIN_VALIDATION_TYPE_DNS)
.then((dns) => {
this.dns = dns;
});
}
}
public closeDialog(): void {
this.dialogRef.close(false);
}
public validate(): void {
this.validating = true;
if (this.domain) {
this.mgmtService
.validateOrgDomain(this.domain.domainName)
.then(() => {
this.dialogRef.close(true);
this.toast.showInfo('ORG.PAGES.ORGDOMAIN.VERIFICATION_SUCCESSFUL', true);
this.validating = false;
})
.catch((error) => {
this.toast.showError(error);
this.validating = false;
});
}
}
public saveFile(): void {
if (this.http) {
const blob = new Blob([this.http.token], { type: 'text/plain;charset=utf-8' });
saveAs(blob, this.http.token + '.txt');
}
}
}
@@ -0,0 +1,67 @@
<ng-container *ngIf="['org.write$'] | hasRole as canwrite$">
<div class="domain-top-view">
<div>
<div class="domain-title-row">
<h2>{{ 'ORG.DOMAINS.TITLE' | translate }}</h2>
<a
mat-icon-button
href="https://zitadel.com/docs/guides/manage/console/organizations#how-zitadel-handles-usernames"
rel="noreferrer"
target="_blank"
>
<i class="las la-info-circle"></i>
</a>
</div>
<p class="desc cnsl-secondary-text">{{ 'ORG.DOMAINS.DESCRIPTION' | translate }}</p>
</div>
<span class="fill-space"></span>
<button
[disabled]="(canwrite$ | async) === false"
matTooltip="Add domain"
mat-raised-button
color="primary"
class="cnsl-action-button"
(click)="addNewDomain()"
>
<mat-icon>add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="addNewDomain()"> </cnsl-action-keys>
</button>
</div>
<cnsl-card *ngFor="let domain of domains" class="domain-card">
<div class="domain">
<span class="title">{{ domain.domainName }}</span>
<i matTooltip="verified" *ngIf="domain.isVerified" class="verified las la-check-circle"></i>
<i matTooltip="primary" *ngIf="domain.isPrimary" class="primary las la-star"></i>
<a
*ngIf="domain.isVerified && !domain.isPrimary && (canwrite$ | async)"
class="primaryset"
(click)="setPrimary(domain)"
>{{ 'ORG.DOMAINS.SETPRIMARY' | translate }}</a
>
<span class="fill-space"></span>
<button
mat-icon-button
[disabled]="(canwrite$ | async) === false || domain.isVerified"
*ngIf="verifyOrgDomains"
(click)="verifyDomain(domain)"
>
<i class="las la-pen"></i>
</button>
<button
class="domain-rem-button"
[disabled]="(canwrite$ | async) === false || domain.isPrimary"
matTooltip="Remove domain"
color="warn"
mat-icon-button
(click)="removeDomain(domain.domainName)"
>
<i class="las la-trash"></i>
</button>
</div>
</cnsl-card>
</ng-container>
@@ -0,0 +1,68 @@
.domain-top-view {
display: flex;
align-items: center;
.domain-title-row {
display: flex;
align-items: center;
h1 {
margin: 0;
}
a i {
font-size: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
}
}
.fill-space {
flex: 1;
}
.actions {
display: flex;
align-items: center;
}
}
.desc {
margin-bottom: 2rem;
font-size: 14px;
}
.domain-card {
.domain {
display: flex;
flex-direction: row;
align-items: center;
.title {
font-size: 16px;
margin-right: 1rem;
}
.primaryset {
font-size: 13px;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.verified,
.primary {
margin-right: 1rem;
}
.verify-btn {
font-size: 13px;
}
.fill-space {
flex: 1;
}
}
}
@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OrgDomainsComponent } from './org-domains.component';
describe('OrgDomainsComponent', () => {
let component: OrgDomainsComponent;
let fixture: ComponentFixture<OrgDomainsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OrgDomainsComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(OrgDomainsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,133 @@
import { Component, OnInit } from '@angular/core';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Domain, DomainValidationType } from 'src/app/proto/generated/zitadel/org_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { AddDomainDialogComponent } from './add-domain-dialog/add-domain-dialog.component';
import { DomainVerificationComponent } from './domain-verification/domain-verification.component';
@Component({
selector: 'cnsl-domains',
templateUrl: './domains.component.html',
styleUrls: ['./domains.component.scss'],
})
export class DomainsComponent implements OnInit {
public domains: Domain.AsObject[] = [];
public primaryDomain: string = '';
public InfoSectionType: any = InfoSectionType;
public verifyOrgDomains: boolean | undefined;
constructor(
private mgmtService: ManagementService,
private toast: ToastService,
private dialog: MatDialog,
breadcrumbService: BreadcrumbService,
) {
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([bread]);
}
ngOnInit(): void {
this.loadDomains();
}
public loadDomains(): void {
this.mgmtService.getDomainPolicy().then((result) => {
this.verifyOrgDomains = result.policy?.validateOrgDomains;
});
this.mgmtService.listOrgDomains().then((result) => {
this.domains = result.resultList;
this.primaryDomain = this.domains.find((domain) => domain.isPrimary)?.domainName ?? '';
});
}
public setPrimary(domain: Domain.AsObject): void {
this.mgmtService
.setPrimaryOrgDomain(domain.domainName)
.then(() => {
this.toast.showInfo('ORG.TOAST.SETPRIMARY', true);
this.loadDomains();
})
.catch((error) => {
this.toast.showError(error);
});
}
public addNewDomain(): void {
const dialogRef = this.dialog.open(AddDomainDialogComponent, {
data: {},
width: '400px',
});
dialogRef.afterClosed().subscribe((domainName) => {
if (domainName) {
this.mgmtService
.addOrgDomain(domainName)
.then(() => {
this.toast.showInfo('ORG.TOAST.DOMAINADDED', true);
if (this.verifyOrgDomains) {
this.verifyDomain({
domainName: domainName,
validationType: DomainValidationType.DOMAIN_VALIDATION_TYPE_UNSPECIFIED,
});
} else {
this.loadDomains();
}
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public removeDomain(domain: string): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'ORG.DOMAINS.DELETE.TITLE',
descriptionKey: 'ORG.DOMAINS.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((del) => {
if (del) {
this.mgmtService
.removeOrgDomain(domain)
.then(() => {
this.toast.showInfo('ORG.TOAST.DOMAINREMOVED', true);
const index = this.domains.findIndex((d) => d.domainName === domain);
if (index > -1) {
this.domains.splice(index, 1);
}
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
public verifyDomain(domain: Partial<Domain.AsObject>): void {
const dialogRef = this.dialog.open(DomainVerificationComponent, {
data: {
domain: domain,
},
width: '500px',
});
dialogRef.afterClosed().subscribe(() => {
this.loadDomains();
});
}
}
@@ -0,0 +1,39 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { CopyToClipboardModule } from 'src/app/directives/copy-to-clipboard/copy-to-clipboard.module';
import { ActionKeysModule } from 'src/app/modules/action-keys/action-keys.module';
import { CardModule } from 'src/app/modules/card/card.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { AddDomainDialogModule } from './add-domain-dialog/add-domain-dialog.module';
import { DomainVerificationComponent } from './domain-verification/domain-verification.component';
import { DomainsComponent } from './domains.component';
@NgModule({
declarations: [DomainsComponent, DomainVerificationComponent],
imports: [
AddDomainDialogModule,
CommonModule,
MatIconModule,
CardModule,
HasRolePipeModule,
ActionKeysModule,
InfoSectionModule,
MatButtonModule,
MatTooltipModule,
CopyToClipboardModule,
InputModule,
TranslateModule,
InfoSectionModule,
MatProgressSpinnerModule,
],
exports: [DomainsComponent],
})
export default class DomainsModule {}
@@ -177,17 +177,6 @@
</a>
</ng-template>
<ng-template cnslHasRole [hasRole]="['org.read']">
<a
class="nav-item"
[routerLinkActive]="['active']"
[routerLink]="['/domains']"
[routerLinkActiveOptions]="{ exact: true }"
>
<span class="label">{{ 'MENU.DOMAINS' | translate }}</span>
</a>
</ng-template>
<ng-template cnslHasRole [hasRole]="['org.read']">
<a
class="nav-item"
@@ -5,7 +5,6 @@
<h2>{{ 'POLICY.DOMAIN_POLICY.TITLE' | translate }}</h2>
<cnsl-info-section *ngIf="isDefault"> {{ 'POLICY.DEFAULTLABEL' | translate }}</cnsl-info-section>
<!-- <ng-template cnslHasRole [hasRole]="['domain.policy.delete']"> -->
<button
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}"
@@ -26,7 +25,6 @@
>
{{ 'POLICY.RESET' | translate }}
</button>
<!-- </ng-template> -->
<div class="domain-policy-content" *ngIf="domainData">
<div class="row">
@@ -18,6 +18,9 @@
<ng-container *ngIf="currentSetting === 'login'">
<cnsl-login-policy [serviceType]="serviceType"></cnsl-login-policy>
</ng-container>
<ng-container *ngIf="currentSetting === 'verified_domains'">
<cnsl-domains></cnsl-domains>
</ng-container>
<ng-container *ngIf="currentSetting === 'domain' && (['iam.policy.write'] | hasRole | async) === true">
<cnsl-domain-policy [serviceType]="serviceType"></cnsl-domain-policy>
</ng-container>
@@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../card/card.module';
import DomainsModule from '../domains/domains.module';
import { DomainPolicyModule } from '../policies/domain-policy/domain-policy.module';
import { GeneralSettingsModule } from '../policies/general-settings/general-settings.module';
import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module';
@@ -40,6 +41,7 @@ import { SettingsListComponent } from './settings-list.component';
PrivacyPolicyModule,
MessageTextsPolicyModule,
SecurityPolicyModule,
DomainsModule,
LoginTextsPolicyModule,
DomainPolicyModule,
TranslateModule,
@@ -43,6 +43,15 @@ export const LOGIN: SidenavSetting = {
},
};
export const VERIFIED_DOMAINS: SidenavSetting = {
id: 'verified_domains',
i18nKey: 'SETTINGS.LIST.VERIFIED_DOMAINS',
groupI18nKey: 'SETTINGS.GROUPS.DOMAIN',
requiredRoles: {
[PolicyComponentServiceType.MGMT]: ['org.read'],
},
};
export const DOMAIN: SidenavSetting = {
id: 'domain',
i18nKey: 'SETTINGS.LIST.DOMAIN',