feat: add notification policy and password change message (#5065)

Implementation of new notification policy with functionality to send email when a password is changed
This commit is contained in:
Stefan Benz 2023-01-25 09:49:41 +01:00 committed by GitHub
parent 8b5894c0bb
commit 19621acfd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 4196 additions and 83 deletions

View File

@ -436,6 +436,8 @@ DefaultInstance:
TOSLink: https://zitadel.com/docs/legal/terms-of-service
PrivacyLink: https://zitadel.com/docs/legal/privacy-policy
HelpLink: ""
NotificationPolicy:
PasswordChange: true
LabelPolicy:
PrimaryColor: "#5469d4"
BackgroundColor: "#fafafa"
@ -514,6 +516,14 @@ DefaultInstance:
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt.
ButtonText: Login
- MessageTextType: PasswordChange
Language: de
Title: ZITADEL - Passwort von Benutzer wurde geändert
PreHeader: Passwort Änderung
Subject: Passwort von Benutzer wurde geändert
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Das Password vom Benutzer wurde geändert. Wenn diese Änderung von jemand anderem gemacht wurde, empfehlen wir die sofortige Zurücksetzung ihres Passworts.
ButtonText: Login
- MessageTextType: InitCode
Language: en
Title: Zitadel - Initialize User
@ -554,6 +564,14 @@ DefaultInstance:
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.UserName}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login.
ButtonText: Login
- MessageTextType: PasswordChange
Language: en
Title: ZITADEL - Password of user has changed
PreHeader: Change password
Subject: Password of user has changed
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: The password of your user has changed. If this change was not done by you, please be advised to immediately reset your password.
ButtonText: Login
InternalAuthZ:
RolePermissionMappings:

View File

@ -3,31 +3,39 @@ import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { MatLegacySelectChange as MatSelectChange } from '@angular/material/legacy-select';
import { BehaviorSubject, from, Observable, of, Subscription } from 'rxjs';
import {
GetCustomPasswordResetMessageTextRequest as AdminGetCustomPasswordResetMessageTextRequest,
GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest,
GetDefaultVerifyEmailMessageTextRequest as AdminGetDefaultVerifyEmailMessageTextRequest,
GetDefaultVerifyPhoneMessageTextRequest as AdminGetDefaultVerifyPhoneMessageTextRequest,
SetDefaultDomainClaimedMessageTextRequest,
SetDefaultInitMessageTextRequest,
SetDefaultPasswordChangeMessageTextRequest,
SetDefaultPasswordlessRegistrationMessageTextRequest,
SetDefaultPasswordResetMessageTextRequest,
SetDefaultVerifyEmailMessageTextRequest,
SetDefaultVerifyPhoneMessageTextRequest,
GetDefaultPasswordChangeMessageTextRequest as AdminGetDefaultPasswordChangeMessageTextRequest,
GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest,
GetDefaultVerifyEmailMessageTextRequest as AdminGetDefaultVerifyEmailMessageTextRequest,
GetDefaultVerifyPhoneMessageTextRequest as AdminGetDefaultVerifyPhoneMessageTextRequest,
GetDefaultPasswordResetMessageTextRequest as AdminGetDefaultPasswordResetMessageTextRequest,
GetDefaultDomainClaimedMessageTextRequest as AdminGetDefaultDomainClaimedMessageTextRequest,
GetDefaultPasswordlessRegistrationMessageTextRequest as AdminGetDefaultPasswordlessRegistrationMessageTextRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import {
GetCustomDomainClaimedMessageTextRequest,
GetCustomInitMessageTextRequest,
GetCustomPasswordChangeMessageTextRequest,
GetCustomPasswordlessRegistrationMessageTextRequest,
GetCustomPasswordResetMessageTextRequest,
GetCustomVerifyEmailMessageTextRequest,
GetCustomVerifyPhoneMessageTextRequest,
GetDefaultDomainClaimedMessageTextRequest,
GetDefaultInitMessageTextRequest,
GetDefaultPasswordChangeMessageTextRequest,
GetDefaultPasswordlessRegistrationMessageTextRequest,
GetDefaultPasswordResetMessageTextRequest,
GetDefaultVerifyEmailMessageTextRequest,
GetDefaultVerifyPhoneMessageTextRequest,
SetCustomDomainClaimedMessageTextRequest,
SetCustomInitMessageTextRequest,
SetCustomPasswordChangeMessageTextRequest,
SetCustomPasswordlessRegistrationMessageTextRequest,
SetCustomPasswordResetMessageTextRequest,
SetCustomVerifyEmailMessageTextRequest,
@ -50,12 +58,30 @@ enum MESSAGETYPES {
PASSWORDRESET = 'PR',
DOMAINCLAIMED = 'DC',
PASSWORDLESS = 'PL',
PASSWORDCHANGE = 'PC',
}
const REQUESTMAP = {
[PolicyComponentServiceType.MGMT]: {
[MESSAGETYPES.PASSWORDCHANGE]: {
get: new GetCustomPasswordChangeMessageTextRequest(),
set: new SetCustomPasswordChangeMessageTextRequest(),
getDefault: new GetDefaultPasswordChangeMessageTextRequest(),
setFcn: (map: Partial<MessageCustomText.AsObject>): SetCustomPasswordChangeMessageTextRequest => {
const req = new SetCustomPasswordChangeMessageTextRequest();
req.setButtonText(map.buttonText ?? '');
req.setFooterText(map.footerText ?? '');
req.setGreeting(map.greeting ?? '');
req.setPreHeader(map.preHeader ?? '');
req.setSubject(map.subject ?? '');
req.setText(map.text ?? '');
req.setTitle(map.title ?? '');
return req;
},
},
[MESSAGETYPES.INIT]: {
get: new GetDefaultInitMessageTextRequest(),
get: new GetCustomInitMessageTextRequest(),
set: new SetCustomInitMessageTextRequest(),
getDefault: new GetDefaultInitMessageTextRequest(),
setFcn: (map: Partial<MessageCustomText.AsObject>): SetCustomInitMessageTextRequest => {
@ -164,6 +190,22 @@ const REQUESTMAP = {
},
},
[PolicyComponentServiceType.ADMIN]: {
[MESSAGETYPES.PASSWORDCHANGE]: {
get: new AdminGetDefaultPasswordChangeMessageTextRequest(),
set: new SetDefaultPasswordChangeMessageTextRequest(),
setFcn: (map: Partial<MessageCustomText.AsObject>): SetDefaultPasswordChangeMessageTextRequest => {
const req = new SetDefaultPasswordChangeMessageTextRequest();
req.setButtonText(map.buttonText ?? '');
req.setFooterText(map.footerText ?? '');
req.setGreeting(map.greeting ?? '');
req.setPreHeader(map.preHeader ?? '');
req.setSubject(map.subject ?? '');
req.setText(map.text ?? '');
req.setTitle(map.title ?? '');
return req;
},
},
[MESSAGETYPES.INIT]: {
get: new AdminGetDefaultInitMessageTextRequest(),
set: new SetDefaultInitMessageTextRequest(),
@ -213,7 +255,7 @@ const REQUESTMAP = {
},
},
[MESSAGETYPES.PASSWORDRESET]: {
get: new AdminGetCustomPasswordResetMessageTextRequest(),
get: new AdminGetDefaultPasswordResetMessageTextRequest(),
set: new SetDefaultPasswordResetMessageTextRequest(),
setFcn: (
map: Partial<SetDefaultPasswordResetMessageTextRequest.AsObject>,
@ -231,7 +273,7 @@ const REQUESTMAP = {
},
},
[MESSAGETYPES.DOMAINCLAIMED]: {
get: new GetDefaultDomainClaimedMessageTextRequest(),
get: new AdminGetDefaultDomainClaimedMessageTextRequest(),
set: new SetDefaultDomainClaimedMessageTextRequest(),
setFcn: (
map: Partial<SetDefaultDomainClaimedMessageTextRequest.AsObject>,
@ -249,7 +291,7 @@ const REQUESTMAP = {
},
},
[MESSAGETYPES.PASSWORDLESS]: {
get: new GetDefaultPasswordlessRegistrationMessageTextRequest(),
get: new AdminGetDefaultPasswordlessRegistrationMessageTextRequest(),
set: new SetDefaultPasswordlessRegistrationMessageTextRequest(),
setFcn: (
map: Partial<SetDefaultPasswordlessRegistrationMessageTextRequest.AsObject>,
@ -382,6 +424,20 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.loginnames', value: '{{.LoginNames}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.changedate', value: '{{.ChangeDate}}' },
],
[MESSAGETYPES.PASSWORDCHANGE]: [
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.preferredLoginName', value: '{{.PreferredLoginName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.username', value: '{{.UserName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.firstname', value: '{{.FirstName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastname', value: '{{.Lastname}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.nickName', value: '{{.NickName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.displayName', value: '{{.DisplayName}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastEmail', value: '{{.LastEmail}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.verifiedEmail', value: '{{.VerifiedEmail}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.lastPhone', value: '{{.LastPhone}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.verifiedPhone', value: '{{.VerifiedPhone}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.loginnames', value: '{{.LoginNames}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.changedate', value: '{{.ChangeDate}}' },
],
};
public locale: string = 'en';
@ -435,6 +491,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return this.stripDetails(this.service.getDefaultDomainClaimedMessageText(req));
case MESSAGETYPES.PASSWORDLESS:
return this.stripDetails(this.service.getDefaultPasswordlessRegistrationMessageText(req));
case MESSAGETYPES.PASSWORDCHANGE:
return this.stripDetails(this.service.getDefaultPasswordChangeMessageText(req));
}
}
@ -453,6 +511,9 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return this.stripDetails((this.service as ManagementService).getCustomDomainClaimedMessageText(req));
case MESSAGETYPES.PASSWORDLESS:
return this.stripDetails((this.service as ManagementService).getCustomPasswordlessRegistrationMessageText(req));
case MESSAGETYPES.PASSWORDCHANGE:
return this.stripDetails((this.service as ManagementService).getCustomPasswordChangeMessageText(req));
default:
return undefined;
}
@ -470,6 +531,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return this.stripDetails((this.service as AdminService).getCustomDomainClaimedMessageText(req));
case MESSAGETYPES.PASSWORDLESS:
return this.stripDetails((this.service as AdminService).getCustomPasswordlessRegistrationMessageText(req));
case MESSAGETYPES.PASSWORDCHANGE:
return this.stripDetails((this.service as AdminService).getCustomPasswordChangeMessageText(req));
default:
return undefined;
}
@ -535,6 +598,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return handler(
(this.service as ManagementService).getCustomPasswordlessRegistrationMessageText(this.updateRequest),
);
case MESSAGETYPES.PASSWORDCHANGE:
return handler((this.service as ManagementService).getCustomPasswordChangeMessageText(this.updateRequest));
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
switch (this.currentType) {
@ -550,6 +615,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return handler((this.service as AdminService).setDefaultDomainClaimedMessageText(this.updateRequest));
case MESSAGETYPES.PASSWORDLESS:
return handler((this.service as AdminService).setDefaultPasswordlessRegistrationMessageText(this.updateRequest));
case MESSAGETYPES.PASSWORDCHANGE:
return handler((this.service as AdminService).setDefaultPasswordChangeMessageText(this.updateRequest));
}
}
}
@ -595,6 +662,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return handler(
(this.service as ManagementService).resetCustomPasswordlessRegistrationMessageTextToDefault(this.locale),
);
case MESSAGETYPES.PASSWORDCHANGE:
return handler((this.service as ManagementService).resetCustomPasswordChangeMessageTextToDefault(this.locale));
default:
return Promise.reject();
}

View File

@ -0,0 +1,49 @@
<h2>{{ 'POLICY.NOTIFICATION.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'POLICY.NOTIFICATION.DESCRIPTION' | translate }}</p>
<div *ngIf="loading" class="spinner-wr">
<mat-spinner diameter="30" color="primary"></mat-spinner>
</div>
<ng-template cnslHasRole [hasRole]="['policy.delete']">
<button
*ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{ 'POLICY.RESET' | translate }}"
color="warn"
(click)="removePolicy()"
mat-stroked-button
>
{{ 'POLICY.RESET' | translate }}
</button>
</ng-template>
<div class="notification-policy-card">
<cnsl-card *ngIf="notificationData">
<div class="notification-policy-content">
<div class="row">
<mat-checkbox
class="slide-toggle"
color="primary"
name="hasUppercase"
ngDefaultControl
[(ngModel)]="notificationData.passwordChange"
[disabled]="(['policy.write'] | hasRole | async) === false"
>
{{ 'POLICY.NOTIFICATION.PASSWORDCHANGE' | translate }}
</mat-checkbox>
</div>
</div>
</cnsl-card>
</div>
<div class="btn-container">
<button
(click)="savePolicy()"
[disabled]="(['policy.write'] | hasRole | async) === false"
color="primary"
type="submit"
mat-raised-button
>
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>

View File

@ -0,0 +1,32 @@
.spinner-wr {
margin: 0.5rem 0;
}
.policy-applied-to {
margin: -1rem 0 0 0;
font-size: 14px;
}
.notification-policy-card {
max-width: 400px;
.notification-policy-content {
display: flex;
flex-direction: column;
width: 100%;
.row {
display: flex;
align-items: center;
}
}
}
.btn-container {
display: flex;
justify-content: flex-start;
button {
display: block;
}
}

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
describe('PasswordComplexityPolicyComponent', () => {
let component: PasswordComplexityPolicyComponent;
let fixture: ComponentFixture<PasswordComplexityPolicyComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PasswordComplexityPolicyComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PasswordComplexityPolicyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,177 @@
import { Component, Injector, Input, OnInit, Type } from '@angular/core';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import {
AddNotificationPolicyRequest,
GetNotificationPolicyResponse as AdminGetNotificationPolicyResponse,
UpdateNotificationPolicyRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import {
AddCustomNotificationPolicyRequest,
GetNotificationPolicyResponse as MgmtGetNotificationPolicyResponse,
} from 'src/app/proto/generated/zitadel/management_pb';
import { NotificationPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.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';
@Component({
selector: 'cnsl-notification-policy',
templateUrl: './notification-policy.component.html',
styleUrls: ['./notification-policy.component.scss'],
})
export class NotificationPolicyComponent implements OnInit {
@Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public service!: ManagementService | AdminService;
public notificationData?: NotificationPolicy.AsObject = { isDefault: false, passwordChange: false };
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public loading: boolean = false;
public InfoSectionType: any = InfoSectionType;
public isDefault: boolean = false;
private hasNotificationPolicy: boolean = false;
constructor(private toast: ToastService, private injector: Injector, private dialog: MatDialog) {}
public ngOnInit(): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
this.fetchData();
}
public fetchData(): void {
this.loading = true;
this.getData()
.then((data) => {
if (data.policy) {
this.hasNotificationPolicy = true;
this.notificationData = data.policy;
this.isDefault = data.policy.isDefault;
this.loading = false;
}
})
.catch((error) => {
this.loading = false;
if (error && error.code === 5) {
console.log(error);
this.hasNotificationPolicy = false;
}
});
}
private async getData(): Promise<
MgmtGetNotificationPolicyResponse.AsObject | AdminGetNotificationPolicyResponse.AsObject
> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).getNotificationPolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).getNotificationPolicy();
}
}
public removePolicy(): void {
if (this.service instanceof ManagementService) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.RESET',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'SETTING.DIALOG.RESET.DEFAULTTITLE',
descriptionKey: 'SETTING.DIALOG.RESET.DEFAULTDESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
(this.service as ManagementService)
.resetNotificationPolicyToDefault()
.then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.isDefault = true;
setTimeout(() => {
this.fetchData();
}, 1000);
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
}
public savePolicy(): void {
if (this.notificationData) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
if ((this.notificationData as NotificationPolicy.AsObject).isDefault) {
const req = new AddCustomNotificationPolicyRequest();
req.setPasswordChange(this.notificationData.passwordChange);
(this.service as ManagementService)
.addCustomNotificationPolicy(req)
.then(() => {
this.isDefault = false;
this.toast.showInfo('POLICY.TOAST.SET', true);
})
.catch((error) => {
this.toast.showError(error);
});
} else {
const req = new UpdateNotificationPolicyRequest();
req.setPasswordChange(this.notificationData.passwordChange);
(this.service as ManagementService)
.updateCustomNotificationPolicy(req)
.then(() => {
this.isDefault = false;
this.toast.showInfo('POLICY.TOAST.SET', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
break;
case PolicyComponentServiceType.ADMIN:
if (this.hasNotificationPolicy) {
const req = new UpdateNotificationPolicyRequest();
req.setPasswordChange(this.notificationData.passwordChange);
(this.service as AdminService)
.updateNotificationPolicy(req)
.then(() => {
this.isDefault = false;
this.toast.showInfo('POLICY.TOAST.SET', true);
})
.catch((error) => {
this.toast.showError(error);
});
} else {
const req = new AddNotificationPolicyRequest();
req.setPasswordChange(this.notificationData.passwordChange);
(this.service as AdminService)
.addNotificationPolicy(req)
.then(() => {
this.isDefault = false;
this.hasNotificationPolicy = true;
this.toast.showInfo('POLICY.TOAST.SET', true);
})
.catch((error) => {
this.toast.showError(error);
});
}
break;
}
}
}
}

View File

@ -0,0 +1,43 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyProgressSpinnerModule as MatProgressSpinnerModule } from '@angular/material/legacy-progress-spinner';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { CardModule } from '../../card/card.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { NotificationPolicyComponent } from './notification-policy.component';
@NgModule({
declarations: [NotificationPolicyComponent],
imports: [
CommonModule,
FormsModule,
InputModule,
MatButtonModule,
MatIconModule,
HasRoleModule,
MatDialogModule,
MatTooltipModule,
MatCheckboxModule,
HasRolePipeModule,
TranslateModule,
WarnDialogModule,
DetailLayoutModule,
CardModule,
MatProgressSpinnerModule,
InfoSectionModule,
],
exports: [NotificationPolicyComponent],
})
export class NotificationPolicyModule {}

View File

@ -1,20 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
const routes: Routes = [
{
path: '',
component: PasswordComplexityPolicyComponent,
data: {
animation: 'DetailPage',
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PasswordComplexityPolicyRoutingModule {}

View File

@ -16,13 +16,11 @@ import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.mod
import { CardModule } from '../../card/card.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { WarnDialogModule } from '../../warn-dialog/warn-dialog.module';
import { PasswordComplexityPolicyRoutingModule } from './password-complexity-policy-routing.module';
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
@NgModule({
declarations: [PasswordComplexityPolicyComponent],
imports: [
PasswordComplexityPolicyRoutingModule,
CommonModule,
FormsModule,
InputModule,

View File

@ -25,9 +25,14 @@
<cnsl-idp-settings [serviceType]="serviceType"></cnsl-idp-settings>
</ng-container>
<ng-container *ngIf="currentSetting === 'notifications' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy>
<cnsl-notification-settings [serviceType]="serviceType"></cnsl-notification-settings>
</ng-container>
<ng-container *ngIf="currentSetting === 'notifications' && serviceType === PolicyComponentServiceType.MGMT">
<cnsl-notification-policy [serviceType]="serviceType"></cnsl-notification-policy
></ng-container>
<ng-container *ngIf="currentSetting === 'oidc' && serviceType === PolicyComponentServiceType.ADMIN">
<cnsl-oidc-configuration></cnsl-oidc-configuration>
</ng-container>

View File

@ -11,6 +11,7 @@ import { IdpSettingsModule } from '../policies/idp-settings/idp-settings.module'
import { LoginPolicyModule } from '../policies/login-policy/login-policy.module';
import { LoginTextsPolicyModule } from '../policies/login-texts/login-texts.module';
import { MessageTextsPolicyModule } from '../policies/message-texts/message-texts.module';
import { NotificationPolicyModule } from '../policies/notification-policy/notification-policy.module';
import { NotificationSettingsModule } from '../policies/notification-settings/notification-settings.module';
import { OIDCConfigurationModule } from '../policies/oidc-configuration/oidc-configuration.module';
import { PasswordComplexityPolicyModule } from '../policies/password-complexity-policy/password-complexity-policy.module';
@ -34,6 +35,7 @@ import { SettingsListComponent } from './settings-list.component';
PasswordLockoutPolicyModule,
PrivateLabelingPolicyModule,
GeneralSettingsModule,
NotificationPolicyModule,
IdpSettingsModule,
PrivacyPolicyModule,
MessageTextsPolicyModule,

View File

@ -92,6 +92,15 @@ export const NOTIFICATIONS: SidenavSetting = {
},
};
export const NOTIFICATION_POLICY: SidenavSetting = {
id: 'notifications',
i18nKey: 'SETTINGS.LIST.NOTIFICATIONS',
groupI18nKey: 'SETTINGS.GROUPS.NOTIFICATIONS',
requiredRoles: {
[PolicyComponentServiceType.MGMT]: ['policy.read'],
},
};
export const MESSAGETEXTS: SidenavSetting = {
id: 'messagetexts',
i18nKey: 'SETTINGS.LIST.MESSAGETEXTS',

View File

@ -11,6 +11,7 @@ import {
DOMAIN,
IDP,
LOCKOUT,
NOTIFICATION_POLICY,
LOGIN,
LOGINTEXTS,
MESSAGETEXTS,
@ -30,6 +31,7 @@ export class OrgSettingsComponent {
IDP,
COMPLEXITY,
LOCKOUT,
NOTIFICATION_POLICY,
DOMAIN,
BRANDING,
MESSAGETEXTS,

View File

@ -204,6 +204,18 @@ import {
GetSecurityPolicyResponse,
SetSecurityPolicyRequest,
SetSecurityPolicyResponse,
GetNotificationPolicyRequest,
GetNotificationPolicyResponse,
UpdateNotificationPolicyRequest,
UpdateNotificationPolicyResponse,
GetDefaultPasswordChangeMessageTextResponse,
GetDefaultPasswordChangeMessageTextRequest,
GetCustomPasswordChangeMessageTextResponse,
SetDefaultPasswordChangeMessageTextRequest,
SetDefaultPasswordChangeMessageTextResponse,
GetCustomPasswordChangeMessageTextRequest,
AddNotificationPolicyRequest,
AddNotificationPolicyResponse,
} from '../proto/generated/zitadel/admin_pb';
import { SearchQuery } from '../proto/generated/zitadel/member_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb';
@ -346,6 +358,24 @@ export class AdminService {
return this.grpcService.admin.setDefaultPasswordlessRegistrationMessageText(req, null).then((resp) => resp.toObject());
}
public getDefaultPasswordChangeMessageText(
req: GetDefaultPasswordChangeMessageTextRequest,
): Promise<GetDefaultPasswordChangeMessageTextResponse.AsObject> {
return this.grpcService.admin.getDefaultPasswordChangeMessageText(req, null).then((resp) => resp.toObject());
}
public getCustomPasswordChangeMessageText(
req: GetCustomPasswordChangeMessageTextRequest,
): Promise<GetCustomPasswordChangeMessageTextResponse.AsObject> {
return this.grpcService.admin.getCustomPasswordChangeMessageText(req, null).then((resp) => resp.toObject());
}
public setDefaultPasswordChangeMessageText(
req: SetDefaultPasswordChangeMessageTextRequest,
): Promise<SetDefaultPasswordChangeMessageTextResponse.AsObject> {
return this.grpcService.admin.setDefaultPasswordChangeMessageText(req, null).then((resp) => resp.toObject());
}
public SetUpOrg(org: SetUpOrgRequest.Org, human: SetUpOrgRequest.Human): Promise<SetUpOrgResponse.AsObject> {
const req = new SetUpOrgRequest();
@ -484,6 +514,21 @@ export class AdminService {
return this.grpcService.admin.setDefaultLanguage(req, null).then((resp) => resp.toObject());
}
/* notification policy */
public getNotificationPolicy(): Promise<GetNotificationPolicyResponse.AsObject> {
const req = new GetNotificationPolicyRequest();
return this.grpcService.admin.getNotificationPolicy(req, null).then((resp) => resp.toObject());
}
public updateNotificationPolicy(req: UpdateNotificationPolicyRequest): Promise<UpdateNotificationPolicyResponse.AsObject> {
return this.grpcService.admin.updateNotificationPolicy(req, null).then((resp) => resp.toObject());
}
public addNotificationPolicy(req: AddNotificationPolicyRequest): Promise<AddNotificationPolicyResponse.AsObject> {
return this.grpcService.admin.addNotificationPolicy(req, null).then((resp) => resp.toObject());
}
/* security policy */
public getSecurityPolicy(): Promise<GetSecurityPolicyResponse.AsObject> {

View File

@ -24,6 +24,8 @@ import {
AddCustomLockoutPolicyResponse,
AddCustomLoginPolicyRequest,
AddCustomLoginPolicyResponse,
AddCustomNotificationPolicyRequest,
AddCustomNotificationPolicyResponse,
AddCustomPasswordAgePolicyRequest,
AddCustomPasswordAgePolicyResponse,
AddCustomPasswordComplexityPolicyRequest,
@ -108,6 +110,8 @@ import {
GetCustomInitMessageTextResponse,
GetCustomLoginTextsRequest,
GetCustomLoginTextsResponse,
GetCustomPasswordChangeMessageTextRequest,
GetCustomPasswordChangeMessageTextResponse,
GetCustomPasswordlessRegistrationMessageTextRequest,
GetCustomPasswordlessRegistrationMessageTextResponse,
GetCustomPasswordResetMessageTextRequest,
@ -124,6 +128,8 @@ import {
GetDefaultLabelPolicyResponse,
GetDefaultLoginTextsRequest,
GetDefaultLoginTextsResponse,
GetDefaultPasswordChangeMessageTextRequest,
GetDefaultPasswordChangeMessageTextResponse,
GetDefaultPasswordComplexityPolicyRequest,
GetDefaultPasswordComplexityPolicyResponse,
GetDefaultPasswordlessRegistrationMessageTextRequest,
@ -156,6 +162,8 @@ import {
GetLoginPolicyResponse,
GetMyOrgRequest,
GetMyOrgResponse,
GetNotificationPolicyRequest,
GetNotificationPolicyResponse,
GetOIDCInformationRequest,
GetOIDCInformationResponse,
GetOrgByDomainGlobalRequest,
@ -343,6 +351,8 @@ import {
ResetCustomInitMessageTextToDefaultResponse,
ResetCustomLoginTextsToDefaultRequest,
ResetCustomLoginTextsToDefaultResponse,
ResetCustomPasswordChangeMessageTextToDefaultRequest,
ResetCustomPasswordChangeMessageTextToDefaultResponse,
ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest,
ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse,
ResetCustomPasswordResetMessageTextToDefaultRequest,
@ -357,6 +367,8 @@ import {
ResetLockoutPolicyToDefaultResponse,
ResetLoginPolicyToDefaultRequest,
ResetLoginPolicyToDefaultResponse,
ResetNotificationPolicyToDefaultRequest,
ResetNotificationPolicyToDefaultResponse,
ResetPasswordAgePolicyToDefaultRequest,
ResetPasswordAgePolicyToDefaultResponse,
ResetPasswordComplexityPolicyToDefaultRequest,
@ -403,6 +415,8 @@ import {
UpdateCustomLockoutPolicyResponse,
UpdateCustomLoginPolicyRequest,
UpdateCustomLoginPolicyResponse,
UpdateCustomNotificationPolicyRequest,
UpdateCustomNotificationPolicyResponse,
UpdateCustomPasswordAgePolicyRequest,
UpdateCustomPasswordAgePolicyResponse,
UpdateCustomPasswordComplexityPolicyRequest,
@ -631,6 +645,26 @@ export class ManagementService {
return this.grpcService.mgmt.getDefaultPasswordlessRegistrationMessageText(req, null).then((resp) => resp.toObject());
}
public getDefaultPasswordChangeMessageText(
req: GetDefaultPasswordChangeMessageTextRequest,
): Promise<GetDefaultPasswordChangeMessageTextResponse.AsObject> {
return this.grpcService.mgmt.getDefaultPasswordChangeMessageText(req, null).then((resp) => resp.toObject());
}
public getCustomPasswordChangeMessageText(
req: GetCustomPasswordChangeMessageTextRequest,
): Promise<GetCustomPasswordChangeMessageTextResponse.AsObject> {
return this.grpcService.mgmt.getCustomPasswordChangeMessageText(req, null).then((resp) => resp.toObject());
}
public resetCustomPasswordChangeMessageTextToDefault(
lang: string,
): Promise<ResetCustomPasswordChangeMessageTextToDefaultResponse.AsObject> {
const req = new ResetCustomPasswordChangeMessageTextToDefaultRequest();
req.setLanguage(lang);
return this.grpcService.mgmt.resetCustomPasswordChangeMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public getCustomPasswordlessRegistrationMessageText(
req: GetCustomPasswordlessRegistrationMessageTextRequest,
): Promise<GetCustomPasswordlessRegistrationMessageTextResponse.AsObject> {
@ -1371,6 +1405,30 @@ export class ManagementService {
}
}
/* notification policy */
public getNotificationPolicy(): Promise<GetNotificationPolicyResponse.AsObject> {
const req = new GetNotificationPolicyRequest();
return this.grpcService.mgmt.getNotificationPolicy(req, null).then((resp) => resp.toObject());
}
public resetNotificationPolicyToDefault(): Promise<ResetNotificationPolicyToDefaultResponse.AsObject> {
const req = new ResetNotificationPolicyToDefaultRequest();
return this.grpcService.mgmt.resetNotificationPolicyToDefault(req, null).then((resp) => resp.toObject());
}
public addCustomNotificationPolicy(
req: AddCustomNotificationPolicyRequest,
): Promise<AddCustomNotificationPolicyResponse.AsObject> {
return this.grpcService.mgmt.addCustomNotificationPolicy(req, null).then((resp) => resp.toObject());
}
public updateCustomNotificationPolicy(
req: UpdateCustomNotificationPolicyRequest,
): Promise<UpdateCustomNotificationPolicyResponse.AsObject> {
return this.grpcService.mgmt.updateCustomNotificationPolicy(req, null).then((resp) => resp.toObject());
}
public getUserByID(id: string): Promise<GetUserByIDResponse.AsObject> {
const req = new GetUserByIDRequest();
req.setId(id);

View File

@ -883,7 +883,7 @@
"LOGIN": "Loginverhalten und Sicherheit",
"LOCKOUT": "Sperrmechanismen",
"COMPLEXITY": "Passwordkomplexität",
"NOTIFICATIONS": "Benachrichtigungen",
"NOTIFICATIONS": "Benachrichtigungseinstellungen",
"NOTIFICATIONS_DESC": "SMTP und SMS Einstellungen",
"MESSAGETEXTS": "Benachrichtigungstexte",
"IDP": "Identity Provider",
@ -1007,6 +1007,11 @@
"NUMBERERROR": "Muss eine Ziffer beinhalten.",
"PATTERNERROR": "Das Passwort erfüllt nicht die vorgeschriebene Richtlinie."
},
"NOTIFICATION": {
"TITLE": "Notification",
"DESCRIPTION": "Legt fest, bei welchen Änderungen Benachrichtigungen gesendet werden",
"PASSWORDCHANGE": "Passwordänderung"
},
"PRIVATELABELING": {
"TITLE": "Branding",
"DESCRIPTION": "Verleihen Sie dem Login Ihren benutzerdefinierten Style und passen Sie das Verhalten an.",
@ -1146,7 +1151,8 @@
"VP": "Telefonnummerverifikation",
"PR": "Passwort Wiederherstellung",
"DC": "Domainbeanspruchung",
"PL": "Passwortlos"
"PL": "Passwortlos",
"PC": "Passwordwechsel"
},
"CHIPS": {
"firstname": "Vorname",

View File

@ -883,7 +883,7 @@
"LOGIN": "Login Behavior and Security",
"LOCKOUT": "Lockout",
"COMPLEXITY": "Password complexity",
"NOTIFICATIONS": "Notification providers and SMTP",
"NOTIFICATIONS": "Notification settings",
"NOTIFICATIONS_DESC": "SMTP and SMS Settings",
"MESSAGETEXTS": "Message Texts",
"IDP": "Identity Providers",
@ -1007,6 +1007,11 @@
"NUMBERERROR": "Must include a digit.",
"PATTERNERROR": "The password does not meet the required pattern."
},
"NOTIFICATION": {
"TITLE": "Notification",
"DESCRIPTION": "Determines on which changes, notifications will be sent.",
"PASSWORDCHANGE": "Password change"
},
"PRIVATELABELING": {
"TITLE": "Branding",
"DESCRIPTION": "Give the login your personalized style and modify its behavior.",
@ -1146,7 +1151,8 @@
"VP": "Verify Phone",
"PR": "Password Reset",
"DC": "Domain Claim",
"PL": "Passwordless"
"PL": "Passwordless",
"PC": "Password Change"
},
"CHIPS": {
"firstname": "Firstname",

View File

@ -883,7 +883,7 @@
"LOGIN": "Comportement de connexion et sécurité",
"LOCKOUT": "Verrouillage",
"COMPLEXITY": "Complexité du mot de passe",
"NOTIFICATIONS": "Fournisseurs de notifications et SMTP",
"NOTIFICATIONS": "Paramètres de notification",
"NOTIFICATIONS_DESC": "Paramètres SMTP et SMS",
"MESSAGETEXTS": "Textes des messages",
"IDP": "Fournisseurs d'identité",
@ -1007,6 +1007,11 @@
"NUMBERERROR": "Doit inclure un chiffre.",
"PATTERNERROR": "Le mot de passe ne correspond pas au modèle requis."
},
"NOTIFICATION": {
"TITLE": "Notifications",
"DESCRIPTION": "Détermine sur quels changements, les notifications seront envoyées",
"PASSWORDCHANGE": "Changement de mot de passe"
},
"PRIVATELABELING": {
"TITLE": "Image de marque",
"DESCRIPTION": "Donnez au login votre style personnalisé et modifiez son comportement.",
@ -1146,7 +1151,8 @@
"VP": "Vérifier le téléphone",
"PR": "Réinitialisation du mot de passe",
"DC": "Réclamation de domaine",
"PL": "Sans mot de passe"
"PL": "Sans mot de passe",
"PC": "Changement de mot de passe"
},
"CHIPS": {
"firstname": "Prénom",

View File

@ -884,7 +884,7 @@
"LOGIN": "Comportamento login e sicurezza",
"LOCKOUT": "Meccanismi di bloccaggio",
"COMPLEXITY": "Complessità della password",
"NOTIFICATIONS": "Notifiche",
"NOTIFICATIONS": "Impostazioni di notifica",
"NOTIFICATIONS_DESC": "Impostazioni SMTP e SMS",
"MESSAGETEXTS": "Testi di notifica",
"IDP": "Identity Providers",
@ -1008,6 +1008,11 @@
"NUMBERERROR": "Deve includere una cifra.",
"PATTERNERROR": "La password non corrisponde al modello richiesto."
},
"NOTIFICATION": {
"TITLE": "Notifiche",
"DESCRIPTION": "Determina su quali modifiche verranno inviate le notifiche",
"PASSWORDCHANGE": "Cambiamento della password"
},
"PRIVATELABELING": {
"TITLE": "Branding",
"DESCRIPTION": "Dai al login il tuo stile personalizzato e modifica il suo comportamento.",
@ -1147,7 +1152,8 @@
"VP": "Verificazione del telefono",
"PR": "Ripristino della password",
"DC": "Rivendicazione del dominio",
"PL": "Autenticazione Passwordless"
"PL": "Autenticazione Passwordless",
"PC": "Cambiamento della password"
},
"CHIPS": {
"firstname": "Nome",

View File

@ -883,7 +883,7 @@
"LOGIN": "登录行为和安全",
"LOCKOUT": "安全锁策略",
"COMPLEXITY": "密码复杂性",
"NOTIFICATIONS": "通知服务商",
"NOTIFICATIONS": "通知设置",
"NOTIFICATIONS_DESC": "SMTP 和 SMS 设置",
"MESSAGETEXTS": "消息文本",
"IDP": "身份提供者",
@ -1007,6 +1007,11 @@
"NUMBERERROR": "密码必须包含数字。",
"PATTERNERROR": "密码不符合要求。"
},
"NOTIFICATION": {
"TITLE": "通知",
"DESCRIPTION": "确定将发送哪些更改、通知",
"PASSWORDCHANGE": "更改密码"
},
"PRIVATELABELING": {
"TITLE": "品牌标识",
"DESCRIPTION": "为登录提供您的个性化风格并修改其行为。",
@ -1145,7 +1150,8 @@
"VP": "验证手机号码",
"PR": "重置密码",
"DC": "域名声明",
"PL": "无密码身份验证"
"PL": "无密码身份验证",
"PC": "修改密码"
},
"CHIPS": {
"firstname": "名",

View File

@ -1072,6 +1072,44 @@ Variable {{.Lang}} can be set to have different links based on the language
PUT: /policies/privacy
### AddNotificationPolicy
> **rpc** AddNotificationPolicy([AddNotificationPolicyRequest](#addnotificationpolicyrequest))
[AddNotificationPolicyResponse](#addnotificationpolicyresponse)
Add a default notification policy for ZITADEL
it impacts all organisations without a customised policy
POST: /policies/notification
### GetNotificationPolicy
> **rpc** GetNotificationPolicy([GetNotificationPolicyRequest](#getnotificationpolicyrequest))
[GetNotificationPolicyResponse](#getnotificationpolicyresponse)
Returns the notification policy defined by the administrators of ZITADEL
GET: /policies/notification
### UpdateNotificationPolicy
> **rpc** UpdateNotificationPolicy([UpdateNotificationPolicyRequest](#updatenotificationpolicyrequest))
[UpdateNotificationPolicyResponse](#updatenotificationpolicyresponse)
Updates the default notification policy of ZITADEL
it impacts all organisations without a customised policy
PUT: /policies/notification
### GetDefaultInitMessageText
> **rpc** GetDefaultInitMessageText([GetDefaultInitMessageTextRequest](#getdefaultinitmessagetextrequest))
@ -1104,7 +1142,7 @@ Returns the custom text for initial message (overwritten in eventstore)
Sets the default custom text for initial message
it impacts all organisations without customized initial message text
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -1156,7 +1194,7 @@ Returns the custom text for password reset message (overwritten in eventstore)
Sets the default custom text for password reset message
it impacts all organisations without customized password reset message text
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -1208,7 +1246,7 @@ Returns the custom text for verify email message (overwritten in eventstore)
Sets the default custom text for verify email message
it impacts all organisations without customized verify email message text
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -1260,7 +1298,7 @@ Returns the custom text for verify phone message
Sets the default custom text for verify phone message
it impacts all organisations without customized verify phone message text
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -1309,10 +1347,10 @@ Returns the custom text for domain claimed message (overwritten in eventstore)
> **rpc** SetDefaultDomainClaimedMessageText([SetDefaultDomainClaimedMessageTextRequest](#setdefaultdomainclaimedmessagetextrequest))
[SetDefaultDomainClaimedMessageTextResponse](#setdefaultdomainclaimedmessagetextresponse)
Sets the default custom text for domain claimed phone message
Sets the default custom text for domain claimed message
it impacts all organisations without customized domain claimed message text
The Following Variables can be used:
{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -1364,7 +1402,7 @@ Returns the custom text for passwordless registration message (overwritten in ev
Sets the default custom text for passwordless registration message
it impacts all organisations without customized passwordless registration message text
The Following Variables can be used:
{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -1384,6 +1422,58 @@ The default text from the translation file will trigger after
DELETE: /text/message/passwordless_registration/{language}
### GetDefaultPasswordChangeMessageText
> **rpc** GetDefaultPasswordChangeMessageText([GetDefaultPasswordChangeMessageTextRequest](#getdefaultpasswordchangemessagetextrequest))
[GetDefaultPasswordChangeMessageTextResponse](#getdefaultpasswordchangemessagetextresponse)
Returns the default text for password change message (translation file)
GET: /text/default/message/password_change/{language}
### GetCustomPasswordChangeMessageText
> **rpc** GetCustomPasswordChangeMessageText([GetCustomPasswordChangeMessageTextRequest](#getcustompasswordchangemessagetextrequest))
[GetCustomPasswordChangeMessageTextResponse](#getcustompasswordchangemessagetextresponse)
Returns the custom text for password change message (overwritten in eventstore)
GET: /text/message/password_change/{language}
### SetDefaultPasswordChangeMessageText
> **rpc** SetDefaultPasswordChangeMessageText([SetDefaultPasswordChangeMessageTextRequest](#setdefaultpasswordchangemessagetextrequest))
[SetDefaultPasswordChangeMessageTextResponse](#setdefaultpasswordchangemessagetextresponse)
Sets the default custom text for password change message
it impacts all organisations without customized password change message text
The Following Variables can be used:
{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
PUT: /text/message/password_change/{language}
### ResetCustomPasswordChangeMessageTextToDefault
> **rpc** ResetCustomPasswordChangeMessageTextToDefault([ResetCustomPasswordChangeMessageTextToDefaultRequest](#resetcustompasswordchangemessagetexttodefaultrequest))
[ResetCustomPasswordChangeMessageTextToDefaultResponse](#resetcustompasswordchangemessagetexttodefaultresponse)
Removes the custom password change message text of the system
The default text from the translation file will trigger after
DELETE: /text/message/password_change/{language}
### GetDefaultLoginTexts
> **rpc** GetDefaultLoginTexts([GetDefaultLoginTextsRequest](#getdefaultlogintextsrequest))
@ -1793,6 +1883,28 @@ This is an empty request
### AddNotificationPolicyRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| password_change | bool | - | |
### AddNotificationPolicyResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### AddOIDCIDPRequest
@ -2210,6 +2322,28 @@ This is an empty request
### GetCustomPasswordChangeMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetCustomPasswordChangeMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| custom_text | zitadel.text.v1.MessageCustomText | - | |
### GetCustomPasswordResetMessageTextRequest
@ -2398,6 +2532,28 @@ This is an empty request
### GetDefaultPasswordChangeMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetDefaultPasswordChangeMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| custom_text | zitadel.text.v1.MessageCustomText | - | |
### GetDefaultPasswordResetMessageTextRequest
@ -2627,6 +2783,23 @@ This is an empty request
### GetNotificationPolicyRequest
This is an empty request
### GetNotificationPolicyResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| policy | zitadel.policy.v1.NotificationPolicy | - | |
### GetOIDCSettingsRequest
This is an empty request
@ -3839,6 +4012,28 @@ this is en empty request
### ResetCustomPasswordChangeMessageTextToDefaultRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### ResetCustomPasswordChangeMessageTextToDefaultResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### ResetCustomPasswordResetMessageTextToDefaultRequest
@ -4085,6 +4280,35 @@ this is en empty request
### SetDefaultPasswordChangeMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| title | string | - | string.max_len: 200<br /> |
| pre_header | string | - | string.max_len: 200<br /> |
| subject | string | - | string.max_len: 200<br /> |
| greeting | string | - | string.max_len: 200<br /> |
| text | string | - | string.max_len: 800<br /> |
| button_text | string | - | string.max_len: 200<br /> |
| footer_text | string | - | string.max_len: 200<br /> |
### SetDefaultPasswordChangeMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### SetDefaultPasswordResetMessageTextRequest
@ -4581,6 +4805,28 @@ this is en empty request
### UpdateNotificationPolicyRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| password_change | bool | - | |
### UpdateNotificationPolicyResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### UpdateOIDCSettingsRequest

View File

@ -2231,7 +2231,7 @@ Variable {{.Lang}} can be set to have different links based on the language
> **rpc** UpdateCustomPrivacyPolicy([UpdateCustomPrivacyPolicyRequest](#updatecustomprivacypolicyrequest))
[UpdateCustomPrivacyPolicyResponse](#updatecustomprivacypolicyresponse)
Update the privacy complexity policy for the organisation
Update the privacy policy for the organisation
With this policy privacy relevant things can be configured (e.g. tos link)
Variable {{.Lang}} can be set to have different links based on the language
@ -2253,6 +2253,71 @@ The default policy of the IAM will trigger after
DELETE: /policies/privacy
### GetNotificationPolicy
> **rpc** GetNotificationPolicy([GetNotificationPolicyRequest](#getnotificationpolicyrequest))
[GetNotificationPolicyResponse](#getnotificationpolicyresponse)
Returns the notification policy of the organisation
With this notification policy it can be configured how users should be notified
GET: /policies/notification
### GetDefaultNotificationPolicy
> **rpc** GetDefaultNotificationPolicy([GetDefaultNotificationPolicyRequest](#getdefaultnotificationpolicyrequest))
[GetDefaultNotificationPolicyResponse](#getdefaultnotificationpolicyresponse)
Returns the default notification policy of the IAM
With this notification privacy it can be configured how users should be notified
GET: /policies/default/notification
### AddCustomNotificationPolicy
> **rpc** AddCustomNotificationPolicy([AddCustomNotificationPolicyRequest](#addcustomnotificationpolicyrequest))
[AddCustomNotificationPolicyResponse](#addcustomnotificationpolicyresponse)
Add a custom notification policy for the organisation
With this notification privacy it can be configured how users should be notified
POST: /policies/notification
### UpdateCustomNotificationPolicy
> **rpc** UpdateCustomNotificationPolicy([UpdateCustomNotificationPolicyRequest](#updatecustomnotificationpolicyrequest))
[UpdateCustomNotificationPolicyResponse](#updatecustomnotificationpolicyresponse)
Update the notification policy for the organisation
With this notification privacy it can be configured how users should be notified
PUT: /policies/notification
### ResetNotificationPolicyToDefault
> **rpc** ResetNotificationPolicyToDefault([ResetNotificationPolicyToDefaultRequest](#resetnotificationpolicytodefaultrequest))
[ResetNotificationPolicyToDefaultResponse](#resetnotificationpolicytodefaultresponse)
Removes the notification policy of the organisation
The default policy of the IAM will trigger after
DELETE: /policies/notification
### GetLabelPolicy
> **rpc** GetLabelPolicy([GetLabelPolicyRequest](#getlabelpolicyrequest))
@ -2485,7 +2550,7 @@ Returns the default text for password reset message
Sets the custom text for password reset message
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -2536,7 +2601,7 @@ Returns the default text for verify email message
Sets the custom text for verify email message
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -2587,7 +2652,7 @@ Returns the custom text for verify email message
Sets the default custom text for verify email message
The Following Variables can be used:
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -2638,7 +2703,7 @@ Returns the custom text for domain claimed message
Sets the custom text for domain claimed message
The Following Variables can be used:
{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -2689,7 +2754,7 @@ Returns the custom text for passwordless link message
Sets the custom text for passwordless link message
The Following Variables can be used:
{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
@ -2709,6 +2774,57 @@ The default text of the IAM will trigger after
DELETE: /text/message/passwordless_registration/{language}
### GetCustomPasswordChangeMessageText
> **rpc** GetCustomPasswordChangeMessageText([GetCustomPasswordChangeMessageTextRequest](#getcustompasswordchangemessagetextrequest))
[GetCustomPasswordChangeMessageTextResponse](#getcustompasswordchangemessagetextresponse)
Returns the custom text for password change message
GET: /text/message/password_change/{language}
### GetDefaultPasswordChangeMessageText
> **rpc** GetDefaultPasswordChangeMessageText([GetDefaultPasswordChangeMessageTextRequest](#getdefaultpasswordchangemessagetextrequest))
[GetDefaultPasswordChangeMessageTextResponse](#getdefaultpasswordchangemessagetextresponse)
Returns the custom text for password change link message
GET: /text/default/message/password_change/{language}
### SetCustomPasswordChangeMessageCustomText
> **rpc** SetCustomPasswordChangeMessageCustomText([SetCustomPasswordChangeMessageTextRequest](#setcustompasswordchangemessagetextrequest))
[SetCustomPasswordChangeMessageTextResponse](#setcustompasswordchangemessagetextresponse)
Sets the custom text for password change message
The Following Variables can be used:
{{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
PUT: /text/message/password_change/{language}
### ResetCustomPasswordChangeMessageTextToDefault
> **rpc** ResetCustomPasswordChangeMessageTextToDefault([ResetCustomPasswordChangeMessageTextToDefaultRequest](#resetcustompasswordchangemessagetexttodefaultrequest))
[ResetCustomPasswordChangeMessageTextToDefaultResponse](#resetcustompasswordchangemessagetexttodefaultresponse)
Removes the custom password change message text of the organisation
The default text of the IAM will trigger after
DELETE: /text/message/password_change/{language}
### GetCustomLoginTexts
> **rpc** GetCustomLoginTexts([GetCustomLoginTextsRequest](#getcustomlogintextsrequest))
@ -3226,6 +3342,28 @@ This is an empty request
### AddCustomNotificationPolicyRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| password_change | bool | - | |
### AddCustomNotificationPolicyResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### AddCustomPasswordAgePolicyRequest
@ -4446,6 +4584,28 @@ This is an empty request
### GetCustomPasswordChangeMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetCustomPasswordChangeMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| custom_text | zitadel.text.v1.MessageCustomText | - | |
### GetCustomPasswordResetMessageTextRequest
@ -4651,6 +4811,23 @@ This is an empty request
### GetDefaultNotificationPolicyRequest
This is an empty request
### GetDefaultNotificationPolicyResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| policy | zitadel.policy.v1.NotificationPolicy | - | |
### GetDefaultPasswordAgePolicyRequest
This is an empty request
@ -4668,6 +4845,28 @@ This is an empty request
### GetDefaultPasswordChangeMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### GetDefaultPasswordChangeMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| custom_text | zitadel.text.v1.MessageCustomText | - | |
### GetDefaultPasswordComplexityPolicyRequest
This is an empty request
@ -5034,6 +5233,23 @@ This is an empty request
### GetNotificationPolicyRequest
This is an empty request
### GetNotificationPolicyResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| policy | zitadel.policy.v1.NotificationPolicy | - | |
### GetOIDCInformationRequest
This is an empty request
@ -7442,6 +7658,28 @@ This is an empty request
### ResetCustomPasswordChangeMessageTextToDefaultRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### ResetCustomPasswordChangeMessageTextToDefaultResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### ResetCustomPasswordResetMessageTextToDefaultRequest
@ -7581,6 +7819,23 @@ This is an empty request
### ResetNotificationPolicyToDefaultRequest
This is an empty request
### ResetNotificationPolicyToDefaultResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### ResetPasswordAgePolicyToDefaultRequest
This is an empty request
@ -7791,6 +8046,35 @@ This is an empty request
### SetCustomPasswordChangeMessageTextRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| language | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| title | string | - | string.max_len: 200<br /> |
| pre_header | string | - | string.max_len: 200<br /> |
| subject | string | - | string.max_len: 200<br /> |
| greeting | string | - | string.max_len: 200<br /> |
| text | string | - | string.max_len: 800<br /> |
| button_text | string | - | string.max_len: 200<br /> |
| footer_text | string | - | string.max_len: 200<br /> |
### SetCustomPasswordChangeMessageTextResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### SetCustomPasswordResetMessageTextRequest
@ -8234,6 +8518,28 @@ This is an empty request
### UpdateCustomNotificationPolicyRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| password_change | bool | - | |
### UpdateCustomNotificationPolicyResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### UpdateCustomPasswordAgePolicyRequest

View File

@ -95,6 +95,19 @@ title: zitadel/policy.proto
### NotificationPolicy
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
| is_default | bool | - | |
| password_change | bool | - | |
### OrgIAMPolicy
deprecated: please use DomainPolicy instead

View File

@ -15,7 +15,7 @@ To access instance settings, use the instance page at `{instanceDomain}/ui/conso
When you configure your instance, you can set the following:
- **General**: Default Language for the UI
- [**Notification providers and SMTP**](#notification-providers-and-smtp): Email Server settings, so initialization-, verification- and other mails are sent from your own domain. For SMS, Twilio is supported as notification provider.
- [**Notification settings**](#notification-providers-and-smtp): Notification and Email Server settings, so initialization-, verification- and other mails are sent from your own domain. For SMS, Twilio is supported as notification provider.
- [**Login Behaviour and Access**](#login-behaviour-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface.
- [**Identity Providers**](#identity-providers): Define IDPs which are available for all organizations
- [**Password Complexity**](#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more.
@ -48,9 +48,16 @@ Make sure you click the "Apply configuration" button after you finish your confi
Branding settings applied on you instance act as a default for all your organizations. If you need custom branding on a organization take a look at our guide under [organization settiong](./organizations#branding).
## Notification providers and SMTP
## Notification settings
In the notification settings you can configure your SMTP Server settings and your SMS Provider. At the moment Twilio is available as SMS provider.
In the notification settings you can configure when to notify users about certain events and you can customize your SMTP Server settings and your SMS Provider.
At the moment Twilio is available as SMS provider.
### Notification
You can configure on which changes the users will be notified. The text of the message can be changed in the [Message texts](#message-texts)
<img src="/docs/img/guides/console/notification.png" alt="Notification" width="400px" />
### SMTP
@ -197,13 +204,14 @@ Example:
These are the texts for your notification mails. Available for change are:
| Message Text | Description |
| -------------- | ---------------------------------------------------------------------------------------------------------------- |
| Domain Claim | Enable self register possibility in the login ui |
| Initialization | The mail after a user has been created. A code is part of the message which then must be verified on first login |
| Passwordless | Possibility to login with an external identity (e.g Google, Microsoft, Apple, etc) |
| Password Reset | Force a user to register and use a multifactor authentication |
| Verify Email | Choose if passwordless login is allowed or not |
| Message Text | Description |
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Domain Claim | The Mail after an organisation claimed a domain for itself. Users on other organisations with this domain will be notified |
| Initialization | The mail after a user has been created. A code is part of the message which then must be verified on first login |
| Passwordless | The Mail to register an additional passwordless device by a link |
| Password Reset | The Mail to reset the password by a link |
| Verify Email | The mail after the email has been changed. A code is part of the message which then must be verified on the next login |
| Password Change | Notify the user, that the password has been changed. Can be configured in [Notification](#notification) |
You can set the locale of the translations on the right.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -252,6 +252,54 @@ func (s *Server) ResetCustomDomainClaimedMessageTextToDefault(ctx context.Contex
}, nil
}
func (s *Server) GetDefaultPasswordChangeMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordChangeMessageTextRequest) (*admin_pb.GetDefaultPasswordChangeMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordChangeMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomPasswordChangeMessageText(ctx context.Context, req *admin_pb.GetCustomPasswordChangeMessageTextRequest) (*admin_pb.GetCustomPasswordChangeMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.PasswordChangeMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultPasswordChangeMessageText(ctx context.Context, req *admin_pb.SetDefaultPasswordChangeMessageTextRequest) (*admin_pb.SetDefaultPasswordChangeMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetPasswordChangeCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultPasswordChangeMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomPasswordChangeMessageTextToDefaultRequest) (*admin_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.PasswordChangeMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetDefaultPasswordlessRegistrationMessageText(ctx context.Context, req *admin_pb.GetDefaultPasswordlessRegistrationMessageTextRequest) (*admin_pb.GetDefaultPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.PasswordlessRegistrationMessageType, req.Language)
if err != nil {

View File

@ -83,6 +83,21 @@ func SetDomainClaimedCustomTextToDomain(msg *admin_pb.SetDefaultDomainClaimedMes
}
}
func SetPasswordChangeCustomTextToDomain(msg *admin_pb.SetDefaultPasswordChangeMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordChangeMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetPasswordlessRegistrationCustomTextToDomain(msg *admin_pb.SetDefaultPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{

View File

@ -0,0 +1,46 @@
package admin
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
func (s *Server) AddNotificationPolicy(ctx context.Context, req *admin_pb.AddNotificationPolicyRequest) (*admin_pb.AddNotificationPolicyResponse, error) {
result, err := s.command.AddDefaultNotificationPolicy(ctx, authz.GetInstance(ctx).InstanceID(), req.GetPasswordChange())
if err != nil {
return nil, err
}
return &admin_pb.AddNotificationPolicyResponse{
Details: object.AddToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetNotificationPolicy(ctx context.Context, _ *admin_pb.GetNotificationPolicyRequest) (*admin_pb.GetNotificationPolicyResponse, error) {
policy, err := s.query.DefaultNotificationPolicy(ctx, true)
if err != nil {
return nil, err
}
return &admin_pb.GetNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil
}
func (s *Server) UpdateNotificationPolicy(ctx context.Context, req *admin_pb.UpdateNotificationPolicyRequest) (*admin_pb.UpdateNotificationPolicyResponse, error) {
result, err := s.command.ChangeDefaultNotificationPolicy(ctx, authz.GetInstance(ctx).InstanceID(), req.GetPasswordChange())
if err != nil {
return nil, err
}
return &admin_pb.UpdateNotificationPolicyResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}

View File

@ -252,6 +252,54 @@ func (s *Server) ResetCustomDomainClaimedMessageTextToDefault(ctx context.Contex
}, nil
}
func (s *Server) GetCustomPasswordChangeMessageText(ctx context.Context, req *mgmt_pb.GetCustomPasswordChangeMessageTextRequest) (*mgmt_pb.GetCustomPasswordChangeMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordChangeMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &mgmt_pb.GetCustomPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetDefaultPasswordChangeMessageText(ctx context.Context, req *mgmt_pb.GetDefaultPasswordChangeMessageTextRequest) (*mgmt_pb.GetDefaultPasswordChangeMessageTextResponse, error) {
msg, err := s.query.IAMMessageTextByTypeAndLanguage(ctx, domain.PasswordChangeMessageType, req.Language)
if err != nil {
return nil, err
}
return &mgmt_pb.GetDefaultPasswordChangeMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetCustomPasswordChangeMessageCustomText(ctx context.Context, req *mgmt_pb.SetCustomPasswordChangeMessageTextRequest) (*mgmt_pb.SetCustomPasswordChangeMessageTextResponse, error) {
result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetPasswordChangeCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &mgmt_pb.SetCustomPasswordChangeMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Context, req *mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultRequest) (*mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveOrgMessageTexts(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordChangeMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &mgmt_pb.ResetCustomPasswordChangeMessageTextToDefaultResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) GetCustomPasswordlessRegistrationMessageText(ctx context.Context, req *mgmt_pb.GetCustomPasswordlessRegistrationMessageTextRequest) (*mgmt_pb.GetCustomPasswordlessRegistrationMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetCtxData(ctx).OrgID, domain.PasswordlessRegistrationMessageType, req.Language, false)
if err != nil {

View File

@ -83,6 +83,21 @@ func SetDomainClaimedCustomTextToDomain(msg *mgmt_pb.SetCustomDomainClaimedMessa
}
}
func SetPasswordChangeCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordChangeMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.PasswordChangeMessageType,
Language: langTag,
Title: msg.Title,
PreHeader: msg.PreHeader,
Subject: msg.Subject,
Greeting: msg.Greeting,
Text: msg.Text,
ButtonText: msg.ButtonText,
FooterText: msg.FooterText,
}
}
func SetPasswordlessRegistrationCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordlessRegistrationMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{

View File

@ -0,0 +1,64 @@
package management
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object"
policy_grpc "github.com/zitadel/zitadel/internal/api/grpc/policy"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
)
func (s *Server) GetNotificationPolicy(ctx context.Context, _ *mgmt_pb.GetNotificationPolicyRequest) (*mgmt_pb.GetNotificationPolicyResponse, error) {
policy, err := s.query.NotificationPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID, false)
if err != nil {
return nil, err
}
return &mgmt_pb.GetNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil
}
func (s *Server) GetDefaultNotificationPolicy(ctx context.Context, _ *mgmt_pb.GetDefaultNotificationPolicyRequest) (*mgmt_pb.GetDefaultNotificationPolicyResponse, error) {
policy, err := s.query.DefaultNotificationPolicy(ctx, true)
if err != nil {
return nil, err
}
return &mgmt_pb.GetDefaultNotificationPolicyResponse{Policy: policy_grpc.ModelNotificationPolicyToPb(policy)}, nil
}
func (s *Server) AddCustomNotificationPolicy(ctx context.Context, req *mgmt_pb.AddCustomNotificationPolicyRequest) (*mgmt_pb.AddCustomNotificationPolicyResponse, error) {
result, err := s.command.AddNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID, req.GetPasswordChange())
if err != nil {
return nil, err
}
return &mgmt_pb.AddCustomNotificationPolicyResponse{
Details: object.AddToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) UpdateCustomNotificationPolicy(ctx context.Context, req *mgmt_pb.UpdateCustomNotificationPolicyRequest) (*mgmt_pb.UpdateCustomNotificationPolicyResponse, error) {
result, err := s.command.ChangeNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID, req.GetPasswordChange())
if err != nil {
return nil, err
}
return &mgmt_pb.UpdateCustomNotificationPolicyResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetNotificationPolicyToDefault(ctx context.Context, _ *mgmt_pb.ResetNotificationPolicyToDefaultRequest) (*mgmt_pb.ResetNotificationPolicyToDefaultResponse, error) {
objectDetails, err := s.command.RemoveNotificationPolicy(ctx, authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
return &mgmt_pb.ResetNotificationPolicyToDefaultResponse{
Details: object.DomainToChangeDetailsPb(objectDetails),
}, nil
}

View File

@ -0,0 +1,20 @@
package policy
import (
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/query"
policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy"
)
func ModelNotificationPolicyToPb(policy *query.NotificationPolicy) *policy_pb.NotificationPolicy {
return &policy_pb.NotificationPolicy{
IsDefault: policy.IsDefault,
PasswordChange: policy.PasswordChange,
Details: object.ToViewDetailsPb(
policy.Sequence,
policy.CreationDate,
policy.ChangeDate,
policy.ResourceOwner,
),
}
}

View File

@ -52,6 +52,10 @@ var (
}
)
func LoginHintLink(origin, username string) string {
return origin + HandlerPrefix + "?login_hint=" + username
}
func (i *spaHandler) Open(name string) (http.File, error) {
ret, err := i.fileSystem.Open(name)
if !os.IsNotExist(err) || path.Ext(name) != "" {

View File

@ -82,6 +82,9 @@ type InstanceSetup struct {
SecondFactorCheckLifetime time.Duration
MultiFactorCheckLifetime time.Duration
}
NotificationPolicy struct {
PasswordChange bool
}
PrivacyPolicy struct {
TOSLink string
PrivacyLink string
@ -236,6 +239,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
prepareAddMultiFactorToDefaultLoginPolicy(instanceAgg, domain.MultiFactorTypeU2FWithPIN),
prepareAddDefaultPrivacyPolicy(instanceAgg, setup.PrivacyPolicy.TOSLink, setup.PrivacyPolicy.PrivacyLink, setup.PrivacyPolicy.HelpLink),
prepareAddDefaultNotificationPolicy(instanceAgg, setup.NotificationPolicy.PasswordChange),
prepareAddDefaultLockoutPolicy(instanceAgg, setup.LockoutPolicy.MaxAttempts, setup.LockoutPolicy.ShouldShowLockoutFailure),
prepareAddDefaultLabelPolicy(

View File

@ -0,0 +1,92 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
)
func (c *Commands) AddDefaultNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddDefaultNotificationPolicy(instanceAgg, passwordChange))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func (c *Commands) ChangeDefaultNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) {
instanceAgg := instance.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeDefaultNotificationPolicy(instanceAgg, passwordChange))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func prepareAddDefaultNotificationPolicy(
a *instance.Aggregate,
passwordChange bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewInstanceNotificationPolicyWriteModel(ctx)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateActive {
return nil, caos_errs.ThrowAlreadyExists(nil, "INSTANCE-xpo1bj", "Errors.Instance.NotificationPolicy.AlreadyExists")
}
return []eventstore.Command{
instance.NewNotificationPolicyAddedEvent(ctx, &a.Aggregate, passwordChange),
}, nil
}, nil
}
}
func prepareChangeDefaultNotificationPolicy(
a *instance.Aggregate,
passwordChange bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewInstanceNotificationPolicyWriteModel(ctx)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-x891na", "Errors.IAM.NotificationPolicy.NotFound")
}
change, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, passwordChange)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-29x02n", "Errors.IAM.NotificationPolicy.NotChanged")
}
return []eventstore.Command{
change,
}, nil
}, nil
}
}

View File

@ -0,0 +1,72 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/policy"
)
type InstanceNotificationPolicyWriteModel struct {
NotificationPolicyWriteModel
}
func NewInstanceNotificationPolicyWriteModel(ctx context.Context) *InstanceNotificationPolicyWriteModel {
return &InstanceNotificationPolicyWriteModel{
NotificationPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: authz.GetInstance(ctx).InstanceID(),
ResourceOwner: authz.GetInstance(ctx).InstanceID(),
},
},
}
}
func (wm *InstanceNotificationPolicyWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *instance.NotificationPolicyAddedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyAddedEvent)
case *instance.NotificationPolicyChangedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyChangedEvent)
}
}
}
func (wm *InstanceNotificationPolicyWriteModel) Reduce() error {
return wm.NotificationPolicyWriteModel.Reduce()
}
func (wm *InstanceNotificationPolicyWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(instance.AggregateType).
AggregateIDs(wm.NotificationPolicyWriteModel.AggregateID).
EventTypes(
instance.NotificationPolicyAddedEventType,
instance.NotificationPolicyChangedEventType).
Builder()
}
func (wm *InstanceNotificationPolicyWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
passwordChange bool,
) (*instance.NotificationPolicyChangedEvent, bool) {
changes := make([]policy.NotificationPolicyChanges, 0)
if wm.PasswordChange != passwordChange {
changes = append(changes, policy.ChangePasswordChange(passwordChange))
}
if len(changes) == 0 {
return nil, false
}
changedEvent, err := instance.NewNotificationPolicyChangedEvent(ctx, aggregate, changes)
if err != nil {
return nil, false
}
return changedEvent, true
}

View File

@ -0,0 +1,260 @@
package command
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/policy"
)
func TestCommandSide_AddDefaultNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
resourceOwner string
passwordChange bool
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "notification policy already existing, already exists error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
},
},
{
name: "add policy,ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
{
name: "add empty policy,ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.AddDefaultNotificationPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.passwordChange)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_ChangeDefaultNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
resourceOwner string
passwordChange bool
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "privacy policy not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
instance.NewNotificationPolicyAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate,
false,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newDefaultNotificationPolicyChangedEvent(context.Background(),
true,
)),
},
),
),
},
args: args{
ctx: context.Background(),
resourceOwner: "INSTANCE",
passwordChange: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "INSTANCE",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.ChangeDefaultNotificationPolicy(tt.args.ctx, tt.args.resourceOwner, tt.args.passwordChange)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func newDefaultNotificationPolicyChangedEvent(ctx context.Context, passwordChange bool) *instance.NotificationPolicyChangedEvent {
event, _ := instance.NewNotificationPolicyChangedEvent(ctx,
&instance.NewAggregate("INSTANCE").Aggregate,
[]policy.NotificationPolicyChanges{
policy.ChangePasswordChange(passwordChange),
},
)
return event
}

View File

@ -49,5 +49,6 @@ func orgWriteModelToPrivacyPolicy(wm *OrgPrivacyPolicyWriteModel) *domain.Privac
ObjectRoot: writeModelToObjectRoot(wm.PrivacyPolicyWriteModel.WriteModel),
TOSLink: wm.TOSLink,
PrivacyLink: wm.PrivacyLink,
HelpLink: wm.HelpLink,
}
}

View File

@ -0,0 +1,139 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
)
func (c *Commands) AddNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x801sk2i", "Errors.ResourceOwnerMissing")
}
orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareAddNotificationPolicy(orgAgg, passwordChange))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func prepareAddNotificationPolicy(
a *org.Aggregate,
passwordChange bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateActive {
return nil, caos_errs.ThrowAlreadyExists(nil, "Org-xa08n2", "Errors.Org.NotificationPolicy.AlreadyExists")
}
return []eventstore.Command{
org.NewNotificationPolicyAddedEvent(ctx, &a.Aggregate, passwordChange),
}, nil
}, nil
}
}
func (c *Commands) ChangeNotificationPolicy(ctx context.Context, resourceOwner string, passwordChange bool) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x091n1g", "Errors.ResourceOwnerMissing")
}
orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareChangeNotificationPolicy(orgAgg, passwordChange))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func prepareChangeNotificationPolicy(
a *org.Aggregate,
passwordChange bool,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-x029n3", "Errors.Org.NotificationPolicy.NotFound")
}
change, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, passwordChange)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-ioqnxz", "Errors.Org.NotificationPolicy.NotChanged")
}
return []eventstore.Command{
change,
}, nil
}, nil
}
}
func (c *Commands) RemoveNotificationPolicy(ctx context.Context, resourceOwner string) (*domain.ObjectDetails, error) {
if resourceOwner == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-x89ns2", "Errors.ResourceOwnerMissing")
}
orgAgg := org.NewAggregate(resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveNotificationPolicy(orgAgg))
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return pushedEventsToObjectDetails(pushedEvents), nil
}
func prepareRemoveNotificationPolicy(
a *org.Aggregate,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewOrgNotificationPolicyWriteModel(a.Aggregate.ID)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.PolicyStateUnspecified || writeModel.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-x029n1s", "Errors.Org.NotificationPolicy.NotFound")
}
return []eventstore.Command{
org.NewNotificationPolicyRemovedEvent(ctx, &a.Aggregate),
}, nil
}, nil
}
}

View File

@ -0,0 +1,73 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
)
type OrgNotificationPolicyWriteModel struct {
NotificationPolicyWriteModel
}
func NewOrgNotificationPolicyWriteModel(orgID string) *OrgNotificationPolicyWriteModel {
return &OrgNotificationPolicyWriteModel{
NotificationPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: orgID,
ResourceOwner: orgID,
},
},
}
}
func (wm *OrgNotificationPolicyWriteModel) AppendEvents(events ...eventstore.Event) {
for _, event := range events {
switch e := event.(type) {
case *org.NotificationPolicyAddedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyAddedEvent)
case *org.NotificationPolicyChangedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyChangedEvent)
case *org.NotificationPolicyRemovedEvent:
wm.NotificationPolicyWriteModel.AppendEvents(&e.NotificationPolicyRemovedEvent)
}
}
}
func (wm *OrgNotificationPolicyWriteModel) Reduce() error {
return wm.NotificationPolicyWriteModel.Reduce()
}
func (wm *OrgNotificationPolicyWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateIDs(wm.NotificationPolicyWriteModel.AggregateID).
AggregateTypes(org.AggregateType).
EventTypes(org.NotificationPolicyAddedEventType,
org.NotificationPolicyChangedEventType,
org.NotificationPolicyRemovedEventType).
Builder()
}
func (wm *OrgNotificationPolicyWriteModel) NewChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
passwordChange bool,
) (*org.NotificationPolicyChangedEvent, bool) {
changes := make([]policy.NotificationPolicyChanges, 0)
if wm.PasswordChange != passwordChange {
changes = append(changes, policy.ChangePasswordChange(passwordChange))
}
if len(changes) == 0 {
return nil, false
}
changedEvent, err := org.NewNotificationPolicyChangedEvent(ctx, aggregate, changes)
if err != nil {
return nil, false
}
return changedEvent, true
}

View File

@ -0,0 +1,391 @@
package command
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
)
func TestCommandSide_AddNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
orgID string
passwordChange bool
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "org id missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "",
passwordChange: true,
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "policy already existing, already exists error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: true,
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
},
},
{
name: "add policy,ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: true,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
{
name: "add policy empty, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
false,
),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: false,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.AddNotificationPolicy(tt.args.ctx, tt.args.orgID, tt.args.passwordChange)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_ChangeNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
orgID string
passwordChange bool
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "org id missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
passwordChange: true,
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "policy not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: true,
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "no changes, precondition error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: true,
},
res: res{
err: caos_errs.IsPreconditionFailed,
},
},
{
name: "change, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
newNotificationPolicyChangedEvent(context.Background(), "org1", false),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
passwordChange: false,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.ChangeNotificationPolicy(tt.args.ctx, tt.args.orgID, tt.args.passwordChange)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func TestCommandSide_RemoveNotificationPolicy(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
}
type args struct {
ctx context.Context
orgID string
}
type res struct {
want *domain.ObjectDetails
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "org id missing, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "policy not existing, not found error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "remove, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
org.NewNotificationPolicyAddedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate,
true,
),
),
),
expectPush(
[]*repository.Event{
eventFromEventPusher(
org.NewNotificationPolicyRemovedEvent(context.Background(),
&org.NewAggregate("org1").Aggregate),
),
},
),
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
}
got, err := r.RemoveNotificationPolicy(tt.args.ctx, tt.args.orgID)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}
func newNotificationPolicyChangedEvent(ctx context.Context, orgID string, passwordChange bool) *org.NotificationPolicyChangedEvent {
event, _ := org.NewNotificationPolicyChangedEvent(ctx,
&org.NewAggregate(orgID).Aggregate,
[]policy.NotificationPolicyChanges{
policy.ChangePasswordChange(passwordChange),
},
)
return event
}

View File

@ -0,0 +1,31 @@
package command
import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/policy"
)
type NotificationPolicyWriteModel struct {
eventstore.WriteModel
PasswordChange bool
State domain.PolicyState
}
func (wm *NotificationPolicyWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *policy.NotificationPolicyAddedEvent:
wm.PasswordChange = e.PasswordChange
wm.State = domain.PolicyStateActive
case *policy.NotificationPolicyChangedEvent:
if e.PasswordChange != nil {
wm.PasswordChange = *e.PasswordChange
}
case *policy.NotificationPolicyRemovedEvent:
wm.State = domain.PolicyStateRemoved
}
}
return wm.WriteModel.Reduce()
}

View File

@ -196,6 +196,23 @@ func (c *Commands) PasswordCodeSent(ctx context.Context, orgID, userID string) (
return err
}
func (c *Commands) PasswordChangeSent(ctx context.Context, orgID, userID string) (err error) {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-pqlm2n", "Errors.User.UserIDMissing")
}
existingPassword, err := c.passwordWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-x902b2v", "Errors.User.NotFound")
}
userAgg := UserAggregateFromWriteModel(&existingPassword.WriteModel)
_, err = c.eventstore.Push(ctx, user.NewHumanPasswordChangeSentEvent(ctx, userAgg))
return err
}
func (c *Commands) HumanCheckPassword(ctx context.Context, orgID, userID, password string, authRequest *domain.AuthRequest, lockoutPolicy *domain.LockoutPolicy) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@ -13,6 +13,7 @@ const (
VerifyPhoneMessageType = "VerifyPhone"
DomainClaimedMessageType = "DomainClaimed"
PasswordlessRegistrationMessageType = "PasswordlessRegistration"
PasswordChangeMessageType = "PasswordChange"
MessageTitle = "Title"
MessagePreHeader = "PreHeader"
MessageSubject = "Subject"
@ -29,6 +30,7 @@ type MessageTexts struct {
VerifyPhone CustomMessageText
DomainClaimed CustomMessageText
PasswordlessRegistration CustomMessageText
PasswordChange CustomMessageText
}
type CustomMessageText struct {
@ -65,6 +67,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *CustomMessageText {
return &m.DomainClaimed
case PasswordlessRegistrationMessageType:
return &m.PasswordlessRegistration
case PasswordChangeMessageType:
return &m.PasswordChange
}
return nil
}
@ -75,5 +79,6 @@ func IsMessageTextType(textType string) bool {
textType == VerifyEmailMessageType ||
textType == VerifyPhoneMessageType ||
textType == DomainClaimedMessageType ||
textType == PasswordlessRegistrationMessageType
textType == PasswordlessRegistrationMessageType ||
textType == PasswordChangeMessageType
}

View File

@ -137,6 +137,10 @@ func (p *notificationsProjection) reducers() []handler.AggregateReducer {
Event: user.HumanPhoneCodeAddedType,
Reduce: p.reducePhoneCodeAdded,
},
{
Event: user.HumanPasswordChangedType,
Reduce: p.reducePasswordChanged,
},
},
},
}
@ -463,6 +467,74 @@ func (p *notificationsProjection) reducePasswordlessCodeRequested(event eventsto
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reducePasswordChanged(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanPasswordChangedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Yko2z8", "reduce.wrong.event.type %s", user.HumanPasswordChangedType)
}
ctx := setNotificationContext(event.Aggregate())
alreadyHandled, err := p.checkIfAlreadyHandled(ctx, event, nil, user.HumanPasswordChangeSentType)
if err != nil {
return nil, err
}
if alreadyHandled {
return crdb.NewNoOpStatement(e), nil
}
notificationPolicy, err := p.queries.NotificationPolicyByOrg(ctx, true, e.Aggregate().ResourceOwner, false)
if errors.IsNotFound(err) {
return crdb.NewNoOpStatement(e), nil
}
if err != nil {
return nil, err
}
if notificationPolicy.PasswordChange {
colors, err := p.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return nil, err
}
template, err := p.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return nil, err
}
notifyUser, err := p.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID, false)
if err != nil {
return nil, err
}
translator, err := p.getTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.PasswordChangeMessageType)
if err != nil {
return nil, err
}
ctx, origin, err := p.origin(ctx)
if err != nil {
return nil, err
}
err = types.SendEmail(
ctx,
string(template.Template),
translator,
notifyUser,
p.getSMTPConfig,
p.getFileSystemProvider,
p.getLogProvider,
colors,
p.assetsPrefix(ctx),
).SendPasswordChange(notifyUser, origin)
if err != nil {
return nil, err
}
err = p.commands.PasswordChangeSent(ctx, e.Aggregate().ResourceOwner, e.Aggregate().ID)
if err != nil {
return nil, err
}
}
return crdb.NewNoOpStatement(e), nil
}
func (p *notificationsProjection) reducePhoneCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanPhoneCodeAddedEvent)
if !ok {

View File

@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Wir haben eine Anfrage für das Hinzufügen eines Token für den passwortlosen Login erhalten. Du kannst den untenstehenden Button verwenden, um dein Token oder Gerät hinzuzufügen.
ButtonText: Passwortlosen Login hinzufügen
PasswordChange:
Title: ZITADEL - Passwort von Benutzer wurde geändert
PreHeader: Passwort Änderung
Subject: Passwort von Benutzer wurde geändert
Greeting: Hallo {{.FirstName}} {{.LastName}},
Text: Das Password vom Benutzer wurde geändert, wenn diese Änderung von jemand anderem gemacht wurde, empfehlen wir die sofortige Zurücksetzung ihres Passworts.
ButtonText: Login

View File

@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: We received a request to add a token for passwordless login. Please use the button below to add your token or device for passwordless login.
ButtonText: Add Passwordless Login
PasswordChange:
Title: ZITADEL - Password of user has changed
PreHeader: Change password
Subject: Password of user has changed
Greeting: Hello {{.FirstName}} {{.LastName}},
Text: The password of your user has changed, if this change was not done by you, please be advised to immediately reset your password.
ButtonText: Login

View File

@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: Bonjour {{.FirstName}} {{.LastName}},
Text: Nous avons reçu une demande d'ajout d'un jeton pour la connexion sans mot de passe. Veuillez utiliser le bouton ci-dessous pour ajouter votre jeton ou dispositif pour la connexion sans mot de passe.
ButtonText: Ajouter une connexion sans mot de passe
PasswordChange:
Title: ZITADEL - Le mot de passe de l'utilisateur a changé
PreHeader: Modifier le mot de passe
Subject: Le mot de passe de l'utilisateur a changé
Greeting: Bonjour {{.FirstName}} {{.LastName}},
Text: Le mot de passe de votre utilisateur a changé, si ce changement n'a pas été fait par vous, nous vous conseillons de réinitialiser immédiatement votre mot de passe.
ButtonText: Login

View File

@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: 'Ciao {{.FirstName}} {{.LastName}},'
Text: Abbiamo ricevuto una richiesta per aggiungere l'autenticazione passwordless. Usa il pulsante qui sotto per aggiungere il tuo token o dispositivo per il login senza password.
ButtonText: Attiva passwordless
PasswordChange:
Title: ZITADEL - La password dell'utente è stata modificata
PreHeader: Modifica della password
Subject: La password dell'utente è stata modificata
Greeting: Ciao {{.FirstName}} {{.LastName}},
Text: La password del vostro utente è cambiata; se questa modifica non è stata fatta da voi, vi consigliamo di reimpostare immediatamente la vostra password.
ButtonText: Login

View File

@ -40,3 +40,10 @@ PasswordlessRegistration:
Greeting: 你好 {{.FirstName}} {{.LastName}},
Text: 我们收到了为无密码登录添加令牌的请求。请使用下面的按钮添加您的令牌或设备以进行无密码登录。
ButtonText: 添加无密码登录
PasswordChange:
Title: ZITADEL - 用户的密码已经改变
PreHeader: 更改密码
Subject: 用户的密码已经改变
Greeting: 你好 {{.FirstName}} {{.LastName}},
Text: 您的用户的密码已经改变,如果这个改变不是由您做的,请注意立即重新设置您的密码。
ButtonText: 登录

View File

@ -0,0 +1,13 @@
package types
import (
"github.com/zitadel/zitadel/internal/api/ui/console"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
func (notify Notify) SendPasswordChange(user *query.NotifyUser, origin string) error {
url := console.LoginHintLink(origin, user.PreferredLoginName)
args := make(map[string]interface{})
return notify(url, args, domain.PasswordChangeMessageType, true)
}

View File

@ -29,6 +29,7 @@ type MessageTexts struct {
VerifyPhone MessageText
DomainClaimed MessageText
PasswordlessRegistration MessageText
PasswordChange MessageText
}
type MessageText struct {
@ -330,6 +331,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *MessageText {
return &m.DomainClaimed
case domain.PasswordlessRegistrationMessageType:
return &m.PasswordlessRegistration
case domain.PasswordChangeMessageType:
return &m.PasswordChange
}
return nil
}

View File

@ -0,0 +1,166 @@
package query
import (
"context"
"database/sql"
errs "errors"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
type NotificationPolicy struct {
ID string
Sequence uint64
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
State domain.PolicyState
PasswordChange bool
IsDefault bool
}
var (
notificationPolicyTable = table{
name: projection.NotificationPolicyProjectionTable,
instanceIDCol: projection.NotificationPolicyColumnInstanceID,
}
NotificationPolicyColID = Column{
name: projection.NotificationPolicyColumnID,
table: notificationPolicyTable,
}
NotificationPolicyColSequence = Column{
name: projection.NotificationPolicyColumnSequence,
table: notificationPolicyTable,
}
NotificationPolicyColCreationDate = Column{
name: projection.NotificationPolicyColumnCreationDate,
table: notificationPolicyTable,
}
NotificationPolicyColChangeDate = Column{
name: projection.NotificationPolicyColumnChangeDate,
table: notificationPolicyTable,
}
NotificationPolicyColResourceOwner = Column{
name: projection.NotificationPolicyColumnResourceOwner,
table: notificationPolicyTable,
}
NotificationPolicyColInstanceID = Column{
name: projection.NotificationPolicyColumnInstanceID,
table: notificationPolicyTable,
}
NotificationPolicyColPasswordChange = Column{
name: projection.NotificationPolicyColumnPasswordChange,
table: notificationPolicyTable,
}
NotificationPolicyColIsDefault = Column{
name: projection.NotificationPolicyColumnIsDefault,
table: notificationPolicyTable,
}
NotificationPolicyColState = Column{
name: projection.NotificationPolicyColumnStateCol,
table: notificationPolicyTable,
}
NotificationPolicyColOwnerRemoved = Column{
name: projection.NotificationPolicyColumnOwnerRemoved,
table: notificationPolicyTable,
}
)
func (q *Queries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (_ *NotificationPolicy, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
if err := projection.NotificationPolicyProjection.Trigger(ctx); err != nil {
return nil, err
}
}
eq := sq.Eq{NotificationPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}
if !withOwnerRemoved {
eq[NotificationPolicyColOwnerRemoved.identifier()] = false
}
stmt, scan := prepareNotificationPolicyQuery()
query, args, err := stmt.Where(
sq.And{
eq,
sq.Or{
sq.Eq{NotificationPolicyColID.identifier(): orgID},
sq.Eq{NotificationPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID()},
},
}).
OrderBy(NotificationPolicyColIsDefault.identifier()).Limit(1).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-Xuoapqm", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, query, args...)
return scan(row)
}
func (q *Queries) DefaultNotificationPolicy(ctx context.Context, shouldTriggerBulk bool) (_ *NotificationPolicy, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggerBulk {
if err := projection.NotificationPolicyProjection.Trigger(ctx); err != nil {
return nil, err
}
}
stmt, scan := prepareNotificationPolicyQuery()
query, args, err := stmt.Where(sq.Eq{
NotificationPolicyColID.identifier(): authz.GetInstance(ctx).InstanceID(),
NotificationPolicyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}).
OrderBy(NotificationPolicyColIsDefault.identifier()).
Limit(1).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-xlqp209", "Errors.Query.SQLStatement")
}
row := q.client.QueryRowContext(ctx, query, args...)
return scan(row)
}
func prepareNotificationPolicyQuery() (sq.SelectBuilder, func(*sql.Row) (*NotificationPolicy, error)) {
return sq.Select(
NotificationPolicyColID.identifier(),
NotificationPolicyColSequence.identifier(),
NotificationPolicyColCreationDate.identifier(),
NotificationPolicyColChangeDate.identifier(),
NotificationPolicyColResourceOwner.identifier(),
NotificationPolicyColPasswordChange.identifier(),
NotificationPolicyColIsDefault.identifier(),
NotificationPolicyColState.identifier(),
).
From(notificationPolicyTable.identifier()).PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*NotificationPolicy, error) {
policy := new(NotificationPolicy)
err := row.Scan(
&policy.ID,
&policy.Sequence,
&policy.CreationDate,
&policy.ChangeDate,
&policy.ResourceOwner,
&policy.PasswordChange,
&policy.IsDefault,
&policy.State,
)
if err != nil {
if errs.Is(err, sql.ErrNoRows) {
return nil, errors.ThrowNotFound(err, "QUERY-x0so2p", "Errors.NotificationPolicy.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-Zixoooq", "Errors.Internal")
}
return policy, nil
}
}

View File

@ -0,0 +1,116 @@
package query
import (
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"regexp"
"testing"
"github.com/zitadel/zitadel/internal/domain"
errs "github.com/zitadel/zitadel/internal/errors"
)
var notificationPolicyStmt = regexp.QuoteMeta(`SELECT projections.notification_policies.id,` +
` projections.notification_policies.sequence,` +
` projections.notification_policies.creation_date,` +
` projections.notification_policies.change_date,` +
` projections.notification_policies.resource_owner,` +
` projections.notification_policies.password_change,` +
` projections.notification_policies.is_default,` +
` projections.notification_policies.state` +
` FROM projections.notification_policies`)
func Test_NotificationPolicyPrepares(t *testing.T) {
type want struct {
sqlExpectations sqlExpectation
err checkErr
}
tests := []struct {
name string
prepare interface{}
want want
object interface{}
}{
{
name: "prepareNotificationPolicyQuery no result",
prepare: prepareNotificationPolicyQuery,
want: want{
sqlExpectations: mockQueries(
notificationPolicyStmt,
nil,
nil,
),
err: func(err error) (error, bool) {
if !errs.IsNotFound(err) {
return fmt.Errorf("err should be NotFoundError got: %w", err), false
}
return nil, true
},
},
object: (*NotificationPolicy)(nil),
},
{
name: "prepareNotificationPolicyQuery found",
prepare: prepareNotificationPolicyQuery,
want: want{
sqlExpectations: mockQuery(
notificationPolicyStmt,
[]string{
"id",
"sequence",
"creation_date",
"change_date",
"resource_owner",
"password_change",
"is_default",
"state",
},
[]driver.Value{
"pol-id",
uint64(20211109),
testNow,
testNow,
"ro",
true,
true,
domain.PolicyStateActive,
},
),
},
object: &NotificationPolicy{
ID: "pol-id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211109,
ResourceOwner: "ro",
State: domain.PolicyStateActive,
PasswordChange: true,
IsDefault: true,
},
},
{
name: "prepareNotificationPolicyQuery sql err",
prepare: prepareNotificationPolicyQuery,
want: want{
sqlExpectations: mockQueryErr(
notificationPolicyStmt,
sql.ErrConnDone,
),
err: func(err error) (error, bool) {
if !errors.Is(err, sql.ErrConnDone) {
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
}
return nil, true
},
},
object: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err)
})
}
}

View File

@ -273,7 +273,8 @@ func isMessageTemplate(template string) bool {
template == domain.VerifyEmailMessageType ||
template == domain.VerifyPhoneMessageType ||
template == domain.DomainClaimedMessageType ||
template == domain.PasswordlessRegistrationMessageType
template == domain.PasswordlessRegistrationMessageType ||
template == domain.PasswordChangeMessageType
}
func isTitle(key string) bool {
return key == domain.MessageTitle

View File

@ -0,0 +1,187 @@
package projection
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/policy"
)
const (
NotificationPolicyProjectionTable = "projections.notification_policies"
NotificationPolicyColumnID = "id"
NotificationPolicyColumnCreationDate = "creation_date"
NotificationPolicyColumnChangeDate = "change_date"
NotificationPolicyColumnResourceOwner = "resource_owner"
NotificationPolicyColumnInstanceID = "instance_id"
NotificationPolicyColumnSequence = "sequence"
NotificationPolicyColumnStateCol = "state"
NotificationPolicyColumnIsDefault = "is_default"
NotificationPolicyColumnPasswordChange = "password_change"
NotificationPolicyColumnOwnerRemoved = "owner_removed"
)
type notificationPolicyProjection struct {
crdb.StatementHandler
}
func newNotificationPolicyProjection(ctx context.Context, config crdb.StatementHandlerConfig) *notificationPolicyProjection {
p := new(notificationPolicyProjection)
config.ProjectionName = NotificationPolicyProjectionTable
config.Reducers = p.reducers()
config.InitCheck = crdb.NewTableCheck(
crdb.NewTable([]*crdb.Column{
crdb.NewColumn(NotificationPolicyColumnID, crdb.ColumnTypeText),
crdb.NewColumn(NotificationPolicyColumnCreationDate, crdb.ColumnTypeTimestamp),
crdb.NewColumn(NotificationPolicyColumnChangeDate, crdb.ColumnTypeTimestamp),
crdb.NewColumn(NotificationPolicyColumnResourceOwner, crdb.ColumnTypeText),
crdb.NewColumn(NotificationPolicyColumnInstanceID, crdb.ColumnTypeText),
crdb.NewColumn(NotificationPolicyColumnSequence, crdb.ColumnTypeInt64),
crdb.NewColumn(NotificationPolicyColumnStateCol, crdb.ColumnTypeEnum),
crdb.NewColumn(NotificationPolicyColumnIsDefault, crdb.ColumnTypeBool),
crdb.NewColumn(NotificationPolicyColumnPasswordChange, crdb.ColumnTypeBool),
crdb.NewColumn(NotificationPolicyColumnOwnerRemoved, crdb.ColumnTypeBool, crdb.Default(false)),
},
crdb.NewPrimaryKey(NotificationPolicyColumnInstanceID, NotificationPolicyColumnID),
),
)
p.StatementHandler = crdb.NewStatementHandler(ctx, config)
return p
}
func (p *notificationPolicyProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: org.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: org.NotificationPolicyAddedEventType,
Reduce: p.reduceAdded,
},
{
Event: org.NotificationPolicyChangedEventType,
Reduce: p.reduceChanged,
},
{
Event: org.NotificationPolicyRemovedEventType,
Reduce: p.reduceRemoved,
},
{
Event: org.OrgRemovedEventType,
Reduce: p.reduceOwnerRemoved,
},
},
},
{
Aggregate: instance.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(NotificationPolicyColumnInstanceID),
},
{
Event: instance.NotificationPolicyAddedEventType,
Reduce: p.reduceAdded,
},
{
Event: instance.NotificationPolicyChangedEventType,
Reduce: p.reduceChanged,
},
},
},
}
}
func (p *notificationPolicyProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) {
var policyEvent policy.NotificationPolicyAddedEvent
var isDefault bool
switch e := event.(type) {
case *org.NotificationPolicyAddedEvent:
policyEvent = e.NotificationPolicyAddedEvent
isDefault = false
case *instance.NotificationPolicyAddedEvent:
policyEvent = e.NotificationPolicyAddedEvent
isDefault = true
default:
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-x02s1m", "reduce.wrong.event.type %v", []eventstore.EventType{org.NotificationPolicyAddedEventType, instance.NotificationPolicyAddedEventType})
}
return crdb.NewCreateStatement(
&policyEvent,
[]handler.Column{
handler.NewCol(NotificationPolicyColumnCreationDate, policyEvent.CreationDate()),
handler.NewCol(NotificationPolicyColumnChangeDate, policyEvent.CreationDate()),
handler.NewCol(NotificationPolicyColumnSequence, policyEvent.Sequence()),
handler.NewCol(NotificationPolicyColumnID, policyEvent.Aggregate().ID),
handler.NewCol(NotificationPolicyColumnStateCol, domain.PolicyStateActive),
handler.NewCol(NotificationPolicyColumnPasswordChange, policyEvent.PasswordChange),
handler.NewCol(NotificationPolicyColumnIsDefault, isDefault),
handler.NewCol(NotificationPolicyColumnResourceOwner, policyEvent.Aggregate().ResourceOwner),
handler.NewCol(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID),
}), nil
}
func (p *notificationPolicyProjection) reduceChanged(event eventstore.Event) (*handler.Statement, error) {
var policyEvent policy.NotificationPolicyChangedEvent
switch e := event.(type) {
case *org.NotificationPolicyChangedEvent:
policyEvent = e.NotificationPolicyChangedEvent
case *instance.NotificationPolicyChangedEvent:
policyEvent = e.NotificationPolicyChangedEvent
default:
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-psom2h19", "reduce.wrong.event.type %v", []eventstore.EventType{org.NotificationPolicyChangedEventType, instance.NotificationPolicyChangedEventType})
}
cols := []handler.Column{
handler.NewCol(NotificationPolicyColumnChangeDate, policyEvent.CreationDate()),
handler.NewCol(NotificationPolicyColumnSequence, policyEvent.Sequence()),
}
if policyEvent.PasswordChange != nil {
cols = append(cols, handler.NewCol(NotificationPolicyColumnPasswordChange, *policyEvent.PasswordChange))
}
return crdb.NewUpdateStatement(
&policyEvent,
cols,
[]handler.Condition{
handler.NewCond(NotificationPolicyColumnID, policyEvent.Aggregate().ID),
handler.NewCond(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID),
}), nil
}
func (p *notificationPolicyProjection) reduceRemoved(event eventstore.Event) (*handler.Statement, error) {
policyEvent, ok := event.(*org.NotificationPolicyRemovedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-Po2iso2", "reduce.wrong.event.type %s", org.NotificationPolicyRemovedEventType)
}
return crdb.NewDeleteStatement(
policyEvent,
[]handler.Condition{
handler.NewCond(NotificationPolicyColumnID, policyEvent.Aggregate().ID),
handler.NewCond(NotificationPolicyColumnInstanceID, policyEvent.Aggregate().InstanceID),
}), nil
}
func (p *notificationPolicyProjection) reduceOwnerRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.OrgRemovedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-poxi9a", "reduce.wrong.event.type %s", org.OrgRemovedEventType)
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(DomainPolicyChangeDateCol, e.CreationDate()),
handler.NewCol(DomainPolicySequenceCol, e.Sequence()),
handler.NewCol(DomainPolicyOwnerRemovedCol, true),
},
[]handler.Condition{
handler.NewCond(DomainPolicyInstanceIDCol, e.Aggregate().InstanceID),
handler.NewCond(DomainPolicyResourceOwnerCol, e.Aggregate().ID),
},
), nil
}

View File

@ -0,0 +1,258 @@
package projection
import (
"testing"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
)
func TestNotificationPolicyProjection_reduces(t *testing.T) {
type args struct {
event func(t *testing.T) eventstore.Event
}
tests := []struct {
name string
args args
reduce func(event eventstore.Event) (*handler.Statement, error)
want wantReduce
}{
{
name: "org reduceAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(org.NotificationPolicyAddedEventType),
org.AggregateType,
[]byte(`{
"passwordChange": true
}`),
), org.NotificationPolicyAddedEventMapper),
},
reduce: (&notificationPolicyProjection{}).reduceAdded,
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.notification_policies (creation_date, change_date, sequence, id, state, password_change, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
uint64(15),
"agg-id",
domain.PolicyStateActive,
true,
false,
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "org reduceChanged",
reduce: (&notificationPolicyProjection{}).reduceChanged,
args: args{
event: getEvent(testEvent(
repository.EventType(org.NotificationPolicyChangedEventType),
org.AggregateType,
[]byte(`{
"passwordChange": true
}`),
), org.NotificationPolicyChangedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, password_change) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "org reduceRemoved",
reduce: (&notificationPolicyProjection{}).reduceRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.NotificationPolicyRemovedEventType),
org.AggregateType,
nil,
), org.NotificationPolicyRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.notification_policies WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
},
},
},
},
},
}, {
name: "instance reduceInstanceRemoved",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.InstanceRemovedEventType),
instance.AggregateType,
nil,
), instance.InstanceRemovedEventMapper),
},
reduce: reduceInstanceRemovedHelper(NotificationPolicyColumnInstanceID),
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.notification_policies WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
},
},
},
},
},
{
name: "instance reduceAdded",
reduce: (&notificationPolicyProjection{}).reduceAdded,
args: args{
event: getEvent(testEvent(
repository.EventType(instance.NotificationPolicyAddedEventType),
instance.AggregateType,
[]byte(`{
"passwordChange": true
}`),
), instance.NotificationPolicyAddedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.notification_policies (creation_date, change_date, sequence, id, state, password_change, is_default, resource_owner, instance_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
uint64(15),
"agg-id",
domain.PolicyStateActive,
true,
true,
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "instance reduceChanged",
reduce: (&notificationPolicyProjection{}).reduceChanged,
args: args{
event: getEvent(testEvent(
repository.EventType(instance.NotificationPolicyChangedEventType),
instance.AggregateType,
[]byte(`{
"passwordChange": true
}`),
), instance.NotificationPolicyChangedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, password_change) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "org.reduceOwnerRemoved",
reduce: (&notificationPolicyProjection{}).reduceOwnerRemoved,
args: args{
event: getEvent(testEvent(
repository.EventType(org.OrgRemovedEventType),
org.AggregateType,
nil,
), org.OrgRemovedEventMapper),
},
want: wantReduce{
aggregateType: eventstore.AggregateType("org"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.notification_policies SET (change_date, sequence, owner_removed) = ($1, $2, $3) WHERE (instance_id = $4) AND (resource_owner = $5)",
expectedArgs: []interface{}{
anyArg{},
uint64(15),
true,
"instance-id",
"agg-id",
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event := baseEvent(t)
got, err := tt.reduce(event)
if ok := errors.IsErrorInvalidArgument(err); !ok {
t.Errorf("no wrong event mapping: %v, got: %v", err, got)
}
event = tt.args.event(t)
got, err = tt.reduce(event)
assertReduce(t, got, err, NotificationPolicyProjectionTable, tt.want)
})
}
}

View File

@ -60,6 +60,7 @@ var (
DebugNotificationProviderProjection *debugNotificationProviderProjection
KeyProjection *keyProjection
SecurityPolicyProjection *securityPolicyProjection
NotificationPolicyProjection *notificationPolicyProjection
NotificationsProjection interface{}
)
@ -133,6 +134,7 @@ func Create(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, c
DebugNotificationProviderProjection = newDebugNotificationProviderProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_notification_provider"]))
KeyProjection = newKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), keyEncryptionAlgorithm, certEncryptionAlgorithm)
SecurityPolicyProjection = newSecurityPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["security_policies"]))
NotificationPolicyProjection = newNotificationPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["notification_policies"]))
newProjectionsList()
return nil
}
@ -224,5 +226,6 @@ func newProjectionsList() {
DebugNotificationProviderProjection,
KeyProjection,
SecurityPolicyProjection,
NotificationPolicyProjection,
}
}

View File

@ -89,5 +89,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, InstanceDomainRemovedEventType, DomainRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, InstanceAddedEventType, InstanceAddedEventMapper).
RegisterFilterEventMapper(AggregateType, InstanceChangedEventType, InstanceChangedEventMapper).
RegisterFilterEventMapper(AggregateType, InstanceRemovedEventType, InstanceRemovedEventMapper)
RegisterFilterEventMapper(AggregateType, InstanceRemovedEventType, InstanceRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper)
}

View File

@ -0,0 +1,73 @@
package instance
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/policy"
)
const (
NotificationPolicyAddedEventType = instanceEventTypePrefix + policy.NotificationPolicyAddedEventType
NotificationPolicyChangedEventType = instanceEventTypePrefix + policy.NotificationPolicyChangedEventType
)
type NotificationPolicyAddedEvent struct {
policy.NotificationPolicyAddedEvent
}
func NewNotificationPolicyAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
passwordChange bool,
) *NotificationPolicyAddedEvent {
return &NotificationPolicyAddedEvent{
NotificationPolicyAddedEvent: *policy.NewNotificationPolicyAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyAddedEventType),
passwordChange),
}
}
func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyAddedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyAddedEvent{NotificationPolicyAddedEvent: *e.(*policy.NotificationPolicyAddedEvent)}, nil
}
type NotificationPolicyChangedEvent struct {
policy.NotificationPolicyChangedEvent
}
func NewNotificationPolicyChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []policy.NotificationPolicyChanges,
) (*NotificationPolicyChangedEvent, error) {
changedEvent, err := policy.NewNotificationPolicyChangedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyChangedEventType),
changes,
)
if err != nil {
return nil, err
}
return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *changedEvent}, nil
}
func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyChangedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *e.(*policy.NotificationPolicyChangedEvent)}, nil
}

View File

@ -83,5 +83,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, FlowClearedEventType, FlowClearedEventMapper).
RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper).
RegisterFilterEventMapper(AggregateType, MetadataRemovedType, MetadataRemovedEventMapper).
RegisterFilterEventMapper(AggregateType, MetadataRemovedAllType, MetadataRemovedAllEventMapper)
RegisterFilterEventMapper(AggregateType, MetadataRemovedAllType, MetadataRemovedAllEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper).
RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper)
}

View File

@ -0,0 +1,102 @@
package org
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
"github.com/zitadel/zitadel/internal/repository/policy"
)
var (
NotificationPolicyAddedEventType = orgEventTypePrefix + policy.NotificationPolicyAddedEventType
NotificationPolicyChangedEventType = orgEventTypePrefix + policy.NotificationPolicyChangedEventType
NotificationPolicyRemovedEventType = orgEventTypePrefix + policy.NotificationPolicyRemovedEventType
)
type NotificationPolicyAddedEvent struct {
policy.NotificationPolicyAddedEvent
}
func NewNotificationPolicyAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
passwordChange bool,
) *NotificationPolicyAddedEvent {
return &NotificationPolicyAddedEvent{
NotificationPolicyAddedEvent: *policy.NewNotificationPolicyAddedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyAddedEventType),
passwordChange,
),
}
}
func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyAddedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyAddedEvent{NotificationPolicyAddedEvent: *e.(*policy.NotificationPolicyAddedEvent)}, nil
}
type NotificationPolicyChangedEvent struct {
policy.NotificationPolicyChangedEvent
}
func NewNotificationPolicyChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
changes []policy.NotificationPolicyChanges,
) (*NotificationPolicyChangedEvent, error) {
changedEvent, err := policy.NewNotificationPolicyChangedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyChangedEventType),
changes,
)
if err != nil {
return nil, err
}
return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *changedEvent}, nil
}
func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyChangedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyChangedEvent{NotificationPolicyChangedEvent: *e.(*policy.NotificationPolicyChangedEvent)}, nil
}
type NotificationPolicyRemovedEvent struct {
policy.NotificationPolicyRemovedEvent
}
func NewNotificationPolicyRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *NotificationPolicyRemovedEvent {
return &NotificationPolicyRemovedEvent{
NotificationPolicyRemovedEvent: *policy.NewNotificationPolicyRemovedEvent(
eventstore.NewBaseEventForPush(
ctx,
aggregate,
NotificationPolicyRemovedEventType),
),
}
}
func NotificationPolicyRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
e, err := policy.NotificationPolicyRemovedEventMapper(event)
if err != nil {
return nil, err
}
return &NotificationPolicyRemovedEvent{NotificationPolicyRemovedEvent: *e.(*policy.NotificationPolicyRemovedEvent)}, nil
}

View File

@ -0,0 +1,127 @@
package policy
import (
"encoding/json"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/repository"
)
const (
NotificationPolicyAddedEventType = "policy.notification.added"
NotificationPolicyChangedEventType = "policy.notification.changed"
NotificationPolicyRemovedEventType = "policy.notification.removed"
)
type NotificationPolicyAddedEvent struct {
eventstore.BaseEvent `json:"-"`
PasswordChange bool `json:"passwordChange,omitempty"`
}
func (e *NotificationPolicyAddedEvent) Data() interface{} {
return e
}
func (e *NotificationPolicyAddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewNotificationPolicyAddedEvent(
base *eventstore.BaseEvent,
passwordChange bool,
) *NotificationPolicyAddedEvent {
return &NotificationPolicyAddedEvent{
BaseEvent: *base,
PasswordChange: passwordChange,
}
}
func NotificationPolicyAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &NotificationPolicyAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "POLIC-0sp2nios", "unable to unmarshal policy")
}
return e, nil
}
type NotificationPolicyChangedEvent struct {
eventstore.BaseEvent `json:"-"`
PasswordChange *bool `json:"passwordChange,omitempty"`
}
func (e *NotificationPolicyChangedEvent) Data() interface{} {
return e
}
func (e *NotificationPolicyChangedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewNotificationPolicyChangedEvent(
base *eventstore.BaseEvent,
changes []NotificationPolicyChanges,
) (*NotificationPolicyChangedEvent, error) {
if len(changes) == 0 {
return nil, errors.ThrowPreconditionFailed(nil, "POLICY-09sp2m", "Errors.NoChangesFound")
}
changeEvent := &NotificationPolicyChangedEvent{
BaseEvent: *base,
}
for _, change := range changes {
change(changeEvent)
}
return changeEvent, nil
}
type NotificationPolicyChanges func(*NotificationPolicyChangedEvent)
func ChangePasswordChange(passwordChange bool) func(*NotificationPolicyChangedEvent) {
return func(e *NotificationPolicyChangedEvent) {
e.PasswordChange = &passwordChange
}
}
func NotificationPolicyChangedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &NotificationPolicyChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, e)
if err != nil {
return nil, errors.ThrowInternal(err, "POLIC-09s2oss", "unable to unmarshal policy")
}
return e, nil
}
type NotificationPolicyRemovedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *NotificationPolicyRemovedEvent) Data() interface{} {
return nil
}
func (e *NotificationPolicyRemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewNotificationPolicyRemovedEvent(base *eventstore.BaseEvent) *NotificationPolicyRemovedEvent {
return &NotificationPolicyRemovedEvent{
BaseEvent: *base,
}
}
func NotificationPolicyRemovedEventMapper(event *repository.Event) (eventstore.Event, error) {
return &NotificationPolicyRemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}

View File

@ -59,6 +59,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, HumanPasswordChangedType, HumanPasswordChangedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordCodeAddedType, HumanPasswordCodeAddedEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordCodeSentType, HumanPasswordCodeSentEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordChangeSentType, HumanPasswordChangeSentEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordCheckSucceededType, HumanPasswordCheckSucceededEventMapper).
RegisterFilterEventMapper(AggregateType, HumanPasswordCheckFailedType, HumanPasswordCheckFailedEventMapper).
RegisterFilterEventMapper(AggregateType, UserIDPLinkAddedType, UserIDPLinkAddedEventMapper).

View File

@ -16,6 +16,7 @@ import (
const (
passwordEventPrefix = humanEventPrefix + "password."
HumanPasswordChangedType = passwordEventPrefix + "changed"
HumanPasswordChangeSentType = passwordEventPrefix + "change.sent"
HumanPasswordCodeAddedType = passwordEventPrefix + "code.added"
HumanPasswordCodeSentType = passwordEventPrefix + "code.sent"
HumanPasswordCheckSucceededType = passwordEventPrefix + "check.succeeded"
@ -144,6 +145,34 @@ func HumanPasswordCodeSentEventMapper(event *repository.Event) (eventstore.Event
}, nil
}
type HumanPasswordChangeSentEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *HumanPasswordChangeSentEvent) Data() interface{} {
return nil
}
func (e *HumanPasswordChangeSentEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func NewHumanPasswordChangeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanPasswordChangeSentEvent {
return &HumanPasswordChangeSentEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanPasswordChangeSentType,
),
}
}
func HumanPasswordChangeSentEventMapper(event *repository.Event) (eventstore.Event, error) {
return &HumanPasswordChangeSentEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type HumanPasswordCheckSucceededEvent struct {
eventstore.BaseEvent `json:"-"`
*AuthRequestInfo

View File

@ -225,6 +225,10 @@ Errors:
Empty: Org IAM Policy ist leer
NotExisting: Org IAM Policy existiert nicht
AlreadyExists: Org IAM Policy existiert bereits
NotificationPolicy:
NotFound: Notification Policy konnte nicht gefunden werden
NotChanged: Notification Policy wurde nicht verändert
AlreadyExists: Notification Policy existiert bereits
Project:
ProjectIDMissing: Project ID fehlt
AlreadyExists: Project existiert bereits auf der Organisation
@ -351,6 +355,10 @@ Errors:
AlreadyExists: Default Org IAM Policy existiert bereits
Empty: Default Org IAM Policy leer
NotChanged: Default Org IAM Policy wurde nicht verändert
NotificationPolicy:
NotFound: Default Notification Policy konnte nicht gefunden werden
NotChanged: Default Notification Policy wurde nicht verändert
AlreadyExists: Default Notification Policy existiert bereits
Policy:
AlreadyExists: Policy existiert bereits
Label:
@ -750,6 +758,10 @@ EventTypes:
added: Passwortaussperrrichtlinie hinzugefügt
changed: Passwortaussperrrichtlinie geändert
removed: Passwortaussperrrichtlinie gelöscht
notification:
added: Notifikation Richtlinie hinzugefügt
changed: Notifikation Richtlinie geändert
removed: Notifikation Richtlinie entfernt
flow:
trigger_actions:
set: Aktionen festgelegt
@ -980,7 +992,7 @@ EventTypes:
removed: Instanzmitglied gelöscht
cascade:
removed: Instanzmitglied kaskadierend gelöscht
notification:
notification:
provider:
debug:
fileadded: Datei zu Debug Notification Provider hinzugefügt
@ -1047,7 +1059,7 @@ EventTypes:
changed: Datenschutzrichtlinie geändert
security:
set: Sicherheitsrichtlinie gesetzt
removed: Instanz gelöscht
secret:
generator:

View File

@ -225,6 +225,10 @@ Errors:
Empty: Org IAM Policy is empty
NotExisting: Org IAM Policy doesn't exist
AlreadyExists: Org IAM Policy already exists
NotificationPolicy:
NotFound: Notification Policy not found
NotChanged: Notification Policy not changed
AlreadyExists: Notification Policy already exists
Project:
ProjectIDMissing: Project Id missing
AlreadyExists: Project already exists on organization
@ -351,6 +355,10 @@ Errors:
NotExisting: Org IAM Policy not existing
AlreadyExists: Org IAM Policy already exists
NotChanged: Org IAM Policy has not been changed
NotificationPolicy:
NotFound: Default Notification Policy not found
NotChanged: Default Notification Policy not changed
AlreadyExists: Default Notification Policy already exists
Policy:
AlreadyExists: Policy already exists
Label:
@ -750,6 +758,10 @@ EventTypes:
added: Lockout policy added
changed: Lockout policy changed
removed: Lockout policy removed
notification:
added: Notification policy added
changed: Notification policy changed
removed: Notification policy removed
flow:
trigger_actions:
set: Action set
@ -1047,7 +1059,7 @@ EventTypes:
changed: Privacy policy changed
security:
set: Security policy set
removed: Instance removed
secret:
generator:

View File

@ -225,6 +225,10 @@ Errors:
Empty: La politique IAM d'Org est vide
NotExisting: La politique Org IAM n'existe pas
AlreadyExists: La politique IAM d'Org existe déjà
NotificationPolicy:
NotFound: La politique notification n'a pas été trouvée
NotChanged: La politique notification n'a pas été modifiée
AlreadyExists: La politique notification existe déjà
Project:
ProjectIDMissing: Id de projet manquant
AlreadyExists: Le projet existe déjà dans l'organisation
@ -351,6 +355,10 @@ Errors:
NotExisting: La politique IAM d'Org n'existe pas
AlreadyExists: La politique IAM d'Org existe déjà
NotChanged: La politique IAM d'Org n'a pas été modifiée
NotificationPolicy:
NotFound: La politique de notification par défaut n'a pas été trouvée
NotChanged: La politique de notification par défaut n'a pas été modifiée
AlreadyExists: La ppolitique de notification par défaut existe déjà
Policy:
AlreadyExists: La politique existe déjà
Label:
@ -725,6 +733,10 @@ EventTypes:
added: Politique de confidentialité et CGU ajoutés
changed: Politique de confidentialité et CGU modifiées
removed: Politique de confidentialité et conditions d'utilisation supprimées
notification:
added: Politique de notification ajoutée
changed: Politique de notification modifiée
removed: Politique de notification supprimée
flow:
trigger_actions:
set: Action set

View File

@ -225,6 +225,10 @@ Errors:
Empty: Mancano le impostazioni Org IAM
NotExisting: Impostazioni Org IAM non esistenti
AlreadyExists: Impostazioni Org IAM già esistenti
NotificationPolicy:
NotFound: Impostazioni di notifica non trovate
NotChanged: Impostazioni di notifica non è stato cambiato
AlreadyExists: Impostazioni di notifica già esistente
Project:
ProjectIDMissing: ID del progetto mancante
AlreadyExists: Il progetto è già stato creato nell'organizzazione
@ -351,6 +355,10 @@ Errors:
NotExisting: Impostazioni Org IAM non esistenti
AlreadyExists: Impostazioni Org IAM già esistenti
NotChanged: Impostazioni Org IAM non sono state cambiate
NotificationPolicy:
NotFound: Impostazioni di notifica predefinite non trovate
NotChanged: Impostazioni di notifica predefinite non è stato cambiato
AlreadyExists: Impostazioni di notifica predefinite già esistente
Policy:
AlreadyExists: Impostazioni già esistenti
Label:
@ -725,6 +733,10 @@ EventTypes:
added: Informativa sulla privacy e termini e condizioni aggiunti
changed: Informativa sulla privacy e termini e condizioni cambiati
removed: Informativa sulla privacy e termini e condizioni rimossi
notification:
added: Impostazione di notifica creata
changed: Impostazione di notifica cambiata
removed: Impostazione di notifica rimossa
flow:
trigger_actions:
set: azioni salvate

View File

@ -225,6 +225,10 @@ Errors:
Empty: 组织 IAM 策略为空
NotExisting: 组织 IAM 策略不存在
AlreadyExists: 组织 IAM 策略已存在
NotificationPolicy:
NotFound: 未找到通知政策
NotChanged: 通知政策没有改变
AlreadyExists: 已经存在的通知政策
Project:
ProjectIDMissing: P缺少项目 ID
AlreadyExists: 项目以存在于组织中
@ -351,6 +355,10 @@ Errors:
NotExisting: 组织 IAM 策略不存在
AlreadyExists: 组织 IAM 策略已存在
NotChanged: 组织 IAM 策略未更改
NotificationPolicy:
NotFound: 没有找到默认的通知政策
NotChanged: 默认的通知政策没有改变
AlreadyExists: 默认的通知政策已经存在
Policy:
AlreadyExists: 策略已存在
Label:
@ -715,6 +723,10 @@ EventTypes:
added: 添加隐私政策和服务条款
changed: 更改隐私政策和服务条款
removed: 删除隐私政策和服务条款
notification:
added: 增加了通知政策
changed: 通知政策改变
removed: 删除了通知政策
flow:
trigger_actions:
set: 设置动作

View File

@ -1945,6 +1945,91 @@ service AdminService {
};
}
//Add a default notification policy for ZITADEL
// it impacts all organisations without a customised policy
rpc AddNotificationPolicy(AddNotificationPolicyRequest) returns (AddNotificationPolicyResponse) {
option (google.api.http) = {
post: "/policies/notification"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "policy";
tags: "notification policy";
tags: "notification";
responses: {
key: "200";
value: {
description: "default notification policy";
};
};
};
}
//Returns the notification policy defined by the administrators of ZITADEL
rpc GetNotificationPolicy(GetNotificationPolicyRequest) returns (GetNotificationPolicyResponse) {
option (google.api.http) = {
get: "/policies/notification";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "policy";
tags: "notification policy";
tags: "notification";
responses: {
key: "200";
value: {
description: "default notification policy";
};
};
};
}
//Updates the default notification policy of ZITADEL
// it impacts all organisations without a customised policy
rpc UpdateNotificationPolicy(UpdateNotificationPolicyRequest) returns (UpdateNotificationPolicyResponse) {
option (google.api.http) = {
put: "/policies/notification";
body: "*";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "policy";
tags: "notification policy";
tags: "notification";
responses: {
key: "200";
value: {
description: "default notification policy updated";
};
};
responses: {
key: "400";
value: {
description: "invalid argument";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
};
};
};
};
};
}
//Returns the default text for initial message (translation file)
rpc GetDefaultInitMessageText(GetDefaultInitMessageTextRequest) returns (GetDefaultInitMessageTextResponse) {
option (google.api.http) = {
@ -1967,10 +2052,11 @@ service AdminService {
};
}
//Sets the default custom text for initial message
// it impacts all organisations without customized initial message text
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetDefaultInitMessageText(SetDefaultInitMessageTextRequest) returns (SetDefaultInitMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/init/{language}";
@ -2019,7 +2105,7 @@ service AdminService {
//Sets the default custom text for password reset message
// it impacts all organisations without customized password reset message text
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetDefaultPasswordResetMessageText(SetDefaultPasswordResetMessageTextRequest) returns (SetDefaultPasswordResetMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/passwordreset/{language}";
@ -2069,7 +2155,7 @@ service AdminService {
//Sets the default custom text for verify email message
// it impacts all organisations without customized verify email message text
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetDefaultVerifyEmailMessageText(SetDefaultVerifyEmailMessageTextRequest) returns (SetDefaultVerifyEmailMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/verifyemail/{language}";
@ -2118,7 +2204,7 @@ service AdminService {
//Sets the default custom text for verify phone message
// it impacts all organisations without customized verify phone message text
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetDefaultVerifyPhoneMessageText(SetDefaultVerifyPhoneMessageTextRequest) returns (SetDefaultVerifyPhoneMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/verifyphone/{language}";
@ -2164,10 +2250,10 @@ service AdminService {
};
}
//Sets the default custom text for domain claimed phone message
//Sets the default custom text for domain claimed message
// it impacts all organisations without customized domain claimed message text
// The Following Variables can be used:
// {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetDefaultDomainClaimedMessageText(SetDefaultDomainClaimedMessageTextRequest) returns (SetDefaultDomainClaimedMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/domainclaimed/{language}";
@ -2216,7 +2302,7 @@ service AdminService {
//Sets the default custom text for passwordless registration message
// it impacts all organisations without customized passwordless registration message text
// The Following Variables can be used:
// {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetDefaultPasswordlessRegistrationMessageText(SetDefaultPasswordlessRegistrationMessageTextRequest) returns (SetDefaultPasswordlessRegistrationMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/passwordless_registration/{language}";
@ -2240,6 +2326,57 @@ service AdminService {
};
}
//Returns the default text for password change message (translation file)
rpc GetDefaultPasswordChangeMessageText(GetDefaultPasswordChangeMessageTextRequest) returns (GetDefaultPasswordChangeMessageTextResponse) {
option (google.api.http) = {
get: "/text/default/message/password_change/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
}
//Returns the custom text for password change message (overwritten in eventstore)
rpc GetCustomPasswordChangeMessageText(GetCustomPasswordChangeMessageTextRequest) returns (GetCustomPasswordChangeMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/password_change/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.read";
};
}
//Sets the default custom text for password change message
// it impacts all organisations without customized password change message text
// The Following Variables can be used:
// {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetDefaultPasswordChangeMessageText(SetDefaultPasswordChangeMessageTextRequest) returns (SetDefaultPasswordChangeMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/password_change/{language}";
body: "*";
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.write";
};
}
// Removes the custom password change message text of the system
// The default text from the translation file will trigger after
rpc ResetCustomPasswordChangeMessageTextToDefault(ResetCustomPasswordChangeMessageTextToDefaultRequest) returns (ResetCustomPasswordChangeMessageTextToDefaultResponse) {
option (google.api.http) = {
delete: "/text/message/password_change/{language}"
};
option (zitadel.v1.auth_option) = {
permission: "iam.policy.delete"
};
}
//Returns the default custom texts for login ui (translation file)
rpc GetDefaultLoginTexts(GetDefaultLoginTextsRequest) returns (GetDefaultLoginTextsResponse) {
option (google.api.http) = {
@ -4111,6 +4248,29 @@ message UpdatePrivacyPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
message AddNotificationPolicyRequest {
bool password_change = 1;
}
message AddNotificationPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message GetNotificationPolicyRequest {}
message GetNotificationPolicyResponse {
zitadel.policy.v1.NotificationPolicy policy = 1;
}
message UpdateNotificationPolicyRequest {
bool password_change = 1;
}
message UpdateNotificationPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultInitMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
@ -4331,6 +4491,51 @@ message ResetCustomDomainClaimedMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultPasswordChangeMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetDefaultPasswordChangeMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message GetCustomPasswordChangeMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetCustomPasswordChangeMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message SetDefaultPasswordChangeMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"de\""
}
];
string title = 2 [(validate.rules).string = {max_len: 200}];
string pre_header = 3 [(validate.rules).string = {max_len: 200}];
string subject = 4 [(validate.rules).string = {max_len: 200}];
string greeting = 5 [(validate.rules).string = {max_len: 200}];
string text = 6 [(validate.rules).string = {max_len: 800}];
string button_text = 7 [(validate.rules).string = {max_len: 200}];
string footer_text = 8 [(validate.rules).string = {max_len: 200}];
}
message SetDefaultPasswordChangeMessageTextResponse {
zitadel.v1.ObjectDetails details = 1;
}
message ResetCustomPasswordChangeMessageTextToDefaultRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message ResetCustomPasswordChangeMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetDefaultPasswordlessRegistrationMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}

View File

@ -2248,7 +2248,7 @@ service ManagementService {
};
}
// Update the privacy complexity policy for the organisation
// Update the privacy policy for the organisation
// With this policy privacy relevant things can be configured (e.g. tos link)
// Variable {{.Lang}} can be set to have different links based on the language
rpc UpdateCustomPrivacyPolicy(UpdateCustomPrivacyPolicyRequest) returns (UpdateCustomPrivacyPolicyResponse) {
@ -2274,6 +2274,68 @@ service ManagementService {
};
}
// Returns the notification policy of the organisation
// With this notification policy it can be configured how users should be notified
rpc GetNotificationPolicy(GetNotificationPolicyRequest) returns (GetNotificationPolicyResponse) {
option (google.api.http) = {
get: "/policies/notification"
};
option (zitadel.v1.auth_option) = {
permission: "policy.read"
};
}
// Returns the default notification policy of the IAM
// With this notification privacy it can be configured how users should be notified
rpc GetDefaultNotificationPolicy(GetDefaultNotificationPolicyRequest) returns (GetDefaultNotificationPolicyResponse) {
option (google.api.http) = {
get: "/policies/default/notification"
};
option (zitadel.v1.auth_option) = {
permission: "policy.read"
};
}
// Add a custom notification policy for the organisation
// With this notification privacy it can be configured how users should be notified
rpc AddCustomNotificationPolicy(AddCustomNotificationPolicyRequest) returns (AddCustomNotificationPolicyResponse) {
option (google.api.http) = {
post: "/policies/notification"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "policy.write"
};
}
// Update the notification policy for the organisation
// With this notification privacy it can be configured how users should be notified
rpc UpdateCustomNotificationPolicy(UpdateCustomNotificationPolicyRequest) returns (UpdateCustomNotificationPolicyResponse) {
option (google.api.http) = {
put: "/policies/notification"
body: "*"
};
option (zitadel.v1.auth_option) = {
permission: "policy.write"
};
}
// Removes the notification policy of the organisation
// The default policy of the IAM will trigger after
rpc ResetNotificationPolicyToDefault(ResetNotificationPolicyToDefaultRequest) returns (ResetNotificationPolicyToDefaultResponse) {
option (google.api.http) = {
delete: "/policies/notification"
};
option (zitadel.v1.auth_option) = {
permission: "policy.delete"
};
}
// Returns the active label policy of the organisation
// With this policy the private labeling can be configured (colors, etc.)
rpc GetLabelPolicy(GetLabelPolicyRequest) returns (GetLabelPolicyResponse) {
@ -2487,7 +2549,7 @@ service ManagementService {
// Sets the custom text for password reset message
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetCustomPasswordResetMessageText(SetCustomPasswordResetMessageTextRequest) returns (SetCustomPasswordResetMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/passwordreset/{language}";
@ -2535,7 +2597,7 @@ service ManagementService {
// Sets the custom text for verify email message
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetCustomVerifyEmailMessageText(SetCustomVerifyEmailMessageTextRequest) returns (SetCustomVerifyEmailMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/verifyemail/{language}";
@ -2583,7 +2645,7 @@ service ManagementService {
// Sets the default custom text for verify email message
// The Following Variables can be used:
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Code}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetCustomVerifyPhoneMessageText(SetCustomVerifyPhoneMessageTextRequest) returns (SetCustomVerifyPhoneMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/verifyphone/{language}";
@ -2631,7 +2693,7 @@ service ManagementService {
// Sets the custom text for domain claimed message
// The Following Variables can be used:
// {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.Domain}} {{.TempUsername}} {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetCustomDomainClaimedMessageCustomText(SetCustomDomainClaimedMessageTextRequest) returns (SetCustomDomainClaimedMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/domainclaimed/{language}";
@ -2679,7 +2741,7 @@ service ManagementService {
// Sets the custom text for passwordless link message
// The Following Variables can be used:
// {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}}
// {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetCustomPasswordlessRegistrationMessageCustomText(SetCustomPasswordlessRegistrationMessageTextRequest) returns (SetCustomPasswordlessRegistrationMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/passwordless_registration/{language}";
@ -2703,6 +2765,54 @@ service ManagementService {
};
}
//Returns the custom text for password change message
rpc GetCustomPasswordChangeMessageText(GetCustomPasswordChangeMessageTextRequest) returns (GetCustomPasswordChangeMessageTextResponse) {
option (google.api.http) = {
get: "/text/message/password_change/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "policy.read";
};
}
//Returns the custom text for password change link message
rpc GetDefaultPasswordChangeMessageText(GetDefaultPasswordChangeMessageTextRequest) returns (GetDefaultPasswordChangeMessageTextResponse) {
option (google.api.http) = {
get: "/text/default/message/password_change/{language}";
};
option (zitadel.v1.auth_option) = {
permission: "policy.read";
};
}
// Sets the custom text for password change message
// The Following Variables can be used:
// {{.UserName}} {{.FirstName}} {{.LastName}} {{.NickName}} {{.DisplayName}} {{.LastEmail}} {{.VerifiedEmail}} {{.LastPhone}} {{.VerifiedPhone}} {{.PreferredLoginName}} {{.LoginNames}} {{.ChangeDate}} {{.CreationDate}}
rpc SetCustomPasswordChangeMessageCustomText(SetCustomPasswordChangeMessageTextRequest) returns (SetCustomPasswordChangeMessageTextResponse) {
option (google.api.http) = {
put: "/text/message/password_change/{language}";
body: "*";
};
option (zitadel.v1.auth_option) = {
permission: "policy.write";
};
}
// Removes the custom password change message text of the organisation
// The default text of the IAM will trigger after
rpc ResetCustomPasswordChangeMessageTextToDefault(ResetCustomPasswordChangeMessageTextToDefaultRequest) returns (ResetCustomPasswordChangeMessageTextToDefaultResponse) {
option (google.api.http) = {
delete: "/text/message/password_change/{language}"
};
option (zitadel.v1.auth_option) = {
permission: "policy.delete"
};
}
//Returns the custom texts for login ui
rpc GetCustomLoginTexts(GetCustomLoginTextsRequest) returns (GetCustomLoginTextsResponse) {
option (google.api.http) = {
@ -4941,6 +5051,43 @@ message ResetPrivacyPolicyToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message GetNotificationPolicyRequest {}
message GetNotificationPolicyResponse {
zitadel.policy.v1.NotificationPolicy policy = 1;
}
//This is an empty request
message GetDefaultNotificationPolicyRequest {}
message GetDefaultNotificationPolicyResponse {
zitadel.policy.v1.NotificationPolicy policy = 1;
}
message AddCustomNotificationPolicyRequest {
bool password_change = 1;
}
message AddCustomNotificationPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
message UpdateCustomNotificationPolicyRequest {
bool password_change = 1;
}
message UpdateCustomNotificationPolicyResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message ResetNotificationPolicyToDefaultRequest {}
message ResetNotificationPolicyToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
//This is an empty request
message GetLabelPolicyRequest {}
@ -5393,6 +5540,51 @@ message ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetCustomPasswordChangeMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetCustomPasswordChangeMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message GetDefaultPasswordChangeMessageTextRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message GetDefaultPasswordChangeMessageTextResponse {
zitadel.text.v1.MessageCustomText custom_text = 1;
}
message SetCustomPasswordChangeMessageTextRequest {
string language = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"de\""
}
];
string title = 2 [(validate.rules).string = {max_len: 200}];
string pre_header = 3 [(validate.rules).string = {max_len: 200}];
string subject = 4 [(validate.rules).string = {max_len: 200}];
string greeting = 5 [(validate.rules).string = {max_len: 200}];
string text = 6 [(validate.rules).string = {max_len: 800}];
string button_text = 7 [(validate.rules).string = {max_len: 200}];
string footer_text = 8 [(validate.rules).string = {max_len: 200}];
}
message SetCustomPasswordChangeMessageTextResponse {
zitadel.v1.ObjectDetails details = 1;
}
message ResetCustomPasswordChangeMessageTextToDefaultRequest {
string language = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}
message ResetCustomPasswordChangeMessageTextToDefaultResponse {
zitadel.v1.ObjectDetails details = 1;
}
message GetOrgIDPByIDRequest {
string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}

View File

@ -285,3 +285,9 @@ message PrivacyPolicy {
bool is_default = 4;
string help_link = 5;
}
message NotificationPolicy {
zitadel.v1.ObjectDetails details = 1;
bool is_default = 2;
bool password_change = 3;
}