Merge branch 'main' into next-merge
@@ -101,7 +101,8 @@ Please make sure you cover your changes with tests before marking a Pull Request
|
|||||||
- [ ] Integration tests against the gRPC server ensure that probable good and bad read and write permissions are tested.
|
- [ ] Integration tests against the gRPC server ensure that probable good and bad read and write permissions are tested.
|
||||||
- [ ] Integration tests against the gRPC server ensure that the API is easily usable despite eventual consistency.
|
- [ ] Integration tests against the gRPC server ensure that the API is easily usable despite eventual consistency.
|
||||||
- [ ] Integration tests against the gRPC server ensure that all probable login and registration flows are covered."
|
- [ ] Integration tests against the gRPC server ensure that all probable login and registration flows are covered."
|
||||||
- [ ] Integration tests ensure that certain commands send expected notifications.
|
- [ ] Integration tests ensure that certain commands emit expected events that trigger notifications.
|
||||||
|
- [ ] Integration tests ensure that certain events trigger expected notifications.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
|
12
README.md
@@ -34,10 +34,10 @@ Do you look for a user management that's quickly set up like Auth0 and open sour
|
|||||||
|
|
||||||
Do you have a project that requires multi-tenant user management with self-service for your customers?
|
Do you have a project that requires multi-tenant user management with self-service for your customers?
|
||||||
|
|
||||||
Look no further — ZITADEL combines the ease of Auth0 with the versatility of Keycloak.
|
Look no further — ZITADEL is the identity infrastructure, simplified for you.
|
||||||
|
|
||||||
We provide you with a wide range of out-of-the-box features to accelerate your project.
|
We provide you with a wide range of out-of-the-box features to accelerate your project.
|
||||||
Multi-tenancy with branding customization, secure login, self-service, OpenID Connect, OAuth2.x, SAML2, LDAP, Passwordless with FIDO2 (including Passkeys), OTP, U2F, and an unlimited audit trail is there for you, ready to use.
|
Multi-tenancy with branding customization, secure login, self-service, OpenID Connect, OAuth2.x, SAML2, LDAP, Passkeys / FIDO2, OTP, U2F, and an unlimited audit trail is there for you, ready to use.
|
||||||
|
|
||||||
With ZITADEL you can rely on a hardened and extensible turnkey solution to solve all of your authentication and authorization needs.
|
With ZITADEL you can rely on a hardened and extensible turnkey solution to solve all of your authentication and authorization needs.
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ See all guides [here](https://zitadel.com/docs/self-hosting/deploy/overview)
|
|||||||
|
|
||||||
If you want to experience a hands-free ZITADEL, you should use [ZITADEL Cloud](https://zitadel.cloud).
|
If you want to experience a hands-free ZITADEL, you should use [ZITADEL Cloud](https://zitadel.cloud).
|
||||||
|
|
||||||
It is free for up to 25'000 authenticated requests and provides you all the features that make ZITADEL great.
|
ZITADEL Cloud comes with a free tier and provides you all the features that you find in the open source version.
|
||||||
Learn more about the [pay-as-you-go pricing](https://zitadel.com/pricing).
|
Learn more about the [pay-as-you-go pricing](https://zitadel.com/pricing).
|
||||||
|
|
||||||
### Example applications
|
### Example applications
|
||||||
@@ -84,6 +84,7 @@ We built ZITADEL with a complex multi-tenancy architecture in mind and provide t
|
|||||||
Yet it offers everything you need for a customer identity ([CIAM](https://zitadel.com/docs/guides/solution-scenarios/b2c)) use case.
|
Yet it offers everything you need for a customer identity ([CIAM](https://zitadel.com/docs/guides/solution-scenarios/b2c)) use case.
|
||||||
|
|
||||||
- [API-first approach](https://zitadel.com/docs/apis/introduction)
|
- [API-first approach](https://zitadel.com/docs/apis/introduction)
|
||||||
|
- [Multi-tenancy](https://zitadel.com/docs/guides/solution-scenarios/b2b) authentication and access management
|
||||||
- Strong audit trail thanks to [event sourcing](https://zitadel.com/docs/concepts/eventstore/overview) as storage pattern
|
- Strong audit trail thanks to [event sourcing](https://zitadel.com/docs/concepts/eventstore/overview) as storage pattern
|
||||||
- [Actions](https://zitadel.com/docs/apis/actions/introduction) to react on events with custom code and extended ZITADEL for you needs
|
- [Actions](https://zitadel.com/docs/apis/actions/introduction) to react on events with custom code and extended ZITADEL for you needs
|
||||||
- [Branding](https://zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience across multiple organizations
|
- [Branding](https://zitadel.com/docs/guides/manage/customize/branding) for a uniform user experience across multiple organizations
|
||||||
@@ -94,12 +95,15 @@ Yet it offers everything you need for a customer identity ([CIAM](https://zitade
|
|||||||
|
|
||||||
Authentication
|
Authentication
|
||||||
- Single Sign On (SSO)
|
- Single Sign On (SSO)
|
||||||
- Passwordless with FIDO2 support (Including Passkeys)
|
- Passkeys support (FIDO2 / WebAuthN)
|
||||||
- Username / Password
|
- Username / Password
|
||||||
- Multifactor authentication with OTP, U2F, Email OTP, SMS OTP
|
- Multifactor authentication with OTP, U2F, Email OTP, SMS OTP
|
||||||
- LDAP
|
- LDAP
|
||||||
|
- External enterprise identity providers and social logins
|
||||||
|
- [Device authorization](https://zitadel.com/docs/guides/solution-scenarios/device-authorization)
|
||||||
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://zitadel.com/docs/apis/openidoauth/endpoints)
|
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://zitadel.com/docs/apis/openidoauth/endpoints)
|
||||||
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://zitadel.com/docs/apis/saml/endpoints)
|
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://zitadel.com/docs/apis/saml/endpoints)
|
||||||
|
- [Custom sessions](https://zitadel.com/docs/guides/integrate/login-ui/username-password) if you need to go beyond OIDC or SAML
|
||||||
- [Machine-to-machine](https://zitadel.com/docs/guides/integrate/serviceusers) with JWT profile, Personal Access Tokens (PAT), and Client Credentials
|
- [Machine-to-machine](https://zitadel.com/docs/guides/integrate/serviceusers) with JWT profile, Personal Access Tokens (PAT), and Client Credentials
|
||||||
|
|
||||||
Multi-Tenancy
|
Multi-Tenancy
|
||||||
|
@@ -235,7 +235,6 @@ func startZitadel(config *Config, masterKey string, server chan<- *Server) error
|
|||||||
commands,
|
commands,
|
||||||
queries,
|
queries,
|
||||||
eventstoreClient,
|
eventstoreClient,
|
||||||
assets.AssetAPIFromDomain(config.ExternalSecure, config.ExternalPort),
|
|
||||||
config.Login.DefaultOTPEmailURLV2,
|
config.Login.DefaultOTPEmailURLV2,
|
||||||
config.SystemDefaults.Notifications.FileSystemPath,
|
config.SystemDefaults.Notifications.FileSystemPath,
|
||||||
keys.User,
|
keys.User,
|
||||||
@@ -311,6 +310,8 @@ func startAPIs(
|
|||||||
authZRepo,
|
authZRepo,
|
||||||
queries,
|
queries,
|
||||||
}
|
}
|
||||||
|
// always set the origin in the context if available in the http headers, no matter for what protocol
|
||||||
|
router.Use(middleware.OriginHandler)
|
||||||
verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers)
|
verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers)
|
||||||
tlsConfig, err := config.TLS.Config()
|
tlsConfig, err := config.TLS.Config()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -444,7 +445,6 @@ func startAPIs(
|
|||||||
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcProvider, config.ExternalSecure)); err != nil {
|
if err := apis.RegisterService(ctx, oidc_v2.CreateServer(commands, queries, oidcProvider, config.ExternalSecure)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes
|
// handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes
|
||||||
apis.RouteGRPC()
|
apis.RouteGRPC()
|
||||||
return nil
|
return nil
|
||||||
|
@@ -26,6 +26,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
name="hasUppercase"
|
name="hasUppercase"
|
||||||
ngDefaultControl
|
ngDefaultControl
|
||||||
|
data-e2e="notification-policy-checkbox"
|
||||||
[(ngModel)]="notificationData.passwordChange"
|
[(ngModel)]="notificationData.passwordChange"
|
||||||
[disabled]="(['policy.write'] | hasRole | async) === false"
|
[disabled]="(['policy.write'] | hasRole | async) === false"
|
||||||
>
|
>
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
|
data-e2e="save-notification-policy-button"
|
||||||
>
|
>
|
||||||
{{ 'ACTIONS.SAVE' | translate }}
|
{{ 'ACTIONS.SAVE' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,24 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { NotificationSettingsComponent } from './notification-settings.component';
|
|
||||||
|
|
||||||
describe('NotificationSettingsComponent', () => {
|
|
||||||
let component: NotificationSettingsComponent;
|
|
||||||
let fixture: ComponentFixture<NotificationSettingsComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
declarations: [NotificationSettingsComponent],
|
|
||||||
}).compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(NotificationSettingsComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -33,6 +33,7 @@
|
|||||||
class="ok-button"
|
class="ok-button"
|
||||||
color="primary"
|
color="primary"
|
||||||
(click)="closeDialogWithRequest()"
|
(click)="closeDialogWithRequest()"
|
||||||
|
data-e2e="save-sms-settings-button"
|
||||||
>
|
>
|
||||||
<span>{{ 'ACTIONS.SAVE' | translate }}</span>
|
<span>{{ 'ACTIONS.SAVE' | translate }}</span>
|
||||||
</button>
|
</button>
|
@@ -14,7 +14,6 @@ import {
|
|||||||
import { SMSProvider, TwilioConfig } from 'src/app/proto/generated/zitadel/settings_pb';
|
import { SMSProvider, TwilioConfig } from 'src/app/proto/generated/zitadel/settings_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';
|
||||||
|
|
||||||
import { PasswordDialogComponent } from '../password-dialog/password-dialog.component';
|
import { PasswordDialogComponent } from '../password-dialog/password-dialog.component';
|
||||||
|
|
||||||
enum SMSProviderType {
|
enum SMSProviderType {
|
@@ -0,0 +1,56 @@
|
|||||||
|
<h2>{{ 'SETTING.SMS.TITLE' | translate }}</h2>
|
||||||
|
|
||||||
|
<div class="spinner-wr">
|
||||||
|
<mat-spinner diameter="30" *ngIf="smsProvidersLoading" color="primary"></mat-spinner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sms-providers">
|
||||||
|
<cnsl-card class="sms-card" [nomargin]="true">
|
||||||
|
<div class="sms-provider">
|
||||||
|
<h4 class="title">Twilio</h4>
|
||||||
|
|
||||||
|
<span
|
||||||
|
*ngIf="twilio"
|
||||||
|
class="state"
|
||||||
|
[ngClass]="{
|
||||||
|
active: twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
|
||||||
|
inactive: twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
|
||||||
|
}"
|
||||||
|
>{{ 'SETTING.SMS.SMSPROVIDERSTATE.' + twilio.state | translate }}</span
|
||||||
|
>
|
||||||
|
|
||||||
|
<span class="fill-space"></span>
|
||||||
|
<button
|
||||||
|
*ngIf="twilio && twilio.id"
|
||||||
|
[disabled]="(['iam.write'] | hasRole | async) === false"
|
||||||
|
mat-stroked-button
|
||||||
|
(click)="toggleSMSProviderState(twilio.id)"
|
||||||
|
>
|
||||||
|
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE">{{
|
||||||
|
'ACTIONS.DEACTIVATE' | translate
|
||||||
|
}}</span>
|
||||||
|
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE">{{
|
||||||
|
'ACTIONS.ACTIVATE' | translate
|
||||||
|
}}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="twilio && twilio.id"
|
||||||
|
color="warn"
|
||||||
|
[disabled]="(['iam.write'] | hasRole | async) === false"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="removeSMSProvider(twilio.id)"
|
||||||
|
data-e2e="remove-sms-provider-button"
|
||||||
|
>
|
||||||
|
<i class="las la-trash"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
[disabled]="(['iam.write'] | hasRole | async) === false"
|
||||||
|
mat-icon-button
|
||||||
|
(click)="editSMSProvider()"
|
||||||
|
data-e2e="new-twilio-button"
|
||||||
|
>
|
||||||
|
<i class="las la-pen"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</cnsl-card>
|
||||||
|
</div>
|
@@ -2,35 +2,6 @@
|
|||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.smtp-form-field,
|
|
||||||
.info-section-warn {
|
|
||||||
max-width: 400px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section-warn {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.smtp-checkbox {
|
|
||||||
max-width: 400px;
|
|
||||||
display: block;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.set-password-btn {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.general-btn-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
.save-button {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sms-providers {
|
.sms-providers {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NotificationSMSProviderComponent } from './notification-sms-provider.component';
|
||||||
|
|
||||||
|
describe('NotificationSMSProviderComponent', () => {
|
||||||
|
let component: NotificationSMSProviderComponent;
|
||||||
|
let fixture: ComponentFixture<NotificationSMSProviderComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [NotificationSMSProviderComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NotificationSMSProviderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,140 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { AddSMSProviderTwilioRequest, UpdateSMSProviderTwilioRequest } from 'src/app/proto/generated/zitadel/admin_pb';
|
||||||
|
import { SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
|
||||||
|
|
||||||
|
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||||
|
import { AdminService } from 'src/app/services/admin.service';
|
||||||
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
|
import { InfoSectionType } from '../../info-section/info-section.component';
|
||||||
|
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
|
||||||
|
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
||||||
|
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'cnsl-notification-sms-provider',
|
||||||
|
templateUrl: './notification-sms-provider.component.html',
|
||||||
|
styleUrls: ['./notification-sms-provider.component.scss'],
|
||||||
|
})
|
||||||
|
export class NotificationSMSProviderComponent {
|
||||||
|
@Input() public serviceType!: PolicyComponentServiceType;
|
||||||
|
public smsProviders: SMSProvider.AsObject[] = [];
|
||||||
|
|
||||||
|
public smsProvidersLoading: boolean = false;
|
||||||
|
|
||||||
|
public SMSProviderConfigState: any = SMSProviderConfigState;
|
||||||
|
public InfoSectionType: any = InfoSectionType;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private service: AdminService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private toast: ToastService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private fetchData(): void {
|
||||||
|
this.smsProvidersLoading = true;
|
||||||
|
this.service
|
||||||
|
.listSMSProviders()
|
||||||
|
.then((smsProviders) => {
|
||||||
|
this.smsProvidersLoading = false;
|
||||||
|
if (smsProviders.resultList) {
|
||||||
|
this.smsProviders = smsProviders.resultList;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.smsProvidersLoading = false;
|
||||||
|
this.toast.showError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public editSMSProvider(): void {
|
||||||
|
const dialogRef = this.dialog.open(DialogAddSMSProviderComponent, {
|
||||||
|
width: '400px',
|
||||||
|
data: {
|
||||||
|
smsProviders: this.smsProviders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((req: AddSMSProviderTwilioRequest | UpdateSMSProviderTwilioRequest) => {
|
||||||
|
if (req) {
|
||||||
|
if (!!this.twilio) {
|
||||||
|
this.service
|
||||||
|
.updateSMSProviderTwilio(req as UpdateSMSProviderTwilioRequest)
|
||||||
|
.then(() => {
|
||||||
|
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
|
||||||
|
this.fetchData();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toast.showError(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.service
|
||||||
|
.addSMSProviderTwilio(req as AddSMSProviderTwilioRequest)
|
||||||
|
.then(() => {
|
||||||
|
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
|
||||||
|
this.fetchData();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toast.showError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleSMSProviderState(id: string): void {
|
||||||
|
const provider = this.smsProviders.find((p) => p.id === id);
|
||||||
|
if (provider) {
|
||||||
|
if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE) {
|
||||||
|
this.service
|
||||||
|
.deactivateSMSProvider(id)
|
||||||
|
.then(() => {
|
||||||
|
this.toast.showInfo('SETTING.SMS.DEACTIVATED', true);
|
||||||
|
this.fetchData();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toast.showError(error);
|
||||||
|
});
|
||||||
|
} else if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE) {
|
||||||
|
this.service
|
||||||
|
.activateSMSProvider(id)
|
||||||
|
.then(() => {
|
||||||
|
this.toast.showInfo('SETTING.SMS.ACTIVATED', true);
|
||||||
|
this.fetchData();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toast.showError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeSMSProvider(id: string): void {
|
||||||
|
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||||
|
data: {
|
||||||
|
confirmKey: 'ACTIONS.DELETE',
|
||||||
|
cancelKey: 'ACTIONS.CANCEL',
|
||||||
|
titleKey: 'SETTING.SMS.REMOVEPROVIDER',
|
||||||
|
descriptionKey: 'SETTING.SMS.REMOVEPROVIDER_DESC',
|
||||||
|
},
|
||||||
|
width: '400px',
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((resp) => {
|
||||||
|
if (resp) {
|
||||||
|
this.service
|
||||||
|
.removeSMSProvider(id)
|
||||||
|
.then(() => {
|
||||||
|
this.toast.showInfo('SETTING.SMS.TWILIO.REMOVED', true);
|
||||||
|
this.fetchData();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toast.showError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public get twilio(): SMSProvider.AsObject | undefined {
|
||||||
|
return this.smsProviders.find((p) => p.twilio);
|
||||||
|
}
|
||||||
|
}
|
@@ -15,11 +15,10 @@ import { InfoSectionModule } from '../../info-section/info-section.module';
|
|||||||
import { InputModule } from '../../input/input.module';
|
import { InputModule } from '../../input/input.module';
|
||||||
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
|
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
|
||||||
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
|
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
|
||||||
import { NotificationSettingsComponent } from './notification-settings.component';
|
import { NotificationSMSProviderComponent } from './notification-sms-provider.component';
|
||||||
import { PasswordDialogComponent } from './password-dialog/password-dialog.component';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [NotificationSettingsComponent, DialogAddSMSProviderComponent, PasswordDialogComponent],
|
declarations: [NotificationSMSProviderComponent, DialogAddSMSProviderComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CardModule,
|
CardModule,
|
||||||
@@ -38,6 +37,6 @@ import { PasswordDialogComponent } from './password-dialog/password-dialog.compo
|
|||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
],
|
],
|
||||||
exports: [NotificationSettingsComponent],
|
exports: [NotificationSMSProviderComponent],
|
||||||
})
|
})
|
||||||
export class NotificationSettingsModule {}
|
export class NotificationSMSProviderModule {}
|
@@ -4,7 +4,13 @@
|
|||||||
<div mat-dialog-content>
|
<div mat-dialog-content>
|
||||||
<cnsl-form-field class="formfield">
|
<cnsl-form-field class="formfield">
|
||||||
<cnsl-label>{{ data.i18nLabel | translate }}</cnsl-label>
|
<cnsl-label>{{ data.i18nLabel | translate }}</cnsl-label>
|
||||||
<input cnslInput [(ngModel)]="password" type="password" autocomplete="new-password" />
|
<input
|
||||||
|
cnslInput
|
||||||
|
[(ngModel)]="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
data-e2e="notification-setting-password"
|
||||||
|
/>
|
||||||
</cnsl-form-field>
|
</cnsl-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions class="action">
|
<div mat-dialog-actions class="action">
|
||||||
@@ -19,6 +25,7 @@
|
|||||||
mat-raised-button
|
mat-raised-button
|
||||||
class="ok-button"
|
class="ok-button"
|
||||||
(click)="closeDialog(password)"
|
(click)="closeDialog(password)"
|
||||||
|
data-e2e="save-notification-setting-password-button"
|
||||||
>
|
>
|
||||||
{{ 'ACTIONS.OK' | translate }}
|
{{ 'ACTIONS.OK' | translate }}
|
||||||
</button>
|
</button>
|
@@ -1,7 +1,7 @@
|
|||||||
<h2>{{ 'SETTING.SMTP.TITLE' | translate }}</h2>
|
<h2>{{ 'SETTING.SMTP.TITLE' | translate }}</h2>
|
||||||
|
|
||||||
<div class="spinner-wr">
|
<div class="spinner-wr">
|
||||||
<mat-spinner diameter="30" *ngIf="smtpLoading || smsProvidersLoading" color="primary"></mat-spinner>
|
<mat-spinner diameter="30" *ngIf="smtpLoading" color="primary"></mat-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<cnsl-info-section
|
<cnsl-info-section
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
(click)="setSMTPPassword()"
|
(click)="setSMTPPassword()"
|
||||||
type="button"
|
type="button"
|
||||||
mat-stroked-button
|
mat-stroked-button
|
||||||
|
data-e2e="add-smtp-password-button"
|
||||||
>
|
>
|
||||||
{{ 'SETTING.SMTP.SETPASSWORD' | translate }}
|
{{ 'SETTING.SMTP.SETPASSWORD' | translate }}
|
||||||
</button>
|
</button>
|
||||||
@@ -60,55 +61,9 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
mat-raised-button
|
mat-raised-button
|
||||||
|
data-e2e="save-smtp-settings-button"
|
||||||
>
|
>
|
||||||
{{ 'ACTIONS.SAVE' | translate }}
|
{{ 'ACTIONS.SAVE' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<br />
|
|
||||||
<h2>{{ 'SETTING.SMS.TITLE' | translate }}</h2>
|
|
||||||
<div class="sms-providers">
|
|
||||||
<cnsl-card class="sms-card" [nomargin]="true">
|
|
||||||
<div class="sms-provider">
|
|
||||||
<h4 class="title">Twilio</h4>
|
|
||||||
|
|
||||||
<span
|
|
||||||
*ngIf="twilio"
|
|
||||||
class="state"
|
|
||||||
[ngClass]="{
|
|
||||||
active: twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE,
|
|
||||||
inactive: twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE
|
|
||||||
}"
|
|
||||||
>{{ 'SETTING.SMS.SMSPROVIDERSTATE.' + twilio.state | translate }}</span
|
|
||||||
>
|
|
||||||
|
|
||||||
<span class="fill-space"></span>
|
|
||||||
<button
|
|
||||||
*ngIf="twilio && twilio.id"
|
|
||||||
[disabled]="(['iam.write'] | hasRole | async) === false"
|
|
||||||
mat-stroked-button
|
|
||||||
(click)="toggleSMSProviderState(twilio.id)"
|
|
||||||
>
|
|
||||||
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE">{{
|
|
||||||
'ACTIONS.DEACTIVATE' | translate
|
|
||||||
}}</span>
|
|
||||||
<span *ngIf="twilio.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE">{{
|
|
||||||
'ACTIONS.ACTIVATE' | translate
|
|
||||||
}}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
*ngIf="twilio && twilio.id"
|
|
||||||
color="warn"
|
|
||||||
[disabled]="(['iam.write'] | hasRole | async) === false"
|
|
||||||
mat-icon-button
|
|
||||||
(click)="removeSMSProvider(twilio.id)"
|
|
||||||
>
|
|
||||||
<i class="las la-trash"></i>
|
|
||||||
</button>
|
|
||||||
<button [disabled]="(['iam.write'] | hasRole | async) === false" mat-icon-button (click)="editSMSProvider()">
|
|
||||||
<i class="las la-pen"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</cnsl-card>
|
|
||||||
</div>
|
|
@@ -0,0 +1,32 @@
|
|||||||
|
.spinner-wr {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smtp-form-field,
|
||||||
|
.info-section-warn {
|
||||||
|
max-width: 400px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section-warn {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smtp-checkbox {
|
||||||
|
max-width: 400px;
|
||||||
|
display: block;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-password-btn {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-btn-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,24 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NotificationSMTPProviderComponent } from './notification-smtp-provider.component';
|
||||||
|
|
||||||
|
describe('NotificationSMTPProviderComponent', () => {
|
||||||
|
let component: NotificationSMTPProviderComponent;
|
||||||
|
let fixture: ComponentFixture<NotificationSMTPProviderComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [NotificationSMTPProviderComponent],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NotificationSMTPProviderComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -3,45 +3,33 @@ import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/
|
|||||||
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
|
||||||
import { take } from 'rxjs';
|
import { take } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
AddSMSProviderTwilioRequest,
|
|
||||||
AddSMTPConfigRequest,
|
AddSMTPConfigRequest,
|
||||||
AddSMTPConfigResponse,
|
AddSMTPConfigResponse,
|
||||||
UpdateSMSProviderTwilioRequest,
|
|
||||||
UpdateSMTPConfigPasswordRequest,
|
UpdateSMTPConfigPasswordRequest,
|
||||||
UpdateSMTPConfigRequest,
|
UpdateSMTPConfigRequest,
|
||||||
UpdateSMTPConfigResponse,
|
UpdateSMTPConfigResponse,
|
||||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||||
import { DebugNotificationProvider, SMSProvider, SMSProviderConfigState } from 'src/app/proto/generated/zitadel/settings_pb';
|
|
||||||
import { AdminService } from 'src/app/services/admin.service';
|
import { AdminService } from 'src/app/services/admin.service';
|
||||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||||
import { ToastService } from 'src/app/services/toast.service';
|
import { ToastService } from 'src/app/services/toast.service';
|
||||||
import { requiredValidator } from '../../form-field/validators/validators';
|
import { requiredValidator } from '../../form-field/validators/validators';
|
||||||
|
|
||||||
import { InfoSectionType } from '../../info-section/info-section.component';
|
import { InfoSectionType } from '../../info-section/info-section.component';
|
||||||
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
|
import { PasswordDialogComponent } from '../notification-sms-provider/password-dialog/password-dialog.component';
|
||||||
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
||||||
import { DialogAddSMSProviderComponent } from './dialog-add-sms-provider/dialog-add-sms-provider.component';
|
|
||||||
import { PasswordDialogComponent } from './password-dialog/password-dialog.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'cnsl-notification-settings',
|
selector: 'cnsl-notification-smtp-provider',
|
||||||
templateUrl: './notification-settings.component.html',
|
templateUrl: './notification-smtp-provider.component.html',
|
||||||
styleUrls: ['./notification-settings.component.scss'],
|
styleUrls: ['./notification-smtp-provider.component.scss'],
|
||||||
})
|
})
|
||||||
export class NotificationSettingsComponent implements OnInit {
|
export class NotificationSMTPProviderComponent implements OnInit {
|
||||||
@Input() public serviceType!: PolicyComponentServiceType;
|
@Input() public serviceType!: PolicyComponentServiceType;
|
||||||
public smsProviders: SMSProvider.AsObject[] = [];
|
|
||||||
public logNotificationProvider!: DebugNotificationProvider.AsObject;
|
|
||||||
public fileNotificationProvider!: DebugNotificationProvider.AsObject;
|
|
||||||
|
|
||||||
public smtpLoading: boolean = false;
|
public smtpLoading: boolean = false;
|
||||||
public smsProvidersLoading: boolean = false;
|
|
||||||
public logProviderLoading: boolean = false;
|
|
||||||
public fileProviderLoading: boolean = false;
|
|
||||||
|
|
||||||
public form!: UntypedFormGroup;
|
public form!: UntypedFormGroup;
|
||||||
|
|
||||||
public SMSProviderConfigState: any = SMSProviderConfigState;
|
|
||||||
public InfoSectionType: any = InfoSectionType;
|
public InfoSectionType: any = InfoSectionType;
|
||||||
|
|
||||||
public hasSMTPConfig: boolean = false;
|
public hasSMTPConfig: boolean = false;
|
||||||
@@ -96,46 +84,6 @@ export class NotificationSettingsComponent implements OnInit {
|
|||||||
this.hasSMTPConfig = false;
|
this.hasSMTPConfig = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smsProvidersLoading = true;
|
|
||||||
this.service
|
|
||||||
.listSMSProviders()
|
|
||||||
.then((smsProviders) => {
|
|
||||||
this.smsProvidersLoading = false;
|
|
||||||
if (smsProviders.resultList) {
|
|
||||||
this.smsProviders = smsProviders.resultList;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.smsProvidersLoading = false;
|
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logProviderLoading = true;
|
|
||||||
this.service
|
|
||||||
.getLogNotificationProvider()
|
|
||||||
.then((logNotificationProvider) => {
|
|
||||||
this.logProviderLoading = false;
|
|
||||||
if (logNotificationProvider.provider) {
|
|
||||||
this.logNotificationProvider = logNotificationProvider.provider;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.logProviderLoading = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fileProviderLoading = true;
|
|
||||||
this.service
|
|
||||||
.getFileSystemNotificationProvider()
|
|
||||||
.then((fileNotificationProvider) => {
|
|
||||||
this.fileProviderLoading = false;
|
|
||||||
if (fileNotificationProvider.provider) {
|
|
||||||
this.fileNotificationProvider = fileNotificationProvider.provider;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.fileProviderLoading = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateData(): Promise<UpdateSMTPConfigResponse.AsObject | AddSMTPConfigResponse> {
|
private updateData(): Promise<UpdateSMTPConfigResponse.AsObject | AddSMTPConfigResponse> {
|
||||||
@@ -175,41 +123,6 @@ export class NotificationSettingsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public editSMSProvider(): void {
|
|
||||||
const dialogRef = this.dialog.open(DialogAddSMSProviderComponent, {
|
|
||||||
width: '400px',
|
|
||||||
data: {
|
|
||||||
smsProviders: this.smsProviders,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((req: AddSMSProviderTwilioRequest | UpdateSMSProviderTwilioRequest) => {
|
|
||||||
if (req) {
|
|
||||||
if (!!this.twilio) {
|
|
||||||
this.service
|
|
||||||
.updateSMSProviderTwilio(req as UpdateSMSProviderTwilioRequest)
|
|
||||||
.then(() => {
|
|
||||||
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
|
|
||||||
this.fetchData();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.service
|
|
||||||
.addSMSProviderTwilio(req as AddSMSProviderTwilioRequest)
|
|
||||||
.then(() => {
|
|
||||||
this.toast.showInfo('SETTING.SMS.TWILIO.ADDED', true);
|
|
||||||
this.fetchData();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setSMTPPassword(): void {
|
public setSMTPPassword(): void {
|
||||||
const dialogRef = this.dialog.open(PasswordDialogComponent, {
|
const dialogRef = this.dialog.open(PasswordDialogComponent, {
|
||||||
width: '400px',
|
width: '400px',
|
||||||
@@ -236,63 +149,6 @@ export class NotificationSettingsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleSMSProviderState(id: string): void {
|
|
||||||
const provider = this.smsProviders.find((p) => p.id === id);
|
|
||||||
if (provider) {
|
|
||||||
if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_ACTIVE) {
|
|
||||||
this.service
|
|
||||||
.deactivateSMSProvider(id)
|
|
||||||
.then(() => {
|
|
||||||
this.toast.showInfo('SETTING.SMS.DEACTIVATED', true);
|
|
||||||
this.fetchData();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
} else if (provider.state === SMSProviderConfigState.SMS_PROVIDER_CONFIG_INACTIVE) {
|
|
||||||
this.service
|
|
||||||
.activateSMSProvider(id)
|
|
||||||
.then(() => {
|
|
||||||
this.toast.showInfo('SETTING.SMS.ACTIVATED', true);
|
|
||||||
this.fetchData();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeSMSProvider(id: string): void {
|
|
||||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
|
||||||
data: {
|
|
||||||
confirmKey: 'ACTIONS.DELETE',
|
|
||||||
cancelKey: 'ACTIONS.CANCEL',
|
|
||||||
titleKey: 'SETTING.SMS.REMOVEPROVIDER',
|
|
||||||
descriptionKey: 'SETTING.SMS.REMOVEPROVIDER_DESC',
|
|
||||||
},
|
|
||||||
width: '400px',
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((resp) => {
|
|
||||||
if (resp) {
|
|
||||||
this.service
|
|
||||||
.removeSMSProvider(id)
|
|
||||||
.then(() => {
|
|
||||||
this.toast.showInfo('SETTING.SMS.TWILIO.REMOVED', true);
|
|
||||||
this.fetchData();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.toast.showError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public get twilio(): SMSProvider.AsObject | undefined {
|
|
||||||
return this.smsProviders.find((p) => p.twilio);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get senderAddress(): AbstractControl | null {
|
public get senderAddress(): AbstractControl | null {
|
||||||
return this.form.get('senderAddress');
|
return this.form.get('senderAddress');
|
||||||
}
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
|
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
|
||||||
|
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
|
||||||
|
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
|
||||||
|
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
|
||||||
|
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 { FormFieldModule } from '../../form-field/form-field.module';
|
||||||
|
import { InfoSectionModule } from '../../info-section/info-section.module';
|
||||||
|
import { InputModule } from '../../input/input.module';
|
||||||
|
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
|
||||||
|
import { PasswordDialogComponent } from '../notification-sms-provider/password-dialog/password-dialog.component';
|
||||||
|
import { NotificationSMTPProviderComponent } from './notification-smtp-provider.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [NotificationSMTPProviderComponent, PasswordDialogComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
CardModule,
|
||||||
|
InfoSectionModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
HasRolePipeModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
InputModule,
|
||||||
|
MatIconModule,
|
||||||
|
FormFieldModule,
|
||||||
|
WarnDialogModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatSelectModule,
|
||||||
|
TranslateModule,
|
||||||
|
],
|
||||||
|
exports: [NotificationSMTPProviderComponent],
|
||||||
|
})
|
||||||
|
export class NotificationSMTPProviderModule {}
|
@@ -51,7 +51,7 @@ export const NOTIFICATION_GROUP: SettingLinks = {
|
|||||||
i18nTitle: 'SETTINGS.GROUPS.NOTIFICATIONS',
|
i18nTitle: 'SETTINGS.GROUPS.NOTIFICATIONS',
|
||||||
i18nDesc: 'SETTINGS.LIST.NOTIFICATIONS_DESC',
|
i18nDesc: 'SETTINGS.LIST.NOTIFICATIONS_DESC',
|
||||||
iamRouterLink: ['/settings'],
|
iamRouterLink: ['/settings'],
|
||||||
queryParams: { id: 'notifications' },
|
queryParams: { id: 'smtpprovider' },
|
||||||
iamWithRole: ['iam.policy.read'],
|
iamWithRole: ['iam.policy.read'],
|
||||||
icon: 'las la-bell',
|
icon: 'las la-bell',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
|
@@ -27,19 +27,18 @@
|
|||||||
<ng-container *ngIf="currentSetting === 'idp'">
|
<ng-container *ngIf="currentSetting === 'idp'">
|
||||||
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
|
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="currentSetting === 'notifications' && serviceType === PolicyComponentServiceType.ADMIN">
|
<ng-container *ngIf="currentSetting === 'notifications'">
|
||||||
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy>
|
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy>
|
||||||
<cnsl-notification-settings [serviceType]="serviceType"></cnsl-notification-settings>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *ngIf="currentSetting === 'smtpprovider' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||||
<ng-container *ngIf="currentSetting === 'notifications' && serviceType === PolicyComponentServiceType.MGMT">
|
<cnsl-notification-smtp-provider [serviceType]="serviceType"></cnsl-notification-smtp-provider>
|
||||||
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy
|
</ng-container>
|
||||||
></ng-container>
|
<ng-container *ngIf="currentSetting === 'smsprovider' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||||
|
<cnsl-notification-sms-provider [serviceType]="serviceType"></cnsl-notification-sms-provider>
|
||||||
|
</ng-container>
|
||||||
<ng-container *ngIf="currentSetting === 'oidc' && serviceType === PolicyComponentServiceType.ADMIN">
|
<ng-container *ngIf="currentSetting === 'oidc' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||||
<cnsl-oidc-configuration></cnsl-oidc-configuration>
|
<cnsl-oidc-configuration></cnsl-oidc-configuration>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="currentSetting === 'secrets' && serviceType === PolicyComponentServiceType.ADMIN">
|
<ng-container *ngIf="currentSetting === 'secrets' && serviceType === PolicyComponentServiceType.ADMIN">
|
||||||
<cnsl-secret-generator></cnsl-secret-generator>
|
<cnsl-secret-generator></cnsl-secret-generator>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -13,7 +13,8 @@ import { LoginPolicyModule } from '../policies/login-policy/login-policy.module'
|
|||||||
import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module';
|
import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module';
|
||||||
import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
|
import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
|
||||||
import { NotificationPolicyModule } from '../policies/notification-policy/notification-policy.module';
|
import { NotificationPolicyModule } from '../policies/notification-policy/notification-policy.module';
|
||||||
import { NotificationSettingsModule } from '../policies/notification-settings/notification-settings.module';
|
import { NotificationSMSProviderModule } from '../policies/notification-sms-provider/notification-sms-provider.module';
|
||||||
|
import { NotificationSMTPProviderModule } from '../policies/notification-smtp-provider/notification-smtp-provider.module';
|
||||||
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module';
|
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module';
|
||||||
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
|
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
|
||||||
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
|
import { PasswordLockoutPolicyModule } from '../policies/password-lockout-policy/password-lockout-policy.module';
|
||||||
@@ -46,7 +47,8 @@ import { SettingsListComponent } from './settings-list.component';
|
|||||||
DomainPolicyModule,
|
DomainPolicyModule,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
HasRolePipeModule,
|
HasRolePipeModule,
|
||||||
NotificationSettingsModule,
|
NotificationSMTPProviderModule,
|
||||||
|
NotificationSMSProviderModule,
|
||||||
OIDCConfigurationModule,
|
OIDCConfigurationModule,
|
||||||
SecretGeneratorModule,
|
SecretGeneratorModule,
|
||||||
],
|
],
|
||||||
|
@@ -98,15 +98,25 @@ export const NOTIFICATIONS: SidenavSetting = {
|
|||||||
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
|
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
|
||||||
requiredRoles: {
|
requiredRoles: {
|
||||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||||
|
[PolicyComponentServiceType.MGMT]: ['policy.read'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NOTIFICATION_POLICY: SidenavSetting = {
|
export const SMTP_PROVIDER: SidenavSetting = {
|
||||||
id: 'notifications',
|
id: 'smtpprovider',
|
||||||
i18nKey: 'SETTINGS.LIST.NOTIFICATIONS',
|
i18nKey: 'SETTINGS.LIST.SMTP_PROVIDER',
|
||||||
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
|
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
|
||||||
requiredRoles: {
|
requiredRoles: {
|
||||||
[PolicyComponentServiceType.MGMT]: ['policy.read'],
|
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SMS_PROVIDER: SidenavSetting = {
|
||||||
|
id: 'smsprovider',
|
||||||
|
i18nKey: 'SETTINGS.LIST.SMS_PROVIDER',
|
||||||
|
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
|
||||||
|
requiredRoles: {
|
||||||
|
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -21,6 +21,8 @@ import {
|
|||||||
PRIVACYPOLICY,
|
PRIVACYPOLICY,
|
||||||
SECRETS,
|
SECRETS,
|
||||||
SECURITY,
|
SECURITY,
|
||||||
|
SMS_PROVIDER,
|
||||||
|
SMTP_PROVIDER,
|
||||||
} from '../../modules/settings-list/settings';
|
} from '../../modules/settings-list/settings';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -36,6 +38,8 @@ export class InstanceSettingsComponent implements OnInit, OnDestroy {
|
|||||||
// notifications
|
// notifications
|
||||||
// { showWarn: true, ...NOTIFICATIONS },
|
// { showWarn: true, ...NOTIFICATIONS },
|
||||||
NOTIFICATIONS,
|
NOTIFICATIONS,
|
||||||
|
SMTP_PROVIDER,
|
||||||
|
SMS_PROVIDER,
|
||||||
// login
|
// login
|
||||||
LOGIN,
|
LOGIN,
|
||||||
IDP,
|
IDP,
|
||||||
@@ -80,7 +84,10 @@ export class InstanceSettingsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.settingsList = this.authService.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin);
|
this.settingsList = this.authService.isAllowedMapper(
|
||||||
|
this.defaultSettingsList,
|
||||||
|
(setting) => setting.requiredRoles.admin || [],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
@@ -15,7 +15,7 @@ import {
|
|||||||
LOGIN,
|
LOGIN,
|
||||||
LOGINTEXTS,
|
LOGINTEXTS,
|
||||||
MESSAGETEXTS,
|
MESSAGETEXTS,
|
||||||
NOTIFICATION_POLICY,
|
NOTIFICATIONS,
|
||||||
PRIVACYPOLICY,
|
PRIVACYPOLICY,
|
||||||
VERIFIED_DOMAINS,
|
VERIFIED_DOMAINS,
|
||||||
} from '../../modules/settings-list/settings';
|
} from '../../modules/settings-list/settings';
|
||||||
@@ -34,7 +34,7 @@ export class OrgSettingsComponent implements OnInit {
|
|||||||
IDP,
|
IDP,
|
||||||
COMPLEXITY,
|
COMPLEXITY,
|
||||||
LOCKOUT,
|
LOCKOUT,
|
||||||
NOTIFICATION_POLICY,
|
NOTIFICATIONS,
|
||||||
VERIFIED_DOMAINS,
|
VERIFIED_DOMAINS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
BRANDING,
|
BRANDING,
|
||||||
@@ -68,7 +68,7 @@ export class OrgSettingsComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.settingsList = this.authService
|
this.settingsList = this.authService
|
||||||
.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.mgmt)
|
.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.mgmt || [])
|
||||||
.pipe(take(1));
|
.pipe(take(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -71,7 +71,7 @@ export const ONBOARDING_EVENTS: OnboardingActions[] = [
|
|||||||
eventType: 'instance.smtp.config.added',
|
eventType: 'instance.smtp.config.added',
|
||||||
oneof: ['instance.smtp.config.added', 'instance.smtp.config.changed'],
|
oneof: ['instance.smtp.config.added', 'instance.smtp.config.changed'],
|
||||||
link: ['/settings'],
|
link: ['/settings'],
|
||||||
fragment: 'notifications',
|
fragment: 'smtpprovider',
|
||||||
iconClasses: 'las la-envelope',
|
iconClasses: 'las la-envelope',
|
||||||
darkcolor: yellowdark,
|
darkcolor: yellowdark,
|
||||||
lightcolor: yellowlight,
|
lightcolor: yellowlight,
|
||||||
|
@@ -1013,6 +1013,8 @@
|
|||||||
"LOCKOUT": "Блокиране",
|
"LOCKOUT": "Блокиране",
|
||||||
"COMPLEXITY": "Сложност на паролата",
|
"COMPLEXITY": "Сложност на паролата",
|
||||||
"NOTIFICATIONS": "Настройки за известията",
|
"NOTIFICATIONS": "Настройки за известията",
|
||||||
|
"SMTP_PROVIDER": "SMTP доставчик",
|
||||||
|
"SMS_PROVIDER": "Доставчик на SMS/телефон",
|
||||||
"NOTIFICATIONS_DESC": "Настройки за SMTP и SMS",
|
"NOTIFICATIONS_DESC": "Настройки за SMTP и SMS",
|
||||||
"MESSAGETEXTS": "Текстове на съобщения",
|
"MESSAGETEXTS": "Текстове на съобщения",
|
||||||
"IDP": "Доставчици на идентичност",
|
"IDP": "Доставчици на идентичност",
|
||||||
|
@@ -1019,6 +1019,8 @@
|
|||||||
"LOCKOUT": "Sperrmechanismen",
|
"LOCKOUT": "Sperrmechanismen",
|
||||||
"COMPLEXITY": "Passwordkomplexität",
|
"COMPLEXITY": "Passwordkomplexität",
|
||||||
"NOTIFICATIONS": "Benachrichtigungseinstellungen",
|
"NOTIFICATIONS": "Benachrichtigungseinstellungen",
|
||||||
|
"SMTP_PROVIDER": "SMTP-Anbieter",
|
||||||
|
"SMS_PROVIDER": "SMS / Telefon Anbieter",
|
||||||
"NOTIFICATIONS_DESC": "SMTP und SMS Einstellungen",
|
"NOTIFICATIONS_DESC": "SMTP und SMS Einstellungen",
|
||||||
"MESSAGETEXTS": "Benachrichtigungstexte",
|
"MESSAGETEXTS": "Benachrichtigungstexte",
|
||||||
"IDP": "Identitätsanbieter",
|
"IDP": "Identitätsanbieter",
|
||||||
|
@@ -1019,7 +1019,9 @@
|
|||||||
"LOGIN": "Login Behavior and Security",
|
"LOGIN": "Login Behavior and Security",
|
||||||
"LOCKOUT": "Lockout",
|
"LOCKOUT": "Lockout",
|
||||||
"COMPLEXITY": "Password complexity",
|
"COMPLEXITY": "Password complexity",
|
||||||
"NOTIFICATIONS": "Notification settings",
|
"NOTIFICATIONS": "Notifications",
|
||||||
|
"SMTP_PROVIDER": "SMTP Provider",
|
||||||
|
"SMS_PROVIDER": "SMS/Phone Provider",
|
||||||
"NOTIFICATIONS_DESC": "SMTP and SMS Settings",
|
"NOTIFICATIONS_DESC": "SMTP and SMS Settings",
|
||||||
"MESSAGETEXTS": "Message Texts",
|
"MESSAGETEXTS": "Message Texts",
|
||||||
"IDP": "Identity Providers",
|
"IDP": "Identity Providers",
|
||||||
|
@@ -1020,6 +1020,8 @@
|
|||||||
"LOCKOUT": "Bloqueo",
|
"LOCKOUT": "Bloqueo",
|
||||||
"COMPLEXITY": "Complejidad de contraseña",
|
"COMPLEXITY": "Complejidad de contraseña",
|
||||||
"NOTIFICATIONS": "Ajustes de notificación",
|
"NOTIFICATIONS": "Ajustes de notificación",
|
||||||
|
"SMTP_PROVIDER": "Proveedor SMTP",
|
||||||
|
"SMS_PROVIDER": "Proveedor SMS/Teléfono",
|
||||||
"NOTIFICATIONS_DESC": "Ajustes SMTP y SMS",
|
"NOTIFICATIONS_DESC": "Ajustes SMTP y SMS",
|
||||||
"MESSAGETEXTS": "Mensajes de texto",
|
"MESSAGETEXTS": "Mensajes de texto",
|
||||||
"IDP": "Proveedores de identidad",
|
"IDP": "Proveedores de identidad",
|
||||||
|
@@ -1019,6 +1019,8 @@
|
|||||||
"LOCKOUT": "Verrouillage",
|
"LOCKOUT": "Verrouillage",
|
||||||
"COMPLEXITY": "Complexité du mot de passe",
|
"COMPLEXITY": "Complexité du mot de passe",
|
||||||
"NOTIFICATIONS": "Paramètres de notification",
|
"NOTIFICATIONS": "Paramètres de notification",
|
||||||
|
"SMTP_PROVIDER": "Fournisseur SMTP",
|
||||||
|
"SMS_PROVIDER": "SMS/Téléphone Fournisseur",
|
||||||
"NOTIFICATIONS_DESC": "Paramètres SMTP et SMS",
|
"NOTIFICATIONS_DESC": "Paramètres SMTP et SMS",
|
||||||
"MESSAGETEXTS": "Textes des messages",
|
"MESSAGETEXTS": "Textes des messages",
|
||||||
"IDP": "Fournisseurs d'identité",
|
"IDP": "Fournisseurs d'identité",
|
||||||
|
@@ -1019,6 +1019,8 @@
|
|||||||
"LOCKOUT": "Meccanismi di bloccaggio",
|
"LOCKOUT": "Meccanismi di bloccaggio",
|
||||||
"COMPLEXITY": "Complessità della password",
|
"COMPLEXITY": "Complessità della password",
|
||||||
"NOTIFICATIONS": "Impostazioni di notifica",
|
"NOTIFICATIONS": "Impostazioni di notifica",
|
||||||
|
"SMTP_PROVIDER": "Fornitore SMTP",
|
||||||
|
"SMS_PROVIDER": "Fornitore di servizi SMS/telefonici",
|
||||||
"NOTIFICATIONS_DESC": "Impostazioni SMTP e SMS",
|
"NOTIFICATIONS_DESC": "Impostazioni SMTP e SMS",
|
||||||
"MESSAGETEXTS": "Testi di notifica",
|
"MESSAGETEXTS": "Testi di notifica",
|
||||||
"IDP": "Fornitori di identità",
|
"IDP": "Fornitori di identità",
|
||||||
|
@@ -1020,6 +1020,8 @@
|
|||||||
"LOCKOUT": "ロックアウト",
|
"LOCKOUT": "ロックアウト",
|
||||||
"COMPLEXITY": "パスワードの複雑さ",
|
"COMPLEXITY": "パスワードの複雑さ",
|
||||||
"NOTIFICATIONS": "通知設定",
|
"NOTIFICATIONS": "通知設定",
|
||||||
|
"SMTP_PROVIDER": "SMTPプロバイダー",
|
||||||
|
"SMS_PROVIDER": "SMS/電話プロバイダー",
|
||||||
"NOTIFICATIONS_DESC": "SMTPおよびSMS設定",
|
"NOTIFICATIONS_DESC": "SMTPおよびSMS設定",
|
||||||
"MESSAGETEXTS": "メッセージテキスト",
|
"MESSAGETEXTS": "メッセージテキスト",
|
||||||
"IDP": "IDプロバイダー",
|
"IDP": "IDプロバイダー",
|
||||||
|
@@ -1021,6 +1021,8 @@
|
|||||||
"LOCKOUT": "Забрана на пристап",
|
"LOCKOUT": "Забрана на пристап",
|
||||||
"COMPLEXITY": "Сложеност на лозинката",
|
"COMPLEXITY": "Сложеност на лозинката",
|
||||||
"NOTIFICATIONS": "Подесувања за известувања",
|
"NOTIFICATIONS": "Подесувања за известувања",
|
||||||
|
"SMTP_PROVIDER": "SMTP провајдер",
|
||||||
|
"SMS_PROVIDER": "СМС/Провајдер на телефон",
|
||||||
"NOTIFICATIONS_DESC": "Подесувања за SMTP и SMS",
|
"NOTIFICATIONS_DESC": "Подесувања за SMTP и SMS",
|
||||||
"MESSAGETEXTS": "Текстови на пораки",
|
"MESSAGETEXTS": "Текстови на пораки",
|
||||||
"IDP": "Identity Providers",
|
"IDP": "Identity Providers",
|
||||||
|
@@ -1019,6 +1019,8 @@
|
|||||||
"LOCKOUT": "Blokada",
|
"LOCKOUT": "Blokada",
|
||||||
"COMPLEXITY": "Złożoność hasła",
|
"COMPLEXITY": "Złożoność hasła",
|
||||||
"NOTIFICATIONS": "Ustawienia powiadomień",
|
"NOTIFICATIONS": "Ustawienia powiadomień",
|
||||||
|
"SMTP_PROVIDER": "Dostawca SMTP",
|
||||||
|
"SMS_PROVIDER": "Dostawca SMS-ów/telefonów",
|
||||||
"NOTIFICATIONS_DESC": "Ustawienia SMTP i SMS",
|
"NOTIFICATIONS_DESC": "Ustawienia SMTP i SMS",
|
||||||
"MESSAGETEXTS": "Teksty wiadomości",
|
"MESSAGETEXTS": "Teksty wiadomości",
|
||||||
"IDP": "Dostawcy tożsamości",
|
"IDP": "Dostawcy tożsamości",
|
||||||
|
@@ -1021,6 +1021,8 @@
|
|||||||
"LOCKOUT": "Bloqueio",
|
"LOCKOUT": "Bloqueio",
|
||||||
"COMPLEXITY": "Complexidade de Senha",
|
"COMPLEXITY": "Complexidade de Senha",
|
||||||
"NOTIFICATIONS": "Configurações de Notificação",
|
"NOTIFICATIONS": "Configurações de Notificação",
|
||||||
|
"SMTP_PROVIDER": "Provedor SMTP",
|
||||||
|
"SMS_PROVIDER": "Provedor de SMS/Telefone",
|
||||||
"NOTIFICATIONS_DESC": "Configurações de SMTP e SMS",
|
"NOTIFICATIONS_DESC": "Configurações de SMTP e SMS",
|
||||||
"MESSAGETEXTS": "Textos de Mensagem",
|
"MESSAGETEXTS": "Textos de Mensagem",
|
||||||
"IDP": "Provedores de Identidade",
|
"IDP": "Provedores de Identidade",
|
||||||
|
@@ -1019,6 +1019,8 @@
|
|||||||
"LOCKOUT": "安全锁策略",
|
"LOCKOUT": "安全锁策略",
|
||||||
"COMPLEXITY": "密码复杂性",
|
"COMPLEXITY": "密码复杂性",
|
||||||
"NOTIFICATIONS": "通知设置",
|
"NOTIFICATIONS": "通知设置",
|
||||||
|
"SMTP_PROVIDER": "SMTP 提供商",
|
||||||
|
"SMS_PROVIDER": "短信/电话提供商",
|
||||||
"NOTIFICATIONS_DESC": "SMTP 和 SMS 设置",
|
"NOTIFICATIONS_DESC": "SMTP 和 SMS 设置",
|
||||||
"MESSAGETEXTS": "消息文本",
|
"MESSAGETEXTS": "消息文本",
|
||||||
"IDP": "身份提供者",
|
"IDP": "身份提供者",
|
||||||
|
@@ -9,7 +9,7 @@ import OrgDescription from "../../../concepts/structure/_org_description.mdx";
|
|||||||
import Column from "../../../../src/components/column";
|
import Column from "../../../../src/components/column";
|
||||||
|
|
||||||
An Organization is where your projects and users live. Looking at a B2B use case, an organization represents a business partner who typically has its own branding and has different access settings like additional federated login providers.
|
An Organization is where your projects and users live. Looking at a B2B use case, an organization represents a business partner who typically has its own branding and has different access settings like additional federated login providers.
|
||||||
Users from one organization are seperated from others.
|
Users from one organization are separated from others.
|
||||||
|
|
||||||
## Create a new organization
|
## Create a new organization
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ If you choose your logged in user as organization manager, a membership for the
|
|||||||
alt="Select Organization"
|
alt="Select Organization"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
If you want to enable your customers to create their organization by themselves, we provide a creation form for a organization. `<https://$CUSTOM-DOMAIN/ui/login/register/org`
|
If you want to enable your customers to create their organization by themselves, we provide a creation form for an organization. `<https://$CUSTOM-DOMAIN/ui/login/register/org`
|
||||||
The customer needs to fill in the form with the organization name and the contact details.
|
The customer needs to fill in the form with the organization name and the contact details.
|
||||||
|
|
||||||
<img
|
<img
|
||||||
@@ -34,8 +34,8 @@ The customer needs to fill in the form with the organization name and the contac
|
|||||||
|
|
||||||
## How ZITADEL handles usernames
|
## How ZITADEL handles usernames
|
||||||
|
|
||||||
If you domain setting "user loginname must contain orgdomain" is disabled. Your username will be unique withing the whole instance.
|
If your domain setting "user loginname must contain orgdomain" is disabled, your username will be unique within the whole instance.
|
||||||
At the moment the username only allowes e-mail formatted input. (This will be changed soon)
|
At the moment the username only allows e-mail formatted input. (This will be changed soon)
|
||||||
|
|
||||||
### User Loginname must contain orgdomain
|
### User Loginname must contain orgdomain
|
||||||
|
|
||||||
@@ -46,9 +46,9 @@ Those loginnames consist of the format `{username}@{domainname}.{zitadeldomain}`
|
|||||||
If your user had the username `john.doe`, the generated loginname would be `john.doe@acme.zitadel.cloud`.
|
If your user had the username `john.doe`, the generated loginname would be `john.doe@acme.zitadel.cloud`.
|
||||||
This also means that only one user with the username `john.doe` can exist in your organization called `ACME`.
|
This also means that only one user with the username `john.doe` can exist in your organization called `ACME`.
|
||||||
|
|
||||||
If you verify your domain name or add additional domains, ZITADEL will generate those additional logonames for you.
|
If you verify your domain name or add additional domains, ZITADEL will generate those additional login names for you.
|
||||||
If the organization would own the domain `acme.ch` and verify it, then the resulting loginname would be `john.doe@acme.ch` in addition to the already generated `john.doe@acme.zitadel.cloud`.
|
If the organization would own the domain `acme.ch` and verify it, then the resulting loginname would be `john.doe@acme.ch` in addition to the already generated `john.doe@acme.zitadel.cloud`.
|
||||||
The user can now use either logonname to authenticate with your application.
|
The user can now use either login name to authenticate with your application.
|
||||||
|
|
||||||
> Note: You can set this setting on your instance as well as your organizations. All available usernames are shown on the top of the user pages.
|
> Note: You can set this setting on your instance as well as your organizations. All available usernames are shown on the top of the user pages.
|
||||||
|
|
||||||
@@ -59,12 +59,14 @@ Users that you create within your organization will be suffixed with this domain
|
|||||||
|
|
||||||
You can improve the user experience, by suffixing users with a domain name that is in your control.
|
You can improve the user experience, by suffixing users with a domain name that is in your control.
|
||||||
If the "validate org domains" settings in the [Domain Settings](./instance-settings#domain-settings) is set to true, you have to prove the ownership of your domain, by DNS or HTTP challenge.
|
If the "validate org domains" settings in the [Domain Settings](./instance-settings#domain-settings) is set to true, you have to prove the ownership of your domain, by DNS or HTTP challenge.
|
||||||
If the settings is set to false, the created domain will automatically be set to verifed.
|
If the setting is set to false, the created domain will automatically be set to verifed.
|
||||||
|
|
||||||
An organization can have multiple domain names, but only one domain can be primary.
|
An organization can have multiple domain names, but only one domain can be primary.
|
||||||
The primary domain defines which login name ZITADEL displays to the user, and what information gets asserted in access_tokens (`preferred_username`).
|
The primary domain defines which login name ZITADEL displays to the user, and what information gets asserted in access_tokens (`preferred_username`).
|
||||||
|
|
||||||
Please note that domain verification also removes the logonname from all users, who might have used this combination in the global organization (ie. users not belonging to a specific organization). Relating to our example with acme.ch: If a user ‘coyote’ exists in the global organization with the logonname coyote@acme.ch, then after verification of acme.ch, this logonname will be replaced with `coyote@{randomvalue.tld}`. ZITADEL will notify users affected by this change.
|
Please note that domain verification also removes the login name from all users, who might have used this combination in the global organization (ie. users not belonging to a specific organization).
|
||||||
|
Relating to our example with acme.ch: If a user ‘coyote’ exists in the global organization with the login name coyote@acme.ch, then after verification of acme.ch, this login name will be replaced with `coyote@{randomvalue.tld}`.
|
||||||
|
ZITADEL will notify users affected by this change.
|
||||||
|
|
||||||
## Verify your domain name
|
## Verify your domain name
|
||||||
|
|
||||||
|
306
docs/docs/guides/migrate/sources/keycloak.md
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
---
|
||||||
|
title: Migrate from Keycloak
|
||||||
|
sidebar_label: From Keycloak
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrating from Keycloak to ZITADEL
|
||||||
|
|
||||||
|
This guide will use [Docker installation](https://www.docker.com/) to run Keycloak and ZITADEL. However, both Keycloak and ZITADEL offer different installation methods. As a result, this guide won't include any required production tuning or security hardening for either system. However, it's advised you follow [recommended guidelines](https://zitadel.com/docs/guides/manage/self-hosted/production) before putting those systems into production. You can skip setting up Keycloak and ZITADEL if you already have running instances.
|
||||||
|
|
||||||
|
## Set up Keycloak
|
||||||
|
### Run Keycloak
|
||||||
|
|
||||||
|
To begin setting up Keycloak, you need to refer to the official [Keycloak Docker image](https://www.keycloak.org/getting-started/getting-started-docker). You'll use it to run a development version of the Keycloak server on your local machine:
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 8081:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:22.0.1 start-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
In a few seconds, Keycloak will be available at [http://localhost:8081](http://localhost:8081). Access the **Administration Console** via the username `admin` and password `admin`:
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-01.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-02.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
|
||||||
|
### Create a realm in Keycloak
|
||||||
|
|
||||||
|
In order to configure Keycloak as the identity provider for your application, you need to create a new realm. This will allow users and authentication resources to be isolated from any other Keycloak usage. Click on the sidebar drop-down menu and select **Create Realm**. Then input the desired realm name and click **Create**:
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-03.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-04.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-05.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
|
||||||
|
### Create user in Keycloak
|
||||||
|
|
||||||
|
The last thing you need to do in Keycloak is to create at least one new user. This user will be able to log into your application.
|
||||||
|
|
||||||
|
On the menu on the left, select **Users**, and click **Add user**. Fill in the username, email, and first and last names, and mark the email as verified. Click on **Create** to create a new user:
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-11.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-12.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-13.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
Now you should attach a password to this user. Select the **Credentials** tab and click **Set password**.
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-14.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
On the new modal panel, input the desired password and select **Save**.
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-15.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-16.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
### Export Keycloak users
|
||||||
|
|
||||||
|
Keycloak provides an [export](https://www.keycloak.org/server/importExport) functionality that allows user information to be extracted into JSON files. While it's intended to be used in another Keycloak instance, you can manipulate it to export users to a different user management system.
|
||||||
|
|
||||||
|
For example, in order to generate the export files with Keycloak, you will need to enter the Docker container, run the export command, and copy it outside the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Recover the Container ID for Keycloak
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# Run the export command inside the Keycloak container
|
||||||
|
# use the container ID of Keycloak
|
||||||
|
docker exec <keycloak container ID> /opt/keycloak/bin/kc.sh export --dir /tmp
|
||||||
|
|
||||||
|
# copy generated files from docker container to local machine
|
||||||
|
docker cp <keycloak container ID>:/tmp/my-realm-users-0.json .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Set up ZITADEL
|
||||||
|
|
||||||
|
After creating a sample application that connects to Keycloak, you need to set up ZITADEL in order to migrate the application and users from Keycloak to ZITADEL. For this, ZITADEL offers a [Docker Compose](https://zitadel.com/docs/self-hosting/deploy/compose) installation guide. Follow the instructions under the [Docker compose](https://zitadel.com/docs/self-hosting/deploy/compose#docker-compose) section to run a ZITADEL instance locally.
|
||||||
|
|
||||||
|
Next, the application will be available at [http://localhost:8080/ui/console/](http://localhost:8080/ui/console/).
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-22.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
Now you can access the console with the following default credentials:
|
||||||
|
|
||||||
|
* **Username**: `zitadel-admin@zitadel.localhost`
|
||||||
|
* **Password**: `Password1!`
|
||||||
|
|
||||||
|
|
||||||
|
## Import Keycloak users into ZITADEL
|
||||||
|
|
||||||
|
As explained in this [ZITADEL user migration guide](https://zitadel.com/docs/guides/migrate/users), you can import users individually or in bulk. Since we are looking at importing a single user from Keycloak, migrating that individual user to ZITADEL can be done with the [ImportHumanUser](https://zitadel.com/docs/apis/resources/mgmt/management-service-import-human-user) endpoint.
|
||||||
|
|
||||||
|
> With this endpoint, an email will only be sent to the user if the email is marked as not verified or if there's no password set.
|
||||||
|
|
||||||
|
### Create a service user to consume ZITADEL API
|
||||||
|
|
||||||
|
But first of all, in order to use this ZITADEL API, you need to create a [service user](https://zitadel.com/docs/guides/integrate/serviceusers#exercise-create-a-service-user).
|
||||||
|
|
||||||
|
Go to the **Users** menu and select the **Service Users** tab. And click the **+ New** button.
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-39.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
Fill in the details of the service user and click **Create**.
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-40.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
Your service user is now created and listed.
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-41.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
### Provide 'Org Owner' permissions to the service user
|
||||||
|
|
||||||
|
This service user needs to have elevated permissions in order to import users. For this example, you should make the service user an organization owner as explained in [this guide](https://zitadel.com/docs/guides/integrate/access-zitadel-apis#add-org_owner-to-service-user).
|
||||||
|
|
||||||
|
Let's change the permissions as follows:
|
||||||
|
|
||||||
|
Click on the button shown in the image below:
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-42.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
Next, select your service user that you created and select the **Org Owner** checkbox to assign the permissions of an organization owner to the service user.
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-43.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
### Generate an access token for the service user
|
||||||
|
|
||||||
|
In order for the service user to access the API, they must be able to authenticate themselves. To authenticate the user, you can use either [JWT with Private Key](/docs/guides/integrate/serviceusers#authenticating-a-service-user) flow (recommended for production) or [Personal Access Tokens](/docs/guides/integrate/pat)(PAT). In this guide, we will choose the latter.
|
||||||
|
|
||||||
|
Go to **Users** -> **Service Users** again and click on the service user, then select **Personal Access Tokens** on the left and click the **+ New** button. Copy the generated personal access token to use it later. Click **Close** after copying the PAT.
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-44.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
### Import user to ZITADEL via ZITADEL API
|
||||||
|
|
||||||
|
if your Keycloak Realm has a single user, your `my-realm-users-0.json` file, into which you exported your Keycloak user previously, will look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"realm" : "my-realm",
|
||||||
|
"users" : [ {
|
||||||
|
"id" : "826731b2-bf17-4bd9-b45c-6a26c76ddaae",
|
||||||
|
"createdTimestamp" : 1693887631918,
|
||||||
|
"username" : "test-user",
|
||||||
|
"enabled" : true,
|
||||||
|
"totp" : false,
|
||||||
|
"emailVerified" : true,
|
||||||
|
"firstName" : "John",
|
||||||
|
"lastName" : "Doe",
|
||||||
|
"email" : "test-user@mail.com",
|
||||||
|
"credentials" : [ {
|
||||||
|
"id" : "c3f3759e-9d8a-4628-aad9-09e66f28a4e2",
|
||||||
|
"type" : "password",
|
||||||
|
"userLabel" : "My password",
|
||||||
|
"createdDate" : 1693888572700,
|
||||||
|
"secretData" : "{\"value\":\"ng6oDRung/pBLayd5ro7IU3mL/p86pg3WvQNQc+N1Eg=\",\"salt\":\"RaXjs4RiUKgJGkX6kp277w==\",\"additionalParameters\":{}}",
|
||||||
|
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
|
||||||
|
} ],
|
||||||
|
"disableableCredentialTypes" : [ ],
|
||||||
|
"requiredActions" : [ ],
|
||||||
|
"realmRoles" : [ "default-roles-my-realm" ],
|
||||||
|
"notBefore" : 0,
|
||||||
|
"groups" : [ ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, you need to transform the JSON to the ZITADEL data format by adhering to the ZITADEL API [specification](https://zitadel.com/docs/apis/resources/mgmt/management-service-import-human-user) to import a user. The minimal format would be as shown below:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"userName": "test-user",
|
||||||
|
"profile": {
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"email": "test-user@mail.com",
|
||||||
|
"isEmailVerified": true
|
||||||
|
},
|
||||||
|
"hashedPassword": {
|
||||||
|
"value": "$pbkdf2-sha256$27500$RaXjs4RiUKgJGkX6kp277w==$ng6oDRung/pBLayd5ro7IU3mL/p86pg3WvQNQc+N1Eg="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, you must install [`zitadel-tools`](https://github.com/zitadel/zitadel-tools/tree/main), which is a utility toolset designed to facilitate various interactions with the ZITADEL platform, mainly with tasks related to authentication, authorization, and data migration. We will be using the `migrate` command:
|
||||||
|
|
||||||
|
Purpose: Assists users in transforming exported data from other identity providers to be compatible with Zitadel's import schema.
|
||||||
|
Supported Providers: Currently, migrations from Auth0 and Keycloak are supported.
|
||||||
|
Usage: Users can get a list of available sub-commands and flags with the --help flag.
|
||||||
|
|
||||||
|
Install `zitadel-tools` using the command below. Ensure you have Go already installed on your machine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/zitadel/zitadel-tools@main
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can run the migration tool for Keycloak as explained in this [guide](https://github.com/zitadel/zitadel-tools/blob/main/cmd/migration/keycloak/readme.md). Let's go through the steps:
|
||||||
|
|
||||||
|
The Keycloak migration tool facilitates the transfer of data to ZITADEL by creating a JSON file tailored to serve as the body for an import request to the ZITADEL API. Note that it's essential that an organization already exists within ZITADEL/
|
||||||
|
|
||||||
|
To perform the migration, you'll need:
|
||||||
|
|
||||||
|
- The organization ID (--org)
|
||||||
|
- A realm.json file (in our case, `my-realm-users-0.json`) that houses your exported Keycloak realm with user details (--realm).
|
||||||
|
- Output path via --output (default: ./importBody.json)
|
||||||
|
- Timeout duration for the data import request using --timeout (default: 30 minutes)
|
||||||
|
- Pretty printing the output JSON with --multiline.
|
||||||
|
|
||||||
|
Execute with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zitadel-tools migrate keycloak --org=<organisation id> --realm=./realm.json --output=./importBody.json --timeout=1h --multiline
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zitadel-tools migrate keycloak --org=233868910057750531 --realm=./my-realm-users-0.json --output=./importBody.json --timeout=1h --multiline
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure `my-realm-users-0.json` is in the same directory for the tool to process it, or provide the path to the file.
|
||||||
|
|
||||||
|
`importBody.json` will now contain the transformed data as shown below:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
{
|
||||||
|
"dataOrgs": {
|
||||||
|
"orgs": [
|
||||||
|
{
|
||||||
|
"orgId": "233868910057750531",
|
||||||
|
"humanUsers": [
|
||||||
|
{
|
||||||
|
"userId": "826731b2-bf17-4bd9-b45c-6a26c76ddaae",
|
||||||
|
"user": {
|
||||||
|
"userName": "test-user",
|
||||||
|
"profile": {
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"email": "test-user@mail.com",
|
||||||
|
"isEmailVerified": true
|
||||||
|
},
|
||||||
|
"hashedPassword": {
|
||||||
|
"value": "$pbkdf2-sha256$27500$RaXjs4RiUKgJGkX6kp277w==$ng6oDRung/pBLayd5ro7IU3mL/p86pg3WvQNQc+N1Eg="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timeout": "1h0m0s"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now copy the following portion to a separate file and name the file `zitadel-users-file.json`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"userId": "826731b2-bf17-4bd9-b45c-6a26c76ddaae",
|
||||||
|
"user": {
|
||||||
|
"userName": "test-user",
|
||||||
|
"profile": {
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe"
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"email": "test-user@mail.com",
|
||||||
|
"isEmailVerified": true
|
||||||
|
},
|
||||||
|
"hashedPassword": {
|
||||||
|
"value": "$pbkdf2-sha256$27500$RaXjs4RiUKgJGkX6kp277w==$ng6oDRung/pBLayd5ro7IU3mL/p86pg3WvQNQc+N1Eg="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now that we have the user details in the required JSON format, let’s call the ZITADEL API to add the user.
|
||||||
|
|
||||||
|
Run the following cURL command to invoke the API and don't forget to replace `<service user access token>` with the service user's personal access token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
--url http://localhost:8080/management/v1/users/human/_import \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'Authorization: Bearer <service user access token>' \
|
||||||
|
--data @zitadel-users-file.json
|
||||||
|
```
|
||||||
|
|
||||||
|
A successful response would be as shown below:
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-46.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
> Note that the previous request imports a single user. If you're using ZITADEL Cloud and have a large number of users, you may hit its rate limit or may need to pay the excess number of API requests. If you experience this, reach out to the [ZITADEL support team](https://zitadel.com/contact), as they can provide an alternative migration tools to move a large number of users.
|
||||||
|
|
||||||
|
|
||||||
|
Now you have imported the Keycloak user into ZITADEL. To view your user go to [http://localhost:8080/ui/console/users](http://localhost:8080/ui/console/users) (or go to the **Users** tab to see the users).
|
||||||
|
|
||||||
|
<img src="/docs/img/guides/migrate/keycloak-47.png" alt="Migrating users from Keycloak to ZITADEL"/>
|
||||||
|
|
||||||
|
|
||||||
|
You can now view the Keycloak user's details in ZITADEL. You can see that the password is available too.
|
@@ -125,6 +125,7 @@ module.exports = {
|
|||||||
items: [
|
items: [
|
||||||
"guides/migrate/sources/zitadel",
|
"guides/migrate/sources/zitadel",
|
||||||
"guides/migrate/sources/auth0",
|
"guides/migrate/sources/auth0",
|
||||||
|
"guides/migrate/sources/keycloak",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
BIN
docs/static/img/guides/console/smtp.png
vendored
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 50 KiB |
BIN
docs/static/img/guides/console/twilio.png
vendored
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 39 KiB |
BIN
docs/static/img/guides/migrate/keycloak-01.png
vendored
Normal file
After Width: | Height: | Size: 144 KiB |
BIN
docs/static/img/guides/migrate/keycloak-02.png
vendored
Normal file
After Width: | Height: | Size: 197 KiB |
BIN
docs/static/img/guides/migrate/keycloak-03.png
vendored
Normal file
After Width: | Height: | Size: 221 KiB |
BIN
docs/static/img/guides/migrate/keycloak-04.png
vendored
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
docs/static/img/guides/migrate/keycloak-05.png
vendored
Normal file
After Width: | Height: | Size: 151 KiB |
BIN
docs/static/img/guides/migrate/keycloak-06.png
vendored
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
docs/static/img/guides/migrate/keycloak-07.png
vendored
Normal file
After Width: | Height: | Size: 193 KiB |
BIN
docs/static/img/guides/migrate/keycloak-08.png
vendored
Normal file
After Width: | Height: | Size: 195 KiB |
BIN
docs/static/img/guides/migrate/keycloak-09.png
vendored
Normal file
After Width: | Height: | Size: 213 KiB |
BIN
docs/static/img/guides/migrate/keycloak-10.png
vendored
Normal file
After Width: | Height: | Size: 249 KiB |
BIN
docs/static/img/guides/migrate/keycloak-11.png
vendored
Normal file
After Width: | Height: | Size: 143 KiB |
BIN
docs/static/img/guides/migrate/keycloak-12.png
vendored
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
docs/static/img/guides/migrate/keycloak-13.png
vendored
Normal file
After Width: | Height: | Size: 205 KiB |
BIN
docs/static/img/guides/migrate/keycloak-14.png
vendored
Normal file
After Width: | Height: | Size: 171 KiB |
BIN
docs/static/img/guides/migrate/keycloak-15.png
vendored
Normal file
After Width: | Height: | Size: 170 KiB |
BIN
docs/static/img/guides/migrate/keycloak-16.png
vendored
Normal file
After Width: | Height: | Size: 177 KiB |
BIN
docs/static/img/guides/migrate/keycloak-17.png
vendored
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
docs/static/img/guides/migrate/keycloak-18.png
vendored
Normal file
After Width: | Height: | Size: 220 KiB |
BIN
docs/static/img/guides/migrate/keycloak-19.png
vendored
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
docs/static/img/guides/migrate/keycloak-20.png
vendored
Normal file
After Width: | Height: | Size: 147 KiB |
BIN
docs/static/img/guides/migrate/keycloak-21.png
vendored
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
docs/static/img/guides/migrate/keycloak-22.png
vendored
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/static/img/guides/migrate/keycloak-23.png
vendored
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
docs/static/img/guides/migrate/keycloak-24.png
vendored
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/static/img/guides/migrate/keycloak-25.png
vendored
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
docs/static/img/guides/migrate/keycloak-26.png
vendored
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
docs/static/img/guides/migrate/keycloak-27.png
vendored
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
docs/static/img/guides/migrate/keycloak-28.png
vendored
Normal file
After Width: | Height: | Size: 256 KiB |
BIN
docs/static/img/guides/migrate/keycloak-29.png
vendored
Normal file
After Width: | Height: | Size: 239 KiB |
BIN
docs/static/img/guides/migrate/keycloak-30.png
vendored
Normal file
After Width: | Height: | Size: 313 KiB |
BIN
docs/static/img/guides/migrate/keycloak-31.png
vendored
Normal file
After Width: | Height: | Size: 341 KiB |
BIN
docs/static/img/guides/migrate/keycloak-32.png
vendored
Normal file
After Width: | Height: | Size: 222 KiB |
BIN
docs/static/img/guides/migrate/keycloak-33.png
vendored
Normal file
After Width: | Height: | Size: 204 KiB |
BIN
docs/static/img/guides/migrate/keycloak-34.png
vendored
Normal file
After Width: | Height: | Size: 223 KiB |
BIN
docs/static/img/guides/migrate/keycloak-35.png
vendored
Normal file
After Width: | Height: | Size: 270 KiB |
BIN
docs/static/img/guides/migrate/keycloak-36.png
vendored
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/static/img/guides/migrate/keycloak-37.png
vendored
Normal file
After Width: | Height: | Size: 849 KiB |
BIN
docs/static/img/guides/migrate/keycloak-38.png
vendored
Normal file
After Width: | Height: | Size: 160 KiB |
BIN
docs/static/img/guides/migrate/keycloak-39.png
vendored
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
docs/static/img/guides/migrate/keycloak-40.png
vendored
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
docs/static/img/guides/migrate/keycloak-41.png
vendored
Normal file
After Width: | Height: | Size: 159 KiB |
BIN
docs/static/img/guides/migrate/keycloak-42.png
vendored
Normal file
After Width: | Height: | Size: 317 KiB |
BIN
docs/static/img/guides/migrate/keycloak-43.png
vendored
Normal file
After Width: | Height: | Size: 345 KiB |
BIN
docs/static/img/guides/migrate/keycloak-44.png
vendored
Normal file
After Width: | Height: | Size: 264 KiB |
BIN
docs/static/img/guides/migrate/keycloak-45.png
vendored
Normal file
After Width: | Height: | Size: 1024 KiB |
BIN
docs/static/img/guides/migrate/keycloak-46.png
vendored
Normal file
After Width: | Height: | Size: 421 KiB |
BIN
docs/static/img/guides/migrate/keycloak-47.png
vendored
Normal file
After Width: | Height: | Size: 280 KiB |
62
e2e/cypress/e2e/instance/settings/notifications.cy.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
const notificationPath = `/settings?id=notifications`;
|
||||||
|
const smtpPath = `/settings?id=smtpprovider`;
|
||||||
|
const smsPath = `/settings?id=smsprovider`;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.context().as('ctx');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('instance notifications', () => {
|
||||||
|
describe('notification settings', () => {
|
||||||
|
it(`should show notification settings`, () => {
|
||||||
|
cy.visit(notificationPath);
|
||||||
|
cy.contains('Notification');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('smtp settings', () => {
|
||||||
|
it(`should show SMTP provider settings`, () => {
|
||||||
|
cy.visit(smtpPath);
|
||||||
|
cy.contains('SMTP Settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should add SMTP provider settings`, () => {
|
||||||
|
cy.visit(smtpPath);
|
||||||
|
cy.get('[formcontrolname="senderAddress"]').clear().type('sender@example.com');
|
||||||
|
cy.get('[formcontrolname="senderName"]').clear().type('Zitadel');
|
||||||
|
cy.get('[formcontrolname="hostAndPort"]').clear().type('smtp.mailtrap.io:2525');
|
||||||
|
cy.get('[formcontrolname="user"]').clear().type('user@example.com');
|
||||||
|
cy.get('[data-e2e="save-smtp-settings-button"]').click();
|
||||||
|
cy.shouldConfirmSuccess();
|
||||||
|
cy.get('[formcontrolname="senderAddress"]').should('have.value', 'sender@example.com');
|
||||||
|
cy.get('[formcontrolname="senderName"]').should('have.value', 'Zitadel');
|
||||||
|
cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailtrap.io:2525');
|
||||||
|
cy.get('[formcontrolname="user"]').should('have.value', 'user@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should add SMTP provider password`, () => {
|
||||||
|
cy.visit(smtpPath);
|
||||||
|
cy.get('[data-e2e="add-smtp-password-button"]').click();
|
||||||
|
cy.get('[data-e2e="notification-setting-password"]').clear().type('dummy@example.com');
|
||||||
|
cy.get('[data-e2e="save-notification-setting-password-button"]').click();
|
||||||
|
cy.shouldConfirmSuccess();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sms settings', () => {
|
||||||
|
it(`should show SMS provider settings`, () => {
|
||||||
|
cy.visit(smsPath);
|
||||||
|
cy.contains('SMS Settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should add SMS provider`, () => {
|
||||||
|
cy.visit(smsPath);
|
||||||
|
cy.get('[data-e2e="new-twilio-button"]').click();
|
||||||
|
cy.get('[formcontrolname="sid"]').clear().type('test');
|
||||||
|
cy.get('[formcontrolname="token"]').clear().type('token');
|
||||||
|
cy.get('[formcontrolname="senderNumber"]').clear().type('2312123132');
|
||||||
|
cy.get('[data-e2e="save-sms-settings-button"]').click();
|
||||||
|
cy.shouldConfirmSuccess();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -55,12 +55,6 @@ func AssetAPI(externalSecure bool) func(context.Context) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssetAPIFromDomain(externalSecure bool, externalPort uint16) func(context.Context) string {
|
|
||||||
return func(ctx context.Context) string {
|
|
||||||
return http_util.BuildHTTP(authz.GetInstance(ctx).RequestedDomain(), externalPort, externalSecure) + HandlerPrefix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Uploader interface {
|
type Uploader interface {
|
||||||
UploadAsset(ctx context.Context, info string, asset *command.AssetUpload, commands *command.Commands) error
|
UploadAsset(ctx context.Context, info string, asset *command.AssetUpload, commands *command.Commands) error
|
||||||
ObjectName(data authz.CtxData) (string, error)
|
ObjectName(data authz.CtxData) (string, error)
|
||||||
|
@@ -133,7 +133,7 @@ func GetAllPermissionsFromCtx(ctx context.Context) []string {
|
|||||||
func checkOrigin(ctx context.Context, origins []string) error {
|
func checkOrigin(ctx context.Context, origins []string) error {
|
||||||
origin := grpc.GetGatewayHeader(ctx, http_util.Origin)
|
origin := grpc.GetGatewayHeader(ctx, http_util.Origin)
|
||||||
if origin == "" {
|
if origin == "" {
|
||||||
origin = http_util.OriginFromCtx(ctx)
|
origin = http_util.OriginHeader(ctx)
|
||||||
if origin == "" {
|
if origin == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
errorsAs "errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
@@ -36,6 +37,9 @@ func ValidateDomainHTTP(domain, token, verifier string) error {
|
|||||||
return errors.ThrowInternal(err, "HTTP-BH42h", "Errors.Internal")
|
return errors.ThrowInternal(err, "HTTP-BH42h", "Errors.Internal")
|
||||||
}
|
}
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
|
if resp.StatusCode == 404 {
|
||||||
|
return errors.ThrowNotFound(err, "ORG-F4zhw", "Errors.Org.DomainVerificationHTTPNotFound")
|
||||||
|
}
|
||||||
return errors.ThrowInternal(err, "HTTP-G2zsw", "Errors.Internal")
|
return errors.ThrowInternal(err, "HTTP-G2zsw", "Errors.Internal")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -46,12 +50,21 @@ func ValidateDomainHTTP(domain, token, verifier string) error {
|
|||||||
if string(body) == verifier {
|
if string(body) == verifier {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return errors.ThrowInvalidArgument(err, "HTTP-GH422", "Errors.Internal")
|
return errors.ThrowNotFound(err, "ORG-GH422", "Errors.Org.DomainVerificationHTTPNoMatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateDomainDNS(domain, verifier string) error {
|
func ValidateDomainDNS(domain, verifier string) error {
|
||||||
txtRecords, err := net.LookupTXT(tokenUrlDNS(domain))
|
txtRecords, err := net.LookupTXT(tokenUrlDNS(domain))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
var dnsError *net.DNSError
|
||||||
|
if errorsAs.As(err, &dnsError) {
|
||||||
|
if dnsError.IsNotFound {
|
||||||
|
return errors.ThrowNotFound(err, "ORG-G241f", "Errors.Org.DomainVerificationTXTNotFound")
|
||||||
|
}
|
||||||
|
if dnsError.IsTimeout {
|
||||||
|
return errors.ThrowNotFound(err, "ORG-K563l", "Errors.Org.DomainVerificationTimeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
return errors.ThrowInternal(err, "HTTP-Hwsw2", "Errors.Internal")
|
return errors.ThrowInternal(err, "HTTP-Hwsw2", "Errors.Internal")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +73,7 @@ func ValidateDomainDNS(domain, verifier string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errors.ThrowInvalidArgument(err, "HTTP-G241f", "Errors.Internal")
|
return errors.ThrowNotFound(err, "ORG-G28if", "Errors.Org.DomainVerificationTXTNoMatch")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TokenUrl(domain, token string, checkType CheckType) (string, error) {
|
func TokenUrl(domain, token string, checkType CheckType) (string, error) {
|
||||||
|
@@ -45,6 +45,7 @@ type key int
|
|||||||
const (
|
const (
|
||||||
httpHeaders key = iota
|
httpHeaders key = iota
|
||||||
remoteAddr
|
remoteAddr
|
||||||
|
origin
|
||||||
)
|
)
|
||||||
|
|
||||||
func CopyHeadersToContext(h http.Handler) http.Handler {
|
func CopyHeadersToContext(h http.Handler) http.Handler {
|
||||||
@@ -61,7 +62,7 @@ func HeadersFromCtx(ctx context.Context) (http.Header, bool) {
|
|||||||
return headers, ok
|
return headers, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func OriginFromCtx(ctx context.Context) string {
|
func OriginHeader(ctx context.Context) string {
|
||||||
headers, ok := ctx.Value(httpHeaders).(http.Header)
|
headers, ok := ctx.Value(httpHeaders).(http.Header)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
@@ -69,6 +70,18 @@ func OriginFromCtx(ctx context.Context) string {
|
|||||||
return headers.Get(Origin)
|
return headers.Get(Origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ComposedOrigin(ctx context.Context) string {
|
||||||
|
o, ok := ctx.Value(origin).(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithComposedOrigin(ctx context.Context, composed string) context.Context {
|
||||||
|
return context.WithValue(ctx, origin, composed)
|
||||||
|
}
|
||||||
|
|
||||||
func RemoteIPFromCtx(ctx context.Context) string {
|
func RemoteIPFromCtx(ctx context.Context) string {
|
||||||
ctxHeaders, ok := HeadersFromCtx(ctx)
|
ctxHeaders, ok := HeadersFromCtx(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
103
internal/api/http/middleware/origin_interceptor.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/muhlemmer/httpforwarded"
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OriginHandler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
origin := composeOrigin(r)
|
||||||
|
if !http_util.IsOrigin(origin) {
|
||||||
|
logging.Debugf("extracted origin is not valid: %s", origin)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r.WithContext(http_util.WithComposedOrigin(r.Context(), origin)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeOrigin(r *http.Request) string {
|
||||||
|
if origin, err := originFromForwardedHeader(r); err != nil {
|
||||||
|
logging.OnError(err).Debug("failed to build origin from forwarded header, trying x-forwarded-* headers")
|
||||||
|
} else {
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
if origin, err := originFromXForwardedHeaders(r); err != nil {
|
||||||
|
logging.OnError(err).Debug("failed to build origin from x-forwarded-* headers, using host header")
|
||||||
|
} else {
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
scheme := "https"
|
||||||
|
if r.TLS == nil {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s://%s", scheme, r.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func originFromForwardedHeader(r *http.Request) (string, error) {
|
||||||
|
fwd, err := httpforwarded.ParseFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var fwdProto, fwdHost, fwdPort string
|
||||||
|
if fwdProto = mostRecentValue(fwd, "proto"); fwdProto == "" {
|
||||||
|
return "", fmt.Errorf("no proto in forwarded header")
|
||||||
|
}
|
||||||
|
if fwdHost = mostRecentValue(fwd, "host"); fwdHost == "" {
|
||||||
|
return "", fmt.Errorf("no host in forwarded header")
|
||||||
|
}
|
||||||
|
fwdPort, foundFwdFor := extractPort(mostRecentValue(fwd, "for"))
|
||||||
|
if !foundFwdFor {
|
||||||
|
return "", fmt.Errorf("no for in forwarded header")
|
||||||
|
}
|
||||||
|
o := fmt.Sprintf("%s://%s", fwdProto, fwdHost)
|
||||||
|
if fwdPort != "" {
|
||||||
|
o += ":" + fwdPort
|
||||||
|
}
|
||||||
|
return o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func originFromXForwardedHeaders(r *http.Request) (string, error) {
|
||||||
|
scheme := r.Header.Get("X-Forwarded-Proto")
|
||||||
|
if scheme == "" {
|
||||||
|
return "", fmt.Errorf("no X-Forwarded-Proto header")
|
||||||
|
}
|
||||||
|
host := r.Header.Get("X-Forwarded-Host")
|
||||||
|
if host == "" {
|
||||||
|
return "", fmt.Errorf("no X-Forwarded-Host header")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s://%s", scheme, host), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPort(raw string) (string, bool) {
|
||||||
|
if u, ok := parseURL(raw); ok {
|
||||||
|
return u.Port(), ok
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseURL(raw string) (*url.URL, bool) {
|
||||||
|
if raw == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
return u, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mostRecentValue(forwarded map[string][]string, key string) string {
|
||||||
|
if forwarded == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
values := forwarded[key]
|
||||||
|
if len(values) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return values[len(values)-1]
|
||||||
|
}
|
@@ -119,6 +119,7 @@ func (wm *InstanceSMTPConfigWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
AggregateIDs(wm.AggregateID).
|
AggregateIDs(wm.AggregateID).
|
||||||
EventTypes(
|
EventTypes(
|
||||||
instance.SMTPConfigAddedEventType,
|
instance.SMTPConfigAddedEventType,
|
||||||
|
instance.SMTPConfigRemovedEventType,
|
||||||
instance.SMTPConfigChangedEventType,
|
instance.SMTPConfigChangedEventType,
|
||||||
instance.SMTPConfigPasswordChangedEventType,
|
instance.SMTPConfigPasswordChangedEventType,
|
||||||
instance.InstanceDomainAddedEventType,
|
instance.InstanceDomainAddedEventType,
|
||||||
|
@@ -213,9 +213,11 @@ func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgD
|
|||||||
return writeModelToObjectDetails(&domainWriteModel.WriteModel), nil
|
return writeModelToObjectDetails(&domainWriteModel.WriteModel), nil
|
||||||
}
|
}
|
||||||
events = append(events, org.NewDomainVerificationFailedEvent(ctx, orgAgg, orgDomain.Domain))
|
events = append(events, org.NewDomainVerificationFailedEvent(ctx, orgAgg, orgDomain.Domain))
|
||||||
_, err = c.eventstore.Push(ctx, events...)
|
|
||||||
logging.LogWithFields("ORG-dhTE", "orgID", orgAgg.ID, "domain", orgDomain.Domain).OnError(err).Error("NewDomainVerificationFailedEvent push failed")
|
_, errPush := c.eventstore.Push(ctx, events...)
|
||||||
return nil, errors.ThrowInvalidArgument(err, "ORG-GH3s", "Errors.Org.DomainVerificationFailed")
|
logging.LogWithFields("ORG-dhTE", "orgID", orgAgg.ID, "domain", orgDomain.Domain).OnError(errPush).Error("NewDomainVerificationFailedEvent push failed")
|
||||||
|
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) {
|
func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) {
|
||||||
|