From e64af04747268883edf6ea8a5b7a6d21a2adac33 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 Apr 2021 13:35:18 +0200 Subject: [PATCH] feat(console): custom domain (#1624) * fix: custom domain * rem logs * Update console/src/assets/i18n/de.json * Update console/src/assets/i18n/en.json Co-authored-by: Livio Amstutz --- .../modules/features/features.component.html | 8 + .../modules/features/features.component.ts | 2 + .../orgs/org-detail/org-detail.component.html | 13 +- .../orgs/org-detail/org-detail.component.scss | 6 +- .../orgs/org-detail/org-detail.component.ts | 362 +++++++++--------- console/src/app/pages/orgs/orgs.module.ts | 64 ++-- console/src/assets/environment.json | 12 +- console/src/assets/i18n/de.json | 4 +- console/src/assets/i18n/en.json | 4 +- 9 files changed, 252 insertions(+), 223 deletions(-) diff --git a/console/src/app/modules/features/features.component.html b/console/src/app/modules/features/features.component.html index 54ee63839e..20a602cd77 100644 --- a/console/src/app/modules/features/features.component.html +++ b/console/src/app/modules/features/features.component.html @@ -112,6 +112,14 @@ [disabled]="(['iam.features.write'] | hasRole | async) == false"> + +
+ {{'FEATURES.DATA.CUSTOMDOMAIN' | translate}} + + + +
diff --git a/console/src/app/modules/features/features.component.ts b/console/src/app/modules/features/features.component.ts index 3cf1c77bf4..1df129fe20 100644 --- a/console/src/app/modules/features/features.component.ts +++ b/console/src/app/modules/features/features.component.ts @@ -158,6 +158,7 @@ export class FeaturesComponent implements OnDestroy { req.setLoginPolicyPasswordless(this.features.loginPolicyPasswordless); req.setPasswordComplexityPolicy(this.features.passwordComplexityPolicy); req.setLabelPolicy(this.features.labelPolicy); + req.setCustomDomain(this.features.customDomain); this.adminService.setOrgFeatures(req).then(() => { this.toast.showInfo('POLICY.TOAST.SET', true); @@ -175,6 +176,7 @@ export class FeaturesComponent implements OnDestroy { dreq.setLoginPolicyPasswordless(this.features.loginPolicyPasswordless); dreq.setPasswordComplexityPolicy(this.features.passwordComplexityPolicy); dreq.setLabelPolicy(this.features.labelPolicy); + dreq.setCustomDomain(this.features.customDomain); this.adminService.setDefaultFeatures(dreq).then(() => { this.toast.showInfo('POLICY.TOAST.SET', true); diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.html b/console/src/app/pages/orgs/org-detail/org-detail.component.html index e9db5f2023..5c7879db64 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.html +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.html @@ -11,8 +11,12 @@
- {{domain.domainName}} + + + {{domain.domainName}} + {{domain?.domainName}} @@ -26,7 +30,12 @@ class="las la-trash">

{{'ORG.PAGES.ORGDOMAIN.VERIFICATION' | translate}}

- diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.scss b/console/src/app/pages/orgs/org-detail/org-detail.component.scss index 03e9e449de..702661e9b9 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.scss +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.scss @@ -71,11 +71,11 @@ h2 { .title { font-size: 16px; margin-right: 1rem; - cursor: pointer; &:not(.disabled) { &:hover { text-decoration: underline; + cursor: pointer; } } } @@ -119,3 +119,7 @@ h2 { font-size: 14px; color: #818a8a; } + +.custom-domain-deactivated { + margin-bottom: 1rem; +} diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index 7e607a582f..49cd7f6ec0 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -22,206 +22,204 @@ import { DomainVerificationComponent } from './domain-verification/domain-verifi @Component({ - selector: 'app-org-detail', - templateUrl: './org-detail.component.html', - styleUrls: ['./org-detail.component.scss'], + selector: 'app-org-detail', + templateUrl: './org-detail.component.html', + styleUrls: ['./org-detail.component.scss'], }) export class OrgDetailComponent implements OnInit { - public org!: Org.AsObject; - public PolicyComponentServiceType: any = PolicyComponentServiceType; + public org!: Org.AsObject; + public PolicyComponentServiceType: any = PolicyComponentServiceType; - public OrgState: any = OrgState; - public ChangeType: any = ChangeType; + public OrgState: any = OrgState; + public ChangeType: any = ChangeType; - public domains: Domain.AsObject[] = []; - public primaryDomain: string = ''; + public domains: Domain.AsObject[] = []; + public primaryDomain: string = ''; - // members - private loadingSubject: BehaviorSubject = new BehaviorSubject(false); - public loading$: Observable = this.loadingSubject.asObservable(); - public totalMemberResult: number = 0; - public membersSubject: BehaviorSubject - = new BehaviorSubject([]); - public PolicyGridType: any = PolicyGridType; + // members + private loadingSubject: BehaviorSubject = new BehaviorSubject(false); + public loading$: Observable = this.loadingSubject.asObservable(); + public totalMemberResult: number = 0; + public membersSubject: BehaviorSubject + = new BehaviorSubject([]); + public PolicyGridType: any = PolicyGridType; - public features!: Features.AsObject; + public features!: Features.AsObject; - constructor( - private dialog: MatDialog, - public translate: TranslateService, - public mgmtService: ManagementService, - private toast: ToastService, - private router: Router, - ) { + constructor( + private dialog: MatDialog, + public translate: TranslateService, + public mgmtService: ManagementService, + private toast: ToastService, + private router: Router, + ) { } + public ngOnInit(): void { + this.getData(); + } + + private async getData(): Promise { + this.mgmtService.getMyOrg().then((resp) => { + if (resp.org) { + this.org = resp.org; + } + }).catch(error => { + this.toast.showError(error); + }); + this.loadMembers(); + this.loadDomains(); + this.loadFeatures(); + } + + public loadDomains(): void { + 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 changeState(event: MatButtonToggleChange | any): void { + if (event.value === OrgState.ORG_STATE_ACTIVE) { + this.mgmtService.reactivateOrg().then(() => { + this.toast.showInfo('ORG.TOAST.REACTIVATED', true); + }).catch((error) => { + this.toast.showError(error); + }); + } else if (event.value === OrgState.ORG_STATE_INACTIVE) { + this.mgmtService.deactivateOrg().then(() => { + this.toast.showInfo('ORG.TOAST.DEACTIVATED', true); + }).catch((error) => { + this.toast.showError(error); + }); } + } - public ngOnInit(): void { - this.getData(); - } + public addNewDomain(): void { + const dialogRef = this.dialog.open(AddDomainDialogComponent, { + data: {}, + width: '400px', + }); - private async getData(): Promise { - this.mgmtService.getMyOrg().then((resp) => { - if (resp.org) { - this.org = resp.org; - } - }).catch(error => { - this.toast.showError(error); - }); - this.loadMembers(); - this.loadDomains(); - this.loadFeatures(); - } + dialogRef.afterClosed().subscribe(resp => { + if (resp) { + this.mgmtService.addOrgDomain(resp).then(() => { + this.toast.showInfo('ORG.TOAST.DOMAINADDED', true); - public loadDomains(): void { - 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); + setTimeout(() => { this.loadDomains(); - }).catch((error) => { + }, 1000); + }).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(resp => { + if (resp) { + 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 openAddMember(): void { + const dialogRef = this.dialog.open(MemberCreateDialogComponent, { + data: { + creationType: CreationType.ORG, + }, + width: '400px', + }); + + dialogRef.afterClosed().subscribe(resp => { + if (resp) { + const users: User.AsObject[] = resp.users; + const roles: string[] = resp.roles; + + if (users && users.length && roles && roles.length) { + Promise.all(users.map(user => { + return this.mgmtService.addOrgMember(user.id, roles); + })).then(() => { + this.toast.showInfo('ORG.TOAST.MEMBERADDED', true); + setTimeout(() => { + this.loadMembers(); + }, 1000); + }).catch(error => { this.toast.showError(error); - }); - } - - public changeState(event: MatButtonToggleChange | any): void { - if (event.value === OrgState.ORG_STATE_ACTIVE) { - this.mgmtService.reactivateOrg().then(() => { - this.toast.showInfo('ORG.TOAST.REACTIVATED', true); - }).catch((error) => { - this.toast.showError(error); - }); - } else if (event.value === OrgState.ORG_STATE_INACTIVE) { - this.mgmtService.deactivateOrg().then(() => { - this.toast.showInfo('ORG.TOAST.DEACTIVATED', true); - }).catch((error) => { - this.toast.showError(error); - }); + }); } - } + } + }); + } - public addNewDomain(): void { - const dialogRef = this.dialog.open(AddDomainDialogComponent, { - data: {}, - width: '400px', - }); + public showDetail(): void { + this.router.navigate(['org/members']); + } - dialogRef.afterClosed().subscribe(resp => { - if (resp) { - this.mgmtService.addOrgDomain(resp).then(() => { - this.toast.showInfo('ORG.TOAST.DOMAINADDED', true); + public verifyDomain(domain: Domain.AsObject): void { + const dialogRef = this.dialog.open(DomainVerificationComponent, { + data: { + domain: domain, + }, + width: '500px', + }); - setTimeout(() => { - this.loadDomains(); - }, 1000); - }).catch(error => { - this.toast.showError(error); - }); - } - }); - } + dialogRef.afterClosed().subscribe((reload) => { + if (reload) { + this.loadDomains(); + } + }); + } - 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', - }); + public loadMembers(): void { + this.loadingSubject.next(true); + from(this.mgmtService.listOrgMembers(100, 0)).pipe( + map(resp => { + if (resp.details?.totalResult) { + this.totalMemberResult = resp.details?.totalResult; + } + return resp.resultList; + }), + catchError(() => of([])), + finalize(() => this.loadingSubject.next(false)), + ).subscribe(members => { + this.membersSubject.next(members); + }); + } - dialogRef.afterClosed().subscribe(resp => { - if (resp) { - 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 openAddMember(): void { - const dialogRef = this.dialog.open(MemberCreateDialogComponent, { - data: { - creationType: CreationType.ORG, - }, - width: '400px', - }); - - dialogRef.afterClosed().subscribe(resp => { - if (resp) { - const users: User.AsObject[] = resp.users; - const roles: string[] = resp.roles; - - if (users && users.length && roles && roles.length) { - Promise.all(users.map(user => { - return this.mgmtService.addOrgMember(user.id, roles); - })).then(() => { - this.toast.showInfo('ORG.TOAST.MEMBERADDED', true); - setTimeout(() => { - this.loadMembers(); - }, 1000); - }).catch(error => { - this.toast.showError(error); - }); - } - } - }); - } - - public showDetail(): void { - this.router.navigate(['org/members']); - } - - public verifyDomain(domain: Domain.AsObject): void { - const dialogRef = this.dialog.open(DomainVerificationComponent, { - data: { - domain: domain, - }, - width: '500px', - }); - - dialogRef.afterClosed().subscribe((reload) => { - if (reload) { - this.loadDomains(); - } - }); - } - - public loadMembers(): void { - this.loadingSubject.next(true); - from(this.mgmtService.listOrgMembers(100, 0)).pipe( - map(resp => { - if (resp.details?.totalResult) { - this.totalMemberResult = resp.details?.totalResult; - } - return resp.resultList; - }), - catchError(() => of([])), - finalize(() => this.loadingSubject.next(false)), - ).subscribe(members => { - this.membersSubject.next(members); - }); - } - - public loadFeatures(): void { - this.loadingSubject.next(true); - this.mgmtService.getFeatures().then(resp => { - if (resp.features) { - this.features = resp.features; - } - }); - } + public loadFeatures(): void { + this.loadingSubject.next(true); + this.mgmtService.getFeatures().then(resp => { + if (resp.features) { + this.features = resp.features; + } + }); + } } diff --git a/console/src/app/pages/orgs/orgs.module.ts b/console/src/app/pages/orgs/orgs.module.ts index 2a295dcc1c..b810520b40 100644 --- a/console/src/app/pages/orgs/orgs.module.ts +++ b/console/src/app/pages/orgs/orgs.module.ts @@ -16,11 +16,13 @@ import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/memb import { CardModule } from 'src/app/modules/card/card.module'; import { ContributorsModule } from 'src/app/modules/contributors/contributors.module'; import { FeaturesModule } from 'src/app/modules/features/features.module'; +import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module'; import { InputModule } from 'src/app/modules/input/input.module'; import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module'; import { PolicyGridModule } from 'src/app/modules/policy-grid/policy-grid.module'; import { SharedModule } from 'src/app/modules/shared/shared.module'; import { WarnDialogModule } from 'src/app/modules/warn-dialog/warn-dialog.module'; +import { HasFeaturePipeModule } from 'src/app/pipes/has-feature-pipe/has-feature-pipe.module'; import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module'; import { ChangesModule } from '../../modules/changes/changes.module'; @@ -30,35 +32,37 @@ import { OrgDetailComponent } from './org-detail/org-detail.component'; import { OrgsRoutingModule } from './orgs-routing.module'; @NgModule({ - declarations: [OrgDetailComponent, DomainVerificationComponent], - imports: [ - CommonModule, - HasRolePipeModule, - OrgsRoutingModule, - FormsModule, - HasRoleModule, - InputModule, - MatButtonModule, - MatDialogModule, - CardModule, - MatIconModule, - ReactiveFormsModule, - MatButtonToggleModule, - MetaLayoutModule, - MatTabsModule, - MatTooltipModule, - WarnDialogModule, - MemberCreateDialogModule, - MatMenuModule, - ChangesModule, - MatProgressSpinnerModule, - AddDomainDialogModule, - TranslateModule, - SharedModule, - ContributorsModule, - CopyToClipboardModule, - PolicyGridModule, - FeaturesModule, - ], + declarations: [OrgDetailComponent, DomainVerificationComponent], + imports: [ + CommonModule, + HasRolePipeModule, + OrgsRoutingModule, + FormsModule, + HasRoleModule, + InputModule, + InfoSectionModule, + MatButtonModule, + MatDialogModule, + CardModule, + MatIconModule, + ReactiveFormsModule, + MatButtonToggleModule, + MetaLayoutModule, + MatTabsModule, + MatTooltipModule, + WarnDialogModule, + MemberCreateDialogModule, + HasFeaturePipeModule, + MatMenuModule, + ChangesModule, + MatProgressSpinnerModule, + AddDomainDialogModule, + TranslateModule, + SharedModule, + ContributorsModule, + CopyToClipboardModule, + PolicyGridModule, + FeaturesModule, + ], }) export class OrgsModule { } diff --git a/console/src/assets/environment.json b/console/src/assets/environment.json index ca98b5de45..6be181ff8d 100644 --- a/console/src/assets/environment.json +++ b/console/src/assets/environment.json @@ -1,8 +1,8 @@ { - "authServiceUrl": "https://api.zitadel.io", - "mgmtServiceUrl": "https://api.zitadel.io", - "adminServiceUrl":"https://api.zitadel.io", - "subscriptionServiceUrl":"https://sub.zitadel.io", - "issuer": "https://issuer.zitadel.io", - "clientid": "69234247558357051@zitadel" + "authServiceUrl": "https://api.zitadel.dev", + "mgmtServiceUrl": "https://api.zitadel.dev", + "adminServiceUrl":"https://api.zitadel.dev", + "subscriptionServiceUrl":"https://sub.zitadel.dev", + "issuer": "https://issuer.zitadel.dev", + "clientid": "70669160379706195@zitadel" } diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index d83d33f3c5..6a8acc0202 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -525,6 +525,7 @@ "ORGDETAIL_TITLE": "Gebe den Namen und die Domain für die neue Organisation ein.", "ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Geben Sie den Namen der neuen Organisation ein.", "ORGDETAILUSER_TITLE": "Organisationsbesitzer hinzufügen", + "CUSTOMDOMAINFEATUREMISSING":"Das Feature custom-domain ist auf Ihrer Organisation nicht freigeschaltet!", "ORGDOMAIN": { "TITLE": "Verifikation der Domain der Organisation", "VERIFICATION": "Überprüfe den Besitz Deiner Domain, indem Du eine Bestätigungsdatei herunterlädst und unter der angegebenen URL speicherst, oder indem Du sie mit einem DNS-Eintrag verifizierst.", @@ -609,7 +610,8 @@ "LOGINPOLICYFACTORS": "Login Richtlinie: Mltifaktoren - benutzerdefiniert", "LOGINPOLICYPASSWORDLESS": "Login Richtlinie: Passwortlose Authentifizierung - benutzerdefiniert", "LOGINPOLICYCOMPLEXITYPOLICY": "Passwortkomplexitäts Richtlinie - benutzerdefiniert", - "LABELPOLICY": "Label Richtlinie - benutzerdefiniert" + "LABELPOLICY": "Label Richtlinie - benutzerdefiniert", + "CUSTOMDOMAIN": "Domänen Verifikation - verfügbar" }, "TIERSTATES": { "0": "Aktiv", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 1383c21853..53e2f8c97f 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -525,6 +525,7 @@ "ORGDETAIL_TITLE": "Enter the name and domain of your new organisation.", "ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Enter the name of your new organisation.", "ORGDETAILUSER_TITLE": "Configure Organisation Owner", + "CUSTOMDOMAINFEATUREMISSING":"The Feature custom-domain is not active on your organization!", "ORGDOMAIN": { "TITLE": "Organisation Domain Ownership Verification", "VERIFICATION": "Verify the 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 DNS entry for the provided URL. To complete, click the button to verify.", @@ -609,7 +610,8 @@ "LOGINPOLICYFACTORS": "Login Policy: Multifactors - custom", "LOGINPOLICYPASSWORDLESS": "Login Policy: Passwordless Authentication - custom", "LOGINPOLICYCOMPLEXITYPOLICY": "Password Complexity Policy - custom", - "LABELPOLICY": "Labeling Policy - custom" + "LABELPOLICY": "Labeling Policy - custom", + "CUSTOMDOMAIN": "Domain Verification - available" }, "TIERSTATES": { "0": "Active",