feat: invite user link (#8578)

# Which Problems Are Solved

As an administrator I want to be able to invite users to my application
with the API V2, some user data I will already prefil, the user should
add the authentication method themself (password, passkey, sso).

# How the Problems Are Solved

- A user can now be created with a email explicitly set to false.
- If a user has no verified email and no authentication method, an
`InviteCode` can be created through the User V2 API.
  - the code can be returned or sent through email
- additionally `URLTemplate` and an `ApplicatioName` can provided for
the email
- The code can be resent and verified through the User V2 API
- The V1 login allows users to verify and resend the code and set a
password (analog user initialization)
- The message text for the user invitation can be customized

# Additional Changes

- `verifyUserPasskeyCode` directly uses `crypto.VerifyCode` (instead of
`verifyEncryptedCode`)
- `verifyEncryptedCode` is removed (unnecessarily queried for the code
generator)

# Additional Context

- closes #8310
- TODO: login V2 will have to implement invite flow:
https://github.com/zitadel/typescript/issues/166
This commit is contained in:
Livio Spring 2024-09-11 12:53:55 +02:00 committed by GitHub
parent 02c78a19c6
commit a07b2f4677
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
114 changed files with 3898 additions and 293 deletions

View File

@ -712,6 +712,13 @@ DefaultInstance:
IncludeUpperLetters: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_OTPEMAIL_INCLUDEUPPERLETTERS
IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_OTPEMAIL_INCLUDEDIGITS
IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_OTPEMAIL_INCLUDESYMBOLS
InviteCode:
Length: 6 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_LENGTH
Expiry: "72h" # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_EXPIRY
IncludeLowerLetters: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDELOWERLETTERS
IncludeUpperLetters: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEUPPERLETTERS
IncludeDigits: true # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDEDIGITS
IncludeSymbols: false # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_INITIALIZEUSERCODE_INCLUDESYMBOLS
PasswordComplexityPolicy:
MinLength: 8 # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_MINLENGTH
HasLowercase: true # ZITADEL_DEFAULTINSTANCE_PASSWORDCOMPLEXITYPOLICY_HASLOWERCASE

View File

@ -7,6 +7,7 @@ import {
GetDefaultInitMessageTextRequest as AdminGetDefaultInitMessageTextRequest,
GetDefaultPasswordChangeMessageTextRequest as AdminGetDefaultPasswordChangeMessageTextRequest,
GetDefaultPasswordlessRegistrationMessageTextRequest as AdminGetDefaultPasswordlessRegistrationMessageTextRequest,
GetDefaultInviteUserMessageTextRequest as AdminGetDefaultInviteUserMessageTextRequest,
GetDefaultPasswordResetMessageTextRequest as AdminGetDefaultPasswordResetMessageTextRequest,
GetDefaultVerifyEmailMessageTextRequest as AdminGetDefaultVerifyEmailMessageTextRequest,
GetDefaultVerifyEmailOTPMessageTextRequest as AdminGetDefaultVerifyEmailOTPMessageTextRequest,
@ -16,6 +17,7 @@ import {
SetDefaultInitMessageTextRequest,
SetDefaultPasswordChangeMessageTextRequest,
SetDefaultPasswordlessRegistrationMessageTextRequest,
SetDefaultInviteUserMessageTextRequest,
SetDefaultPasswordResetMessageTextRequest,
SetDefaultVerifyEmailMessageTextRequest,
SetDefaultVerifyEmailOTPMessageTextRequest,
@ -27,6 +29,7 @@ import {
GetCustomInitMessageTextRequest,
GetCustomPasswordChangeMessageTextRequest,
GetCustomPasswordlessRegistrationMessageTextRequest,
GetCustomInviteUserMessageTextRequest,
GetCustomPasswordResetMessageTextRequest,
GetCustomVerifyEmailMessageTextRequest,
GetCustomVerifyEmailOTPMessageTextRequest,
@ -36,6 +39,7 @@ import {
GetDefaultInitMessageTextRequest,
GetDefaultPasswordChangeMessageTextRequest,
GetDefaultPasswordlessRegistrationMessageTextRequest,
GetDefaultInviteUserMessageTextRequest,
GetDefaultPasswordResetMessageTextRequest,
GetDefaultVerifyEmailMessageTextRequest,
GetDefaultVerifyEmailOTPMessageTextRequest,
@ -45,6 +49,7 @@ import {
SetCustomInitMessageTextRequest,
SetCustomPasswordChangeMessageTextRequest,
SetCustomPasswordlessRegistrationMessageTextRequest,
SetCustomInviteUserMessageTextRequest,
SetCustomPasswordResetMessageTextRequest,
SetCustomVerifyEmailMessageTextRequest,
SetCustomVerifyEmailOTPMessageTextRequest,
@ -73,6 +78,7 @@ enum MESSAGETYPES {
PASSWORDCHANGE = 'PC',
VERIFYSMSOTP = 'VSO',
VERIFYEMAILOTP = 'VEO',
INVITEUSER = 'IU',
}
const REQUESTMAP = {
@ -226,6 +232,23 @@ const REQUESTMAP = {
req.setText(map.text ?? '');
req.setTitle(map.title ?? '');
return req;
},
},
[MESSAGETYPES.INVITEUSER]: {
get: new GetCustomInviteUserMessageTextRequest(),
set: new SetCustomInviteUserMessageTextRequest(),
getDefault: new GetDefaultInviteUserMessageTextRequest(),
setFcn: (map: Partial<SetCustomInviteUserMessageTextRequest.AsObject>): SetCustomInviteUserMessageTextRequest => {
const req = new SetCustomInviteUserMessageTextRequest();
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;
},
},
@ -371,6 +394,22 @@ const REQUESTMAP = {
req.setText(map.text ?? '');
req.setTitle(map.title ?? '');
return req;
},
},
[MESSAGETYPES.INVITEUSER]: {
get: new AdminGetDefaultInviteUserMessageTextRequest(),
set: new SetDefaultInviteUserMessageTextRequest(),
setFcn: (map: Partial<SetDefaultInviteUserMessageTextRequest.AsObject>): SetDefaultInviteUserMessageTextRequest => {
const req = new SetDefaultInviteUserMessageTextRequest();
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;
},
},
@ -540,6 +579,21 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.loginnames', value: '{{.LoginNames}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.changedate', value: '{{.ChangeDate}}' },
],
[MESSAGETYPES.INVITEUSER]: [
{ 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}}' },
{ key: 'POLICY.MESSAGE_TEXTS.CHIPS.applicationName', value: '{{.ApplicationName}}' },
],
};
public language: string = 'en';
@ -599,6 +653,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return this.stripEmail(this.service.getDefaultPasswordlessRegistrationMessageText(req));
case MESSAGETYPES.PASSWORDCHANGE:
return this.stripEmail(this.service.getDefaultPasswordChangeMessageText(req));
case MESSAGETYPES.INVITEUSER:
return this.stripEmail(this.service.getDefaultInviteUserMessageText(req));
}
}
@ -622,6 +678,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return this.stripEmail(this.service.getCustomPasswordlessRegistrationMessageText(req));
case MESSAGETYPES.PASSWORDCHANGE:
return this.stripEmail(this.service.getCustomPasswordChangeMessageText(req));
case MESSAGETYPES.INVITEUSER:
return this.stripEmail(this.service.getCustomInviteUserMessageText(req));
default:
return undefined;
}
@ -690,6 +748,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
);
case MESSAGETYPES.PASSWORDCHANGE:
return handler((this.service as ManagementService).setCustomPasswordChangeMessageText(this.updateRequest));
case MESSAGETYPES.INVITEUSER:
return handler((this.service as ManagementService).setCustomInviteUserMessageText(this.updateRequest));
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
switch (this.currentType) {
@ -711,6 +771,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return handler((this.service as AdminService).setDefaultPasswordlessRegistrationMessageText(this.updateRequest));
case MESSAGETYPES.PASSWORDCHANGE:
return handler((this.service as AdminService).setDefaultPasswordChangeMessageText(this.updateRequest));
case MESSAGETYPES.INVITEUSER:
return handler((this.service as AdminService).setDefaultInviteUserMessageText(this.updateRequest));
}
}
}
@ -763,6 +825,8 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
return handler(this.service.resetCustomPasswordlessRegistrationMessageTextToDefault(this.language));
case MESSAGETYPES.PASSWORDCHANGE:
return handler(this.service.resetCustomPasswordChangeMessageTextToDefault(this.language));
case MESSAGETYPES.INVITEUSER:
return handler(this.service.resetCustomInviteUserMessageTextToDefault(this.language));
default:
return Promise.reject();
}

View File

@ -72,6 +72,8 @@ import {
GetCustomPasswordChangeMessageTextResponse,
GetCustomPasswordlessRegistrationMessageTextRequest,
GetCustomPasswordlessRegistrationMessageTextResponse,
GetCustomInviteUserMessageTextRequest,
GetCustomInviteUserMessageTextResponse,
GetCustomPasswordResetMessageTextRequest,
GetCustomPasswordResetMessageTextResponse,
GetCustomVerifyEmailMessageTextRequest,
@ -96,6 +98,8 @@ import {
GetDefaultPasswordChangeMessageTextResponse,
GetDefaultPasswordlessRegistrationMessageTextRequest,
GetDefaultPasswordlessRegistrationMessageTextResponse,
GetDefaultInviteUserMessageTextRequest,
GetDefaultInviteUserMessageTextResponse,
GetDefaultPasswordResetMessageTextRequest,
GetDefaultPasswordResetMessageTextResponse,
GetDefaultVerifyEmailMessageTextRequest,
@ -224,6 +228,8 @@ import {
SetDefaultPasswordChangeMessageTextResponse,
SetDefaultPasswordlessRegistrationMessageTextRequest,
SetDefaultPasswordlessRegistrationMessageTextResponse,
SetDefaultInviteUserMessageTextRequest,
SetDefaultInviteUserMessageTextResponse,
SetDefaultPasswordResetMessageTextRequest,
SetDefaultPasswordResetMessageTextResponse,
SetDefaultVerifyEmailMessageTextRequest,
@ -311,6 +317,8 @@ import {
ResetCustomPasswordChangeMessageTextToDefaultResponse,
ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest,
ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse,
ResetCustomInviteUserMessageTextToDefaultRequest,
ResetCustomInviteUserMessageTextToDefaultResponse,
ResetCustomPasswordResetMessageTextToDefaultRequest,
ResetCustomPasswordResetMessageTextToDefaultResponse,
ResetCustomVerifyEmailMessageTextToDefaultRequest,
@ -722,6 +730,32 @@ export class AdminService {
return this.grpcService.admin.resetCustomPasswordChangeMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public getDefaultInviteUserMessageText(
req: GetDefaultInviteUserMessageTextRequest,
): Promise<GetDefaultInviteUserMessageTextResponse.AsObject> {
return this.grpcService.admin.getDefaultInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public getCustomInviteUserMessageText(
req: GetCustomInviteUserMessageTextRequest,
): Promise<GetCustomInviteUserMessageTextResponse.AsObject> {
return this.grpcService.admin.getCustomInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public setDefaultInviteUserMessageText(
req: SetDefaultInviteUserMessageTextRequest,
): Promise<SetDefaultInviteUserMessageTextResponse.AsObject> {
return this.grpcService.admin.setDefaultInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public resetCustomInviteUserMessageTextToDefault(
lang: string,
): Promise<ResetCustomInviteUserMessageTextToDefaultResponse.AsObject> {
const req = new ResetCustomInviteUserMessageTextToDefaultRequest();
req.setLanguage(lang);
return this.grpcService.admin.resetCustomInviteUserMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public SetUpOrg(org: SetUpOrgRequest.Org, human: SetUpOrgRequest.Human): Promise<SetUpOrgResponse.AsObject> {
const req = new SetUpOrgRequest();

View File

@ -133,6 +133,8 @@ import {
GetCustomLoginTextsResponse,
GetCustomPasswordChangeMessageTextRequest,
GetCustomPasswordChangeMessageTextResponse,
GetCustomInviteUserMessageTextRequest,
GetCustomInviteUserMessageTextResponse,
GetCustomPasswordlessRegistrationMessageTextRequest,
GetCustomPasswordlessRegistrationMessageTextResponse,
GetCustomPasswordResetMessageTextRequest,
@ -155,6 +157,8 @@ import {
GetDefaultLoginTextsResponse,
GetDefaultPasswordChangeMessageTextRequest,
GetDefaultPasswordChangeMessageTextResponse,
GetDefaultInviteUserMessageTextRequest,
GetDefaultInviteUserMessageTextResponse,
GetDefaultPasswordComplexityPolicyRequest,
GetDefaultPasswordComplexityPolicyResponse,
GetDefaultPasswordlessRegistrationMessageTextRequest,
@ -386,6 +390,8 @@ import {
ResetCustomLoginTextsToDefaultResponse,
ResetCustomPasswordChangeMessageTextToDefaultRequest,
ResetCustomPasswordChangeMessageTextToDefaultResponse,
ResetCustomInviteUserMessageTextToDefaultRequest,
ResetCustomInviteUserMessageTextToDefaultResponse,
ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest,
ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse,
ResetCustomPasswordResetMessageTextToDefaultRequest,
@ -423,6 +429,8 @@ import {
SetCustomLoginTextsResponse,
SetCustomPasswordChangeMessageTextRequest,
SetCustomPasswordChangeMessageTextResponse,
SetCustomInviteUserMessageTextRequest,
SetCustomInviteUserMessageTextResponse,
SetCustomPasswordlessRegistrationMessageTextRequest,
SetCustomPasswordlessRegistrationMessageTextResponse,
SetCustomPasswordResetMessageTextRequest,
@ -804,6 +812,32 @@ export class ManagementService {
return this.grpcService.mgmt.resetCustomPasswordChangeMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public getDefaultInviteUserMessageText(
req: GetDefaultInviteUserMessageTextRequest,
): Promise<GetDefaultInviteUserMessageTextResponse.AsObject> {
return this.grpcService.mgmt.getDefaultInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public getCustomInviteUserMessageText(
req: GetCustomInviteUserMessageTextRequest,
): Promise<GetCustomInviteUserMessageTextResponse.AsObject> {
return this.grpcService.mgmt.getCustomInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public setCustomInviteUserMessageText(
req: SetCustomInviteUserMessageTextRequest,
): Promise<SetCustomInviteUserMessageTextResponse.AsObject> {
return this.grpcService.mgmt.setCustomInviteUserMessageCustomText(req, null).then((resp) => resp.toObject());
}
public resetCustomInviteUserMessageTextToDefault(
lang: string,
): Promise<ResetCustomInviteUserMessageTextToDefaultResponse.AsObject> {
const req = new ResetCustomInviteUserMessageTextToDefaultRequest();
req.setLanguage(lang);
return this.grpcService.mgmt.resetCustomInviteUserMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public updateUserName(userId: string, username: string): Promise<UpdateUserNameResponse.AsObject> {
const req = new UpdateUserNameRequest();
req.setUserId(userId);

View File

@ -197,7 +197,8 @@
"VE": "Когато потребител промени своя имейл адрес, той ще получи имейл с връзка за верифициране на новия адрес.",
"VP": "Когато потребител промени своя телефонен номер, той ще получи SMS с код за верификация на новия номер.",
"VEO": "Когато потребител добави метод за One-Time Password чрез имейл, трябва да го активира, като въведе код, изпратен на неговия имейл адрес.",
"VSO": "Когато потребител добави метод за One-Time Password чрез SMS, трябва да го активира, като въведе код, изпратен на неговия телефонен номер."
"VSO": "Когато потребител добави метод за One-Time Password чрез SMS, трябва да го активира, като въведе код, изпратен на неговия телефонен номер.",
"IU": "Когато се създаде покана за потребител, те ще получат имейл с връзка за задаване на своя метод за удостоверяване."
}
},
"LOGIN_TEXTS": {
@ -1666,7 +1667,8 @@
"PR": "Нулиране на парола",
"DC": "Заявка за домейн",
"PL": "Без парола",
"PC": "Промяна на паролата"
"PC": "Промяна на паролата",
"IU": "Покана за потребител"
},
"CHIPS": {
"firstname": "Първо име",
@ -1686,7 +1688,8 @@
"tempUsername": "Временно потребителско име",
"otp": "Еднократна парола",
"verifyUrl": "URL за потвърждаване на еднократна парола",
"expiry": "Изтичане"
"expiry": "Изтичане",
"applicationName": "Името на приложението"
},
"TOAST": {
"UPDATED": "Персонализираните текстове са запазени."

View File

@ -197,7 +197,8 @@
"VE": "Když uživatel změní svou e-mailovou adresu, obdrží e-mail s odkazem na ověření nové adresy.",
"VP": "Když uživatel změní své telefonní číslo, obdrží SMS s kódem pro ověření nového čísla.",
"VEO": "Když uživatel přidá metodu jednorázového hesla přes e-mail, musí ji aktivovat zadáním kódu poslaného na jeho e-mailovou adresu.",
"VSO": "Když uživatel přidá metodu jednorázového hesla přes SMS, musí ji aktivovat zadáním kódu poslaného na jeho telefonní číslo."
"VSO": "Když uživatel přidá metodu jednorázového hesla přes SMS, musí ji aktivovat zadáním kódu poslaného na jeho telefonní číslo.",
"IU": "Když se vytvoří pozvánka pro uživatele, obdrží e-mail s odkazem na nastavení své metody ověřování."
}
},
"LOGIN_TEXTS": {
@ -1667,7 +1668,8 @@
"PR": "Reset hesla",
"DC": "Nárok na doménu",
"PL": "Bezheslový",
"PC": "Změna hesla"
"PC": "Změna hesla",
"IU": "Pozvat uživatele"
},
"CHIPS": {
"firstname": "Křestní jméno",
@ -1687,7 +1689,8 @@
"tempUsername": "Dočasné uživatelské jméno",
"otp": "Jednorázové heslo",
"verifyUrl": "Ověřovací URL jednorázového hesla",
"expiry": "Expirace"
"expiry": "Expirace",
"applicationName": "Název aplikace"
},
"TOAST": {
"UPDATED": "Vlastní texty uloženy."

View File

@ -197,7 +197,8 @@
"VE": "Wenn ein Benutzer seine E-Mail-Adresse ändert, erhält er eine E-Mail mit einem Link zur Verifizierung der neuen Adresse.",
"VP": "Wenn ein Benutzer seine Telefonnummer ändert, erhält er eine SMS mit einem Code zur Verifizierung der neuen Nummer.",
"VEO": "Wenn ein Benutzer eine Einmalpasswort-Methode per E-Mail hinzufügt, muss er sie aktivieren, indem er einen Code eingibt, der an seine E-Mail-Adresse gesendet wurde.",
"VSO": "Wenn ein Benutzer eine Einmalpasswort-Methode per SMS hinzufügt, muss er sie aktivieren, indem er einen Code eingibt, der an seine Telefonnummer gesendet wurde."
"VSO": "Wenn ein Benutzer eine Einmalpasswort-Methode per SMS hinzufügt, muss er sie aktivieren, indem er einen Code eingibt, der an seine Telefonnummer gesendet wurde.",
"IU": "Wenn ein Benutzer-Einladungscode erstellt wird, erhält er eine E-Mail mit einem Link zur Einstellung seiner Authentifizierungsmethode."
}
},
"LOGIN_TEXTS": {
@ -1667,7 +1668,8 @@
"PR": "Passwort Wiederherstellung",
"DC": "Domainbeanspruchung",
"PL": "Passwortlos",
"PC": "Passwordwechsel"
"PC": "Passwordwechsel",
"IU": "Benutzer einladen"
},
"CHIPS": {
"firstname": "Vorname",
@ -1687,7 +1689,8 @@
"tempUsername": "Temp. Username",
"otp": "Einmalpasswort",
"verifyUrl": "URL zur Überprüfung des Einmalpassworts",
"expiry": "Ablauf"
"expiry": "Ablauf",
"applicationName": "Anwendungsname"
},
"TOAST": {
"UPDATED": "Benutzerdefinierte Texte gespeichert."

View File

@ -197,7 +197,8 @@
"VE": "When a user changes their email address, they will receive an email with a link to verify the new address.",
"VP": "When a user changes their phone number, they will receive an SMS with a code to verify the new number.",
"VEO": "When a user adds a One-Time Password via email method, they need to activate it by entering a code sent to their email address.",
"VSO": "When a user adds a One-Time Password via SMS method, they need to activate it by entering a code sent to their phone number."
"VSO": "When a user adds a One-Time Password via SMS method, they need to activate it by entering a code sent to their phone number.",
"IU": "When a user invite code is created, they will receive an email with a link to set their authentication method."
}
},
"LOGIN_TEXTS": {
@ -1667,7 +1668,8 @@
"PR": "Password Reset",
"DC": "Domain Claim",
"PL": "Passwordless",
"PC": "Password Change"
"PC": "Password Change",
"IU": "Invite User"
},
"CHIPS": {
"firstname": "Given name",
@ -1687,7 +1689,8 @@
"tempUsername": "Temp username",
"otp": "One-time password",
"verifyUrl": "Verify One-time-password URL",
"expiry": "Expiry"
"expiry": "Expiry",
"applicationName": "Application name"
},
"TOAST": {
"UPDATED": "Custom Texts saved."

View File

@ -197,7 +197,8 @@
"VE": "Cuando un usuario cambia su dirección de correo electrónico, recibirá un correo electrónico con un enlace para verificar la nueva dirección.",
"VP": "Cuando un usuario cambia su número de teléfono, recibirá un SMS con un código para verificar el nuevo número.",
"VEO": "Cuando un usuario agrega una Contraseña de Un Solo Uso mediante correo electrónico, necesita activarla ingresando un código enviado a su dirección de correo electrónico.",
"VSO": "Cuando un usuario agrega una Contraseña de Un Solo Uso mediante SMS, necesita activarla ingresando un código enviado a su número de teléfono."
"VSO": "Cuando un usuario agrega una Contraseña de Un Solo Uso mediante SMS, necesita activarla ingresando un código enviado a su número de teléfono.",
"IU": "Cuando se crea un código de invitación de usuario, recibirán un correo electrónico con un enlace para configurar su método de autenticación."
}
},
"LOGIN_TEXTS": {
@ -1668,7 +1669,8 @@
"PR": "Restablecimiento de contraseña",
"DC": "Reclamar un dominio",
"PL": "Acceso sin contraseña",
"PC": "Cambio de contraseña"
"PC": "Cambio de contraseña",
"IU": "Invitar usuario"
},
"CHIPS": {
"firstname": "Nombre",
@ -1688,7 +1690,8 @@
"tempUsername": "Nombre de usuario temporal",
"otp": "Contraseña de un solo uso",
"verifyUrl": "URL para verificar la contraseña de un solo uso",
"expiry": "Expiración"
"expiry": "Expiración",
"applicationName": "Nombre de la aplicación"
},
"TOAST": {
"UPDATED": "Textos personalizados guardados."

View File

@ -197,7 +197,8 @@
"VE": "Lorsqu'un utilisateur change son adresse e-mail, il recevra un e-mail avec un lien pour vérifier la nouvelle adresse.",
"VP": "Lorsqu'un utilisateur change son numéro de téléphone, il recevra un SMS avec un code pour vérifier le nouveau numéro.",
"VEO": "Lorsqu'un utilisateur ajoute un Mot de Passe à Usage Unique via e-mail, il doit l'activer en entrant un code envoyé à son adresse e-mail.",
"VSO": "Lorsqu'un utilisateur ajoute un Mot de Passe à Usage Unique via SMS, il doit l'activer en entrant un code envoyé à son numéro de téléphone."
"VSO": "Lorsqu'un utilisateur ajoute un Mot de Passe à Usage Unique via SMS, il doit l'activer en entrant un code envoyé à son numéro de téléphone.",
"IU": "Lorsqu'un code d'invitation d'utilisateur est créé, il recevra un e-mail avec un lien pour configurer sa méthode d'authentification."
}
},
"LOGIN_TEXTS": {
@ -1667,7 +1668,8 @@
"PR": "Réinitialisation du mot de passe",
"DC": "Réclamation de domaine",
"PL": "Sans mot de passe",
"PC": "Modification du mot de passe"
"PC": "Modification du mot de passe",
"IU": "Inviter un utilisateur"
},
"CHIPS": {
"firstname": "Prénom",
@ -1687,7 +1689,8 @@
"tempUsername": "Nom d'utilisateur temporaire",
"otp": "Mot de passe à usage unique",
"verifyUrl": "URL pour vérifier le mot de passe à usage unique",
"expiry": "Expiration"
"expiry": "Expiration",
"applicationName": "Nom de l'application"
},
"TOAST": {
"UPDATED": "Textes personnalisés enregistrés."

View File

@ -185,7 +185,8 @@
"VE": "Saat pengguna mengubah alamat emailnya, mereka akan menerima email berisi tautan untuk memverifikasi alamat baru.",
"VP": "Saat pengguna mengganti nomor teleponnya, mereka akan menerima SMS berisi kode untuk memverifikasi nomor baru.",
"VEO": "Ketika pengguna menambahkan Kata Sandi Sekali Pakai melalui metode email, mereka perlu mengaktifkannya dengan memasukkan kode yang dikirimkan ke alamat email mereka.",
"VSO": "Ketika pengguna menambahkan One-Time Password melalui metode SMS, mereka perlu mengaktifkannya dengan memasukkan kode yang dikirimkan ke nomor telepon mereka."
"VSO": "Ketika pengguna menambahkan One-Time Password melalui metode SMS, mereka perlu mengaktifkannya dengan memasukkan kode yang dikirimkan ke nomor telepon mereka.",
"IU": "Ketika kode undangan pengguna dibuat, mereka akan menerima email dengan tautan untuk mengatur metode otentikasi mereka."
}
},
"LOGIN_TEXTS": {
@ -1533,7 +1534,8 @@
"PR": "Reset Kata Sandi",
"DC": "Klaim Domain",
"PL": "Tanpa kata sandi",
"PC": "Perubahan Kata Sandi"
"PC": "Perubahan Kata Sandi",
"IU": "Mengundang Pengguna"
},
"CHIPS": {
"firstname": "Nama yang diberikan",
@ -1553,7 +1555,8 @@
"tempUsername": "Nama pengguna sementara",
"otp": "Kata sandi satu kali",
"verifyUrl": "Verifikasi URL kata sandi satu kali",
"expiry": "Kedaluwarsa"
"expiry": "Kedaluwarsa",
"applicationName": "Nama aplikasi"
},
"TOAST": { "UPDATED": "Teks Khusus disimpan." }
},

View File

@ -197,7 +197,8 @@
"VE": "Quando un utente cambia il suo indirizzo email, riceverà un'email con un link per verificare il nuovo indirizzo.",
"VP": "Quando un utente cambia il suo numero di telefono, riceverà un SMS con un codice per verificare il nuovo numero.",
"VEO": "Quando un utente aggiunge una Password Monouso tramite metodo email, deve attivarla inserendo un codice inviato al suo indirizzo email.",
"VSO": "Quando un utente aggiunge una Password Monouso tramite metodo SMS, deve attivarla inserendo un codice inviato al suo numero di telefono."
"VSO": "Quando un utente aggiunge una Password Monouso tramite metodo SMS, deve attivarla inserendo un codice inviato al suo numero di telefono.",
"IU": "Quando viene creato un codice di invito per un utente, riceverà un'e-mail con un collegamento per impostare il suo metodo di autenticazione."
}
},
"LOGIN_TEXTS": {
@ -1667,7 +1668,8 @@
"PR": "Ripristino della password",
"DC": "Rivendicazione del dominio",
"PL": "Autenticazione Passwordless",
"PC": "Cambiamento della password"
"PC": "Cambiamento della password",
"IU": "Invita utente"
},
"CHIPS": {
"firstname": "Nome",
@ -1687,7 +1689,8 @@
"tempUsername": "Nome utente temporaneo",
"otp": "Password monouso",
"verifyUrl": "URL per verificare la password monouso",
"expiry": "Scadenza"
"expiry": "Scadenza",
"applicationName": "Nome dell'applicazione"
},
"TOAST": {
"UPDATED": "Testi personalizzati salvati."

View File

@ -197,7 +197,8 @@
"VE": "ユーザーがメールアドレスを変更すると、新しいアドレスを検証するリンクが記載されたメールを受け取ります。",
"VP": "ユーザーが電話番号を変更すると、新しい番号を検証するコードが記載されたSMSを受け取ります。",
"VEO": "ユーザーがメール経由でワンタイムパスワードの方法を追加すると、それをアクティブにするためにメールに送信されたコードを入力する必要があります。",
"VSO": "ユーザーがSMS経由でワンタイムパスワードの方法を追加すると、それをアクティブにするために電話番号に送信されたコードを入力する必要があります。"
"VSO": "ユーザーがSMS経由でワンタイムパスワードの方法を追加すると、それをアクティブにするために電話番号に送信されたコードを入力する必要があります。",
"IU": "ユーザー招待コードが作成されると、認証方法を設定するためのリンクを含むメールが送信されます。"
}
},
"LOGIN_TEXTS": {
@ -1683,7 +1684,8 @@
"tempUsername": "一時ユーザー名",
"otp": "ワンタイムパスワード",
"verifyUrl": "ワンタイムパスワードを確認するURL",
"expiry": "有効期限"
"expiry": "有効期限",
"applicationName": "アプリケーション名"
},
"TOAST": {
"UPDATED": "カスタムテキストが保存されました。"

View File

@ -197,7 +197,8 @@
"VE": "Кога корисник ја менува својата е-маил адреса, тој ќе добие е-маил со врска за верификација на новата адреса.",
"VP": "Кога корисник ја менува својата телефонска бројка, тој ќе добие SMS со код за верификација на новиот број.",
"VEO": "Кога корисник додава метод за еднократна лозинка преку е-маил, потребно е да го активира со внесување на кодот испратен на нивната е-маил адреса.",
"VSO": "Кога корисник додава метод за еднократна лозинка преку SMS, потребно е да го активира со внесување на кодот испратен на нивниот телефонски број."
"VSO": "Кога корисник додава метод за еднократна лозинка преку SMS, потребно е да го активира со внесување на кодот испратен на нивниот телефонски број.",
"IU": "Кога се создаде покана за корисникот, тие ќе добијат имејл со врска за поставување на нивниот метод за автентикација."
}
},
"LOGIN_TEXTS": {
@ -1668,7 +1669,8 @@
"PR": "Ресетирање на лозинка",
"DC": "Зафатница на домен",
"PL": "Лозинка без лозинка",
"PC": "Промена на лозинка"
"PC": "Промена на лозинка",
"IU": "Покана за корисникот"
},
"CHIPS": {
"firstname": "Име",
@ -1688,7 +1690,8 @@
"tempUsername": "Привремено корисничко име",
"otp": "Еднократна лозинка",
"verifyUrl": "URL за потврдување на еднократна лозинка",
"expiry": "Истекување"
"expiry": "Истекување",
"applicationName": "Името на апликацијата"
},
"TOAST": {
"UPDATED": "Прилагодените текстови се зачувани."

View File

@ -197,7 +197,8 @@
"VE": "Wanneer een gebruiker zijn e-mailadres wijzigt, ontvangt hij een e-mail met een link om het nieuwe adres te verifiëren.",
"VP": "Wanneer een gebruiker zijn telefoonnummer wijzigt, ontvangt hij een SMS met een code om het nieuwe nummer te verifiëren.",
"VEO": "Wanneer een gebruiker een eenmalig wachtwoord via e-mailmethode toevoegt, moeten ze dit activeren door een code in te voeren die naar hun e-mailadres is verzonden.",
"VSO": "Wanneer een gebruiker een eenmalig wachtwoord via SMS-methode toevoegt, moeten ze dit activeren door een code in te voeren die naar hun telefoonnummer is verzonden."
"VSO": "Wanneer een gebruiker een eenmalig wachtwoord via SMS-methode toevoegt, moeten ze dit activeren door een code in te voeren die naar hun telefoonnummer is verzonden.",
"IU": "Wanneer een uitnodigingscode voor gebruikers wordt gemaakt, ontvangt de gebruiker een e-mail met een link om zijn verificatiemethode in te stellen."
}
},
"LOGIN_TEXTS": {
@ -1666,7 +1667,8 @@
"PR": "Wachtwoord Reset",
"DC": "Domein Claim",
"PL": "Wachtwoordloos",
"PC": "Wachtwoord Verandering"
"PC": "Wachtwoord Verandering",
"IU": "Gebruiker uitnodigen"
},
"CHIPS": {
"firstname": "Voornaam",
@ -1686,7 +1688,8 @@
"tempUsername": "Tijdelijke gebruikersnaam",
"otp": "Eenmalig wachtwoord",
"verifyUrl": "Verifieer Eenmalig-wachtwoord URL",
"expiry": "Vervaldatum"
"expiry": "Vervaldatum",
"applicationName": "Toepassingsnaam"
},
"TOAST": {
"UPDATED": "Aangepaste Teksten opgeslagen."

View File

@ -197,7 +197,8 @@
"VE": "Gdy użytkownik zmieni swój adres e-mail, otrzyma e-mail z linkiem do weryfikacji nowego adresu.",
"VP": "Gdy użytkownik zmieni swój numer telefonu, otrzyma SMS z kodem do weryfikacji nowego numeru.",
"VEO": "Gdy użytkownik doda metodę jednorazowego hasła przez e-mail, musi ją aktywować, wprowadzając kod wysłany na jego adres e-mail.",
"VSO": "Gdy użytkownik doda metodę jednorazowego hasła przez SMS, musi ją aktywować, wprowadzając kod wysłany na jego numer telefonu."
"VSO": "Gdy użytkownik doda metodę jednorazowego hasła przez SMS, musi ją aktywować, wprowadzając kod wysłany na jego numer telefonu.",
"IU": "Kiedy zostanie utworzony kod zaproszenia użytkownika, otrzyma on e-mail z linkiem do ustawienia swojej metody uwierzytelniania."
}
},
"LOGIN_TEXTS": {
@ -1666,7 +1667,8 @@
"PR": "Resetowanie hasła",
"DC": "Rejestracja domeny",
"PL": "Bez hasła",
"PC": "Zmiana hasła"
"PC": "Zmiana hasła",
"IU": "Zaproś użytkownika"
},
"CHIPS": {
"firstname": "Imię",
@ -1686,7 +1688,8 @@
"tempUsername": "Tymczasowa nazwa użytkownika",
"otp": "Hasło jednorazowe",
"verifyUrl": "URL do weryfikacji hasła jednorazowego",
"expiry": "Wygaśnięcie"
"expiry": "Wygaśnięcie",
"applicationName": "Nazwa aplikacji"
},
"TOAST": {
"UPDATED": "Teksty niestandardowe zapisane."

View File

@ -197,7 +197,8 @@
"VE": "Quando um usuário muda seu endereço de e-mail, ele receberá um e-mail com um link para verificar o novo endereço.",
"VP": "Quando um usuário muda seu número de telefone, ele receberá um SMS com um código para verificar o novo número.",
"VEO": "Quando um usuário adiciona um método de Senha Única via e-mail, ele precisa ativá-lo inserindo um código enviado para seu endereço de e-mail.",
"VSO": "Quando um usuário adiciona um método de Senha Única via SMS, ele precisa ativá-lo inserindo um código enviado para seu número de telefone."
"VSO": "Quando um usuário adiciona um método de Senha Única via SMS, ele precisa ativá-lo inserindo um código enviado para seu número de telefone.",
"IU": "Quando um código de convite de usuário é criado, eles receberão um e-mail com um link para configurar seu método de autenticação."
}
},
"LOGIN_TEXTS": {
@ -1668,7 +1669,8 @@
"PR": "Redefinição de Senha",
"DC": "Reivindicação de Domínio",
"PL": "Sem senha",
"PC": "Alteração de Senha"
"PC": "Alteração de Senha",
"IU": "Convidar usuário"
},
"CHIPS": {
"firstname": "Nome próprio",
@ -1688,7 +1690,8 @@
"tempUsername": "Nome de usuário temporário",
"otp": "Senha de uso único",
"verifyUrl": "URL para verificar a senha de uso único",
"expiry": "Data de expiração"
"expiry": "Data de expiração",
"applicationName": "Nome do aplicativo"
},
"TOAST": {
"UPDATED": "Textos personalizados salvos."

View File

@ -197,7 +197,8 @@
"VE": "Когда пользователь меняет свой адрес электронной почты, он получает электронное письмо со ссылкой для подтверждения нового адреса.",
"VP": "Когда пользователь меняет свой телефонный номер, он получает SMS с кодом для подтверждения нового номера.",
"VEO": "Когда пользователь добавляет метод одноразового пароля по электронной почте, ему необходимо активировать его, введя код, отправленный на его адрес электронной почты.",
"VSO": "Когда пользователь добавляет метод одноразового пароля по SMS, ему необходимо активировать его, введя код, отправленный на его телефонный номер."
"VSO": "Когда пользователь добавляет метод одноразового пароля по SMS, ему необходимо активировать его, введя код, отправленный на его телефонный номер.",
"IU": "Когда создается код приглашения пользователя, он получит электронное письмо со ссылкой для настройки своего метода аутентификации."
}
},
"LOGIN_TEXTS": {
@ -1735,7 +1736,8 @@
"PR": "Восстановление пароля",
"DC": "Утверждение домена",
"PL": "Без пароля",
"PC": "Изменение пароля"
"PC": "Изменение пароля",
"IU": "Пригласить пользователя"
},
"CHIPS": {
"firstname": "Имя",
@ -1755,7 +1757,8 @@
"tempUsername": "Временное имя пользователя",
"otp": "Одноразовый пароль",
"verifyUrl": "Проверка URL-адреса с одноразовым паролем",
"expiry": "Срок действия"
"expiry": "Срок действия",
"applicationName": "Имя приложения"
},
"TOAST": {
"UPDATED": "Тексты сохранены."

View File

@ -197,7 +197,8 @@
"VE": "När en användare ändrar sin e-postadress, kommer de att få ett mail med en länk för att verifiera den nya adressen.",
"VP": "När en användare ändrar sitt telefonnummer, kommer de att få ett SMS med en kod för att verifiera det nya numret.",
"VEO": "När en användare lägger till en engångslösenord via e-postmetod, måste de aktivera den genom att ange en kod som skickas till deras e-postadress.",
"VSO": "När en användare lägger till en engångslösenord via SMS-metod, måste de aktivera den genom att ange en kod som skickas till deras telefonnummer."
"VSO": "När en användare lägger till en engångslösenord via SMS-metod, måste de aktivera den genom att ange en kod som skickas till deras telefonnummer.",
"IU": "När en inbjudningskod för användare skapas, får de ett e-mail med en länk för att ställa in sin autentiseringsmetod."
}
},
"LOGIN_TEXTS": {
@ -1671,7 +1672,8 @@
"PR": "Återställ Lösenord",
"DC": "Domänkrav",
"PL": "lösenordsfri",
"PC": "Lösenordsändring"
"PC": "Lösenordsändring",
"IU": "Bjud in användare"
},
"CHIPS": {
"firstname": "Förnamn",
@ -1691,7 +1693,8 @@
"tempUsername": "Tillfälligt användarnamn",
"otp": "Engångslösenord",
"verifyUrl": "Verifiera Engångslösenord URL",
"expiry": "Utgångsdatum"
"expiry": "Utgångsdatum",
"applicationName": "Applikationsnamn"
},
"TOAST": {
"UPDATED": "Anpassade Texter sparade."

View File

@ -197,7 +197,8 @@
"VE": "当用户更改其电子邮件地址时,他们将收到一封带有验证新地址链接的电子邮件。",
"VP": "当用户更改其电话号码时,他们将收到一条带有验证新号码的代码的短信。",
"VEO": "当用户通过电子邮件方式添加一次性密码时,他们需要通过输入发送到其电子邮件地址的代码来激活它。",
"VSO": "当用户通过短信方式添加一次性密码时,他们需要通过输入发送到其电话号码的代码来激活它。"
"VSO": "当用户通过短信方式添加一次性密码时,他们需要通过输入发送到其电话号码的代码来激活它。",
"IU": "当创建用户邀请代码时,他们将收到一封包含设置其身份验证方法的链接的电子邮件。"
}
},
"LOGIN_TEXTS": {
@ -1666,7 +1667,8 @@
"PR": "重置密码",
"DC": "域名声明",
"PL": "无密码身份验证",
"PC": "修改密码"
"PC": "修改密码",
"IU": "邀请用户"
},
"CHIPS": {
"firstname": "名",
@ -1686,7 +1688,8 @@
"tempUsername": "临时用户名",
"otp": "一次性密码",
"verifyUrl": "验证一次性密码的URL",
"expiry": "过期时间"
"expiry": "过期时间",
"applicationName": "应用程序名称"
},
"TOAST": {
"UPDATED": "自定义文本已保存。"

View File

@ -396,6 +396,54 @@ func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Conte
}, nil
}
func (s *Server) GetDefaultInviteUserMessageText(ctx context.Context, req *admin_pb.GetDefaultInviteUserMessageTextRequest) (*admin_pb.GetDefaultInviteUserMessageTextResponse, error) {
msg, err := s.query.DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx, domain.InviteUserMessageType, req.Language)
if err != nil {
return nil, err
}
return &admin_pb.GetDefaultInviteUserMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetCustomInviteUserMessageText(ctx context.Context, req *admin_pb.GetCustomInviteUserMessageTextRequest) (*admin_pb.GetCustomInviteUserMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetInstance(ctx).InstanceID(), domain.InviteUserMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &admin_pb.GetCustomInviteUserMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetDefaultInviteUserMessageText(ctx context.Context, req *admin_pb.SetDefaultInviteUserMessageTextRequest) (*admin_pb.SetDefaultInviteUserMessageTextResponse, error) {
result, err := s.command.SetDefaultMessageText(ctx, authz.GetInstance(ctx).InstanceID(), SetInviteUserCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &admin_pb.SetDefaultInviteUserMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomInviteUserMessageTextToDefault(ctx context.Context, req *admin_pb.ResetCustomInviteUserMessageTextToDefaultRequest) (*admin_pb.ResetCustomInviteUserMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveInstanceMessageTexts(ctx, domain.InviteUserMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &admin_pb.ResetCustomInviteUserMessageTextToDefaultResponse{
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

@ -122,6 +122,21 @@ func SetPasswordChangeCustomTextToDomain(msg *admin_pb.SetDefaultPasswordChangeM
}
}
func SetInviteUserCustomTextToDomain(msg *admin_pb.SetDefaultInviteUserMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.InviteUserMessageType,
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

@ -793,6 +793,7 @@ func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD
importVerifyPhoneMessageTexts(ctx, s, errors, org)
importDomainClaimedMessageTexts(ctx, s, errors, org)
importPasswordlessRegistrationMessageTexts(ctx, s, errors, org)
importInviteUserMessageTexts(ctx, s, errors, org)
if err := importHumanUsers(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode); err != nil {
return err
}
@ -975,6 +976,21 @@ func importPasswordlessRegistrationMessageTexts(ctx context.Context, s *Server,
}
}
func importInviteUserMessageTexts(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, org *admin_pb.DataOrg) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.End() }()
if org.PasswordlessRegistrationMessages == nil {
return
}
for _, message := range org.GetInviteUserMessages() {
_, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetInviteUserCustomTextToDomain(message))
if err != nil {
*errors = append(*errors, &admin_pb.ImportDataError{Type: "invite_user_messages", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()})
}
}
}
func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, success *admin_pb.ImportDataSuccess, count *counts, org *admin_pb.DataOrg) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -1236,6 +1252,7 @@ func (s *Server) dataOrgsV1ToDataOrgs(ctx context.Context, dataOrgs *v1_pb.Impor
VerifyPhoneMessages: orgV1.GetVerifyPhoneMessages(),
DomainClaimedMessages: orgV1.GetDomainClaimedMessages(),
PasswordlessRegistrationMessages: orgV1.GetPasswordlessRegistrationMessages(),
InviteUserMessages: orgV1.GetInviteUserMessages(),
OidcIdps: orgV1.GetOidcIdps(),
JwtIdps: orgV1.GetJwtIdps(),
UserLinks: orgV1.GetUserLinks(),

View File

@ -396,6 +396,54 @@ func (s *Server) ResetCustomPasswordChangeMessageTextToDefault(ctx context.Conte
}, nil
}
func (s *Server) GetCustomInviteUserMessageText(ctx context.Context, req *mgmt_pb.GetCustomInviteUserMessageTextRequest) (*mgmt_pb.GetCustomInviteUserMessageTextResponse, error) {
msg, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, authz.GetCtxData(ctx).OrgID, domain.InviteUserMessageType, req.Language, false)
if err != nil {
return nil, err
}
return &mgmt_pb.GetCustomInviteUserMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) GetDefaultInviteUserMessageText(ctx context.Context, req *mgmt_pb.GetDefaultInviteUserMessageTextRequest) (*mgmt_pb.GetDefaultInviteUserMessageTextResponse, error) {
msg, err := s.query.IAMMessageTextByTypeAndLanguage(ctx, domain.InviteUserMessageType, req.Language)
if err != nil {
return nil, err
}
return &mgmt_pb.GetDefaultInviteUserMessageTextResponse{
CustomText: text_grpc.ModelCustomMessageTextToPb(msg),
}, nil
}
func (s *Server) SetCustomInviteUserMessageCustomText(ctx context.Context, req *mgmt_pb.SetCustomInviteUserMessageTextRequest) (*mgmt_pb.SetCustomInviteUserMessageTextResponse, error) {
result, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, SetInviteUserCustomTextToDomain(req))
if err != nil {
return nil, err
}
return &mgmt_pb.SetCustomInviteUserMessageTextResponse{
Details: object.ChangeToDetailsPb(
result.Sequence,
result.EventDate,
result.ResourceOwner,
),
}, nil
}
func (s *Server) ResetCustomInviteUserMessageTextToDefault(ctx context.Context, req *mgmt_pb.ResetCustomInviteUserMessageTextToDefaultRequest) (*mgmt_pb.ResetCustomInviteUserMessageTextToDefaultResponse, error) {
result, err := s.command.RemoveOrgMessageTexts(ctx, authz.GetCtxData(ctx).OrgID, domain.InviteUserMessageType, language.Make(req.Language))
if err != nil {
return nil, err
}
return &mgmt_pb.ResetCustomInviteUserMessageTextToDefaultResponse{
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

@ -122,6 +122,21 @@ func SetPasswordChangeCustomTextToDomain(msg *mgmt_pb.SetCustomPasswordChangeMes
}
}
func SetInviteUserCustomTextToDomain(msg *mgmt_pb.SetCustomInviteUserMessageTextRequest) *domain.CustomMessageText {
langTag := language.Make(msg.Language)
return &domain.CustomMessageText{
MessageTextType: domain.InviteUserMessageType,
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

@ -2437,3 +2437,310 @@ func TestServer_ListAuthenticationMethodTypes(t *testing.T) {
})
}
}
func TestServer_CreateInviteCode(t *testing.T) {
type args struct {
ctx context.Context
req *user.CreateInviteCodeRequest
prepare func(request *user.CreateInviteCodeRequest) error
}
tests := []struct {
name string
args args
want *user.CreateInviteCodeResponse
wantErr bool
}{
{
name: "create, not existing",
args: args{
CTX,
&user.CreateInviteCodeRequest{
UserId: "notexisting",
},
func(request *user.CreateInviteCodeRequest) error { return nil },
},
wantErr: true,
},
{
name: "create, ok",
args: args{
ctx: CTX,
req: &user.CreateInviteCodeRequest{},
prepare: func(request *user.CreateInviteCodeRequest) error {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
want: &user.CreateInviteCodeResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.DefaultOrg.Id,
},
},
},
{
name: "create, invalid template",
args: args{
ctx: CTX,
req: &user.CreateInviteCodeRequest{
Verification: &user.CreateInviteCodeRequest_SendCode{
SendCode: &user.SendInviteCode{
UrlTemplate: gu.Ptr("{{"),
},
},
},
prepare: func(request *user.CreateInviteCodeRequest) error {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
wantErr: true,
},
{
name: "create, valid template",
args: args{
ctx: CTX,
req: &user.CreateInviteCodeRequest{
Verification: &user.CreateInviteCodeRequest_SendCode{
SendCode: &user.SendInviteCode{
UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"),
ApplicationName: gu.Ptr("TestApp"),
},
},
},
prepare: func(request *user.CreateInviteCodeRequest) error {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
want: &user.CreateInviteCodeResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.DefaultOrg.Id,
},
},
},
{
name: "create, return code, ok",
args: args{
ctx: CTX,
req: &user.CreateInviteCodeRequest{
Verification: &user.CreateInviteCodeRequest_ReturnCode{
ReturnCode: &user.ReturnInviteCode{},
},
},
prepare: func(request *user.CreateInviteCodeRequest) error {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
want: &user.CreateInviteCodeResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.DefaultOrg.Id,
},
InviteCode: gu.Ptr("something"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.CreateInviteCode(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
if tt.want.GetInviteCode() != "" {
assert.NotEmpty(t, got.GetInviteCode())
} else {
assert.Empty(t, got.GetInviteCode())
}
})
}
}
func TestServer_ResendInviteCode(t *testing.T) {
type args struct {
ctx context.Context
req *user.ResendInviteCodeRequest
prepare func(request *user.ResendInviteCodeRequest) error
}
tests := []struct {
name string
args args
want *user.ResendInviteCodeResponse
wantErr bool
}{
{
name: "user not existing",
args: args{
CTX,
&user.ResendInviteCodeRequest{
UserId: "notexisting",
},
func(request *user.ResendInviteCodeRequest) error { return nil },
},
wantErr: true,
},
{
name: "code not existing",
args: args{
ctx: CTX,
req: &user.ResendInviteCodeRequest{},
prepare: func(request *user.ResendInviteCodeRequest) error {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
wantErr: true,
},
{
name: "code not sent before",
args: args{
ctx: CTX,
req: &user.ResendInviteCodeRequest{},
prepare: func(request *user.ResendInviteCodeRequest) error {
userResp := Instance.CreateHumanUser(CTX)
request.UserId = userResp.GetUserId()
Instance.CreateInviteCode(CTX, userResp.GetUserId())
return nil
},
},
wantErr: true,
},
{
name: "resend, ok",
args: args{
ctx: CTX,
req: &user.ResendInviteCodeRequest{},
prepare: func(request *user.ResendInviteCodeRequest) error {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
_, err := Instance.Client.UserV2.CreateInviteCode(CTX, &user.CreateInviteCodeRequest{
UserId: resp.GetUserId(),
})
return err
},
},
want: &user.ResendInviteCodeResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.DefaultOrg.Id,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.ResendInviteCode(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_VerifyInviteCode(t *testing.T) {
type args struct {
ctx context.Context
req *user.VerifyInviteCodeRequest
prepare func(request *user.VerifyInviteCodeRequest) error
}
tests := []struct {
name string
args args
want *user.VerifyInviteCodeResponse
wantErr bool
}{
{
name: "user not existing",
args: args{
CTX,
&user.VerifyInviteCodeRequest{
UserId: "notexisting",
},
func(request *user.VerifyInviteCodeRequest) error { return nil },
},
wantErr: true,
},
{
name: "code not existing",
args: args{
ctx: CTX,
req: &user.VerifyInviteCodeRequest{},
prepare: func(request *user.VerifyInviteCodeRequest) error {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
return nil
},
},
wantErr: true,
},
{
name: "invalid code",
args: args{
ctx: CTX,
req: &user.VerifyInviteCodeRequest{
VerificationCode: "invalid",
},
prepare: func(request *user.VerifyInviteCodeRequest) error {
userResp := Instance.CreateHumanUser(CTX)
request.UserId = userResp.GetUserId()
Instance.CreateInviteCode(CTX, userResp.GetUserId())
return nil
},
},
wantErr: true,
},
{
name: "verify, ok",
args: args{
ctx: CTX,
req: &user.VerifyInviteCodeRequest{},
prepare: func(request *user.VerifyInviteCodeRequest) error {
userResp := Instance.CreateHumanUser(CTX)
request.UserId = userResp.GetUserId()
codeResp := Instance.CreateInviteCode(CTX, userResp.GetUserId())
request.VerificationCode = codeResp.GetInviteCode()
return nil
},
},
want: &user.VerifyInviteCodeResponse{
Details: &object.Details{
ChangeDate: timestamppb.Now(),
ResourceOwner: Instance.DefaultOrg.Id,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.prepare(tt.args.req)
require.NoError(t, err)
got, err := Client.VerifyInviteCode(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -45,14 +45,6 @@ func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
if username == "" {
username = req.GetEmail().GetEmail()
}
var urlTemplate string
if req.GetEmail().GetSendCode() != nil {
urlTemplate = req.GetEmail().GetSendCode().GetUrlTemplate()
// test the template execution so the async notification will not fail because of it and the user won't realize
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil {
return nil, err
}
}
passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired()
metadata := make([]*command.AddMetadataEntry, len(req.Metadata))
for i, metadataEntry := range req.Metadata {
@ -69,6 +61,10 @@ func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
DisplayName: link.GetUserName(),
}
}
email, err := addUserRequestEmailToCommand(req.GetEmail())
if err != nil {
return nil, err
}
return &command.AddHuman{
ID: req.GetUserId(),
Username: username,
@ -76,12 +72,7 @@ func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
LastName: req.GetProfile().GetFamilyName(),
NickName: req.GetProfile().GetNickName(),
DisplayName: req.GetProfile().GetDisplayName(),
Email: command.Email{
Address: domain.EmailAddress(req.GetEmail().GetEmail()),
Verified: req.GetEmail().GetIsVerified(),
ReturnCode: req.GetEmail().GetReturnCode() != nil,
URLTemplate: urlTemplate,
},
Email: email,
Phone: command.Phone{
Number: domain.PhoneNumber(req.GetPhone().GetPhone()),
Verified: req.GetPhone().GetIsVerified(),
@ -100,6 +91,25 @@ func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
}, nil
}
func addUserRequestEmailToCommand(email *user.SetHumanEmail) (command.Email, error) {
address := domain.EmailAddress(email.GetEmail())
switch v := email.GetVerification().(type) {
case *user.SetHumanEmail_ReturnCode:
return command.Email{Address: address, ReturnCode: true}, nil
case *user.SetHumanEmail_SendCode:
urlTemplate := v.SendCode.GetUrlTemplate()
// test the template execution so the async notification will not fail because of it and the user won't realize
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, "userID", "code", "orgID"); err != nil {
return command.Email{}, err
}
return command.Email{Address: address, URLTemplate: urlTemplate}, nil
case *user.SetHumanEmail_IsVerified:
return command.Email{Address: address, Verified: v.IsVerified, NoEmailVerification: true}, nil
default:
return command.Email{Address: address}, nil
}
}
func genderToDomain(gender user.Gender) domain.Gender {
switch gender {
case user.Gender_GENDER_UNSPECIFIED:
@ -617,3 +627,54 @@ func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.Authenticatio
return user.AuthenticationMethodType_AUTHENTICATION_METHOD_TYPE_UNSPECIFIED
}
}
func (s *Server) CreateInviteCode(ctx context.Context, req *user.CreateInviteCodeRequest) (*user.CreateInviteCodeResponse, error) {
invite, err := createInviteCodeRequestToCommand(req)
if err != nil {
return nil, err
}
details, code, err := s.command.CreateInviteCode(ctx, invite)
if err != nil {
return nil, err
}
return &user.CreateInviteCodeResponse{
Details: object.DomainToDetailsPb(details),
InviteCode: code,
}, nil
}
func (s *Server) ResendInviteCode(ctx context.Context, req *user.ResendInviteCodeRequest) (*user.ResendInviteCodeResponse, error) {
details, err := s.command.ResendInviteCode(ctx, req.GetUserId(), "", "")
if err != nil {
return nil, err
}
return &user.ResendInviteCodeResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func (s *Server) VerifyInviteCode(ctx context.Context, req *user.VerifyInviteCodeRequest) (*user.VerifyInviteCodeResponse, error) {
details, err := s.command.VerifyInviteCode(ctx, req.GetUserId(), req.GetVerificationCode())
if err != nil {
return nil, err
}
return &user.VerifyInviteCodeResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func createInviteCodeRequestToCommand(req *user.CreateInviteCodeRequest) (*command.CreateUserInvite, error) {
switch v := req.GetVerification().(type) {
case *user.CreateInviteCodeRequest_SendCode:
urlTemplate := v.SendCode.GetUrlTemplate()
// test the template execution so the async notification will not fail because of it and the user won't realize
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil {
return nil, err
}
return &command.CreateUserInvite{UserID: req.GetUserId(), URLTemplate: urlTemplate, ApplicationName: v.SendCode.GetApplicationName()}, nil
case *user.CreateInviteCodeRequest_ReturnCode:
return &command.CreateUserInvite{UserID: req.GetUserId(), ReturnCode: true}, nil
default:
return &command.CreateUserInvite{UserID: req.GetUserId()}, nil
}
}

View File

@ -0,0 +1,154 @@
package login
import (
"net/http"
"net/url"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
queryInviteUserCode = "code"
queryInviteUserUserID = "userID"
queryInviteUserLoginName = "loginname"
tmplInviteUser = "inviteuser"
)
type inviteUserFormData struct {
Code string `schema:"code"`
LoginName string `schema:"loginname"`
Password string `schema:"password"`
PasswordConfirm string `schema:"passwordconfirm"`
UserID string `schema:"userID"`
OrgID string `schema:"orgID"`
Resend bool `schema:"resend"`
}
type inviteUserData struct {
baseData
profileData
Code string
LoginName string
UserID string
MinLength uint64
HasUppercase string
HasLowercase string
HasNumber string
HasSymbol string
}
func InviteUserLink(origin, userID, loginName, code, orgID string, authRequestID string) string {
v := url.Values{}
v.Set(queryInviteUserUserID, userID)
v.Set(queryInviteUserLoginName, loginName)
v.Set(queryInviteUserCode, code)
v.Set(queryOrgID, orgID)
v.Set(QueryAuthRequestID, authRequestID)
return externalLink(origin) + EndpointInviteUser + "?" + v.Encode()
}
func (l *Login) handleInviteUser(w http.ResponseWriter, r *http.Request) {
authReq := l.checkOptionalAuthRequestOfEmailLinks(r)
userID := r.FormValue(queryInviteUserUserID)
orgID := r.FormValue(queryOrgID)
code := r.FormValue(queryInviteUserCode)
loginName := r.FormValue(queryInviteUserLoginName)
l.renderInviteUser(w, r, authReq, userID, orgID, loginName, code, nil)
}
func (l *Login) handleInviteUserCheck(w http.ResponseWriter, r *http.Request) {
data := new(inviteUserFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
if data.Resend {
l.resendUserInvite(w, r, authReq, data.UserID, data.OrgID, data.LoginName)
return
}
l.checkUserInviteCode(w, r, authReq, data)
}
func (l *Login) checkUserInviteCode(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *inviteUserFormData) {
if data.Password != data.PasswordConfirm {
err := zerrors.ThrowInvalidArgument(nil, "VIEW-KJS3h", "Errors.User.Password.ConfirmationWrong")
l.renderInviteUser(w, r, authReq, data.UserID, data.OrgID, data.LoginName, data.Code, err)
return
}
userOrgID := ""
if authReq != nil {
userOrgID = authReq.UserOrgID
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err := l.command.VerifyInviteCodeSetPassword(setUserContext(r.Context(), data.UserID, userOrgID), data.UserID, data.Code, data.Password, userAgentID)
if err != nil {
l.renderInviteUser(w, r, authReq, data.UserID, data.OrgID, data.LoginName, "", err)
return
}
if authReq == nil {
l.defaultRedirect(w, r)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) resendUserInvite(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, orgID, loginName string) {
var userOrgID, authRequestID string
if authReq != nil {
userOrgID = authReq.UserOrgID
authRequestID = authReq.ID
}
_, err := l.command.ResendInviteCode(setUserContext(r.Context(), userID, userOrgID), userID, userOrgID, authRequestID)
l.renderInviteUser(w, r, authReq, userID, orgID, loginName, "", err)
}
func (l *Login) renderInviteUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, orgID, loginName string, code string, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if authReq != nil {
userID = authReq.UserID
orgID = authReq.UserOrgID
}
translator := l.getTranslator(r.Context(), authReq)
data := inviteUserData{
baseData: l.getBaseData(r, authReq, translator, "InviteUser.Title", "InviteUser.Description", errID, errMessage),
profileData: l.getProfileData(authReq),
UserID: userID,
Code: code,
}
// if the user clicked on the link in the mail, we need to make sure the loginName is rendered
if authReq == nil {
data.LoginName = loginName
data.UserName = loginName
}
policy := l.getPasswordComplexityPolicyByUserID(r, userID)
if policy != nil {
data.MinLength = policy.MinLength
if policy.HasUppercase {
data.HasUppercase = UpperCaseRegex
}
if policy.HasLowercase {
data.HasLowercase = LowerCaseRegex
}
if policy.HasSymbol {
data.HasSymbol = SymbolRegex
}
if policy.HasNumber {
data.HasNumber = NumberRegex
}
}
if authReq == nil {
if err == nil {
l.customTexts(r.Context(), translator, orgID)
}
}
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplInviteUser], data, nil)
}

View File

@ -68,6 +68,7 @@ func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName
tmplInitPasswordDone: "init_password_done.html",
tmplInitUser: "init_user.html",
tmplInitUserDone: "init_user_done.html",
tmplInviteUser: "invite_user.html",
tmplPasswordResetDone: "password_reset_done.html",
tmplChangePassword: "change_password.html",
tmplChangePasswordDone: "change_password_done.html",
@ -193,6 +194,9 @@ func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName
"initUserUrl": func() string {
return path.Join(r.pathPrefix, EndpointInitUser)
},
"inviteUserUrl": func() string {
return path.Join(r.pathPrefix, EndpointInviteUser)
},
"changePasswordUrl": func() string {
return path.Join(r.pathPrefix, EndpointChangePassword)
},
@ -329,6 +333,8 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
l.renderInternalError(w, r, authReq, zerrors.ThrowPreconditionFailed(nil, "APP-asb43", "Errors.User.GrantRequired"))
case *domain.ProjectRequiredStep:
l.renderInternalError(w, r, authReq, zerrors.ThrowPreconditionFailed(nil, "APP-m92d", "Errors.User.ProjectRequired"))
case *domain.VerifyInviteStep:
l.renderInviteUser(w, r, authReq, "", "", "", "", nil)
default:
l.renderInternalError(w, r, authReq, zerrors.ThrowInternal(nil, "APP-ds3QF", "step no possible"))
}

View File

@ -30,6 +30,7 @@ const (
EndpointChangePassword = "/password/change"
EndpointPasswordReset = "/password/reset"
EndpointInitUser = "/user/init"
EndpointInviteUser = "/user/invite"
EndpointMFAVerify = "/mfa/verify"
EndpointMFAPrompt = "/mfa/prompt"
EndpointMFAInitVerify = "/mfa/init/verify"
@ -94,6 +95,8 @@ func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router
router.HandleFunc(EndpointPasswordReset, login.handlePasswordReset).Methods(http.MethodGet)
router.HandleFunc(EndpointInitUser, login.handleInitUser).Methods(http.MethodGet)
router.HandleFunc(EndpointInitUser, login.handleInitUserCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointInviteUser, login.handleInviteUser).Methods(http.MethodGet)
router.HandleFunc(EndpointInviteUser, login.handleInviteUserCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAVerify, login.handleMFAVerify).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAPrompt, login.handleMFAPromptSelection).Methods(http.MethodGet)
router.HandleFunc(EndpointMFAPrompt, login.handleMFAPrompt).Methods(http.MethodPost)

View File

@ -81,6 +81,14 @@ InitUserDone:
Description: Имейлът е потвърден и паролата е успешно зададена
NextButtonText: следващия
CancelButtonText: анулиране
InviteUser:
Title: Активиране на потребителя
Description: Проверете своя имейл с кода по-долу и задайте паролата си.
CodeLabel: Код
NewPasswordLabel: Нова парола
NewPasswordConfirm: Потвърди парола
NextButtonText: Напред
ResendButtonText: Изпрати отново код
InitMFAPrompt:
Title: 2-факторна настройка
Description: >-

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: Další
CancelButtonText: Zrušit
InviteUser:
Title: Aktivace uživatele
Description: Ověřte svůj e-mail pomocí níže uvedeného kódu a nastavte si heslo.
CodeLabel: Kód
NewPasswordLabel: Nové heslo
NewPasswordConfirm: Potvrďte heslo
NextButtonText: Další
ResendButtonText: Odeslat kód znovu
InitMFAPrompt:
Title: Nastavení 2-faktorové autentizace
Description: 2-faktorová autentizace vám poskytuje další zabezpečení pro váš uživatelský účet. Tím je zajištěno, že k vašemu účtu máte přístup pouze vy.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: Weiter
CancelButtonText: Abbrechen
InviteUser:
Title: Benutzer aktivieren
Description: Bestätige deine E-Mail-Adresse mit dem unten stehenden Code und lege dein Passwort fest.
CodeLabel: Code
NewPasswordLabel: Neues Passwort
NewPasswordConfirm: Passwort bestätigen
NextButtonText: Weiter
ResendButtonText: Code erneut senden
InitMFAPrompt:
Title: Zweitfaktor hinzufügen
Description: Die Zwei-Faktor-Authentifizierung gibt dir eine zusätzliche Sicherheit für dein Benutzerkonto. Damit stellst du sicher, dass nur du Zugriff auf dein Konto hast.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: Next
CancelButtonText: Cancel
InviteUser:
Title: Activate User
Description: Verify your e-mail with the code below and set your password.
CodeLabel: Code
NewPasswordLabel: New Password
NewPasswordConfirm: Confirm Password
NextButtonText: Next
ResendButtonText: Resend Code
InitMFAPrompt:
Title: 2-Factor Setup
Description: 2-factor authentication gives you an additional security for your user account. This ensures that only you have access to your account.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: siguiente
CancelButtonText: cancelar
InviteUser:
Title: Activar usuario
Description: Verifica tu email con el siguiente código y establece tu contraseña.
CodeLabel: Código
NewPasswordLabel: Nueva contraseña
NewPasswordConfirm: Confirmar contraseña
NextButtonText: siguiente
ResendButtonText: reenviar código
InitMFAPrompt:
Title: Configuración de doble factor
Description: La autenticación de doble factor te proporciona seguridad adicional para tu cuenta de usuario. Ésta asegura que solo tú tienes acceso a tu cuenta.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: Suivant
CancelButtonText: Annuler
InviteUser:
Title: Activer l'utilisateur
Description: Vérifiez votre e-mail avec le code ci-dessous et définissez votre mot de passe.
CodeLabel: Code
NewPasswordLabel: Nouveau mot de passe
NewPasswordConfirm: Confirmer le mot de passe
NextButtonText: Suivant
ResendButtonText: Renvoyer le code
InitMFAPrompt:
Title: Configuration authentification à 2 facteurs
Description: L'authentification authentification à 2 facteurs vous offre une sécurité supplémentaire pour votre compte d'utilisateur. Vous êtes ainsi assuré d'être le seul à avoir accès à votre compte.

View File

@ -76,6 +76,14 @@ InitUserDone:
Description: Email terverifikasi dan Kata Sandi berhasil ditetapkan
NextButtonText: Berikutnya
CancelButtonText: Membatalkan
InviteUser:
Title: Aktifkan Pengguna
Description: Verifikasi email Anda dengan kode di bawah ini dan atur kata sandi Anda.
CodeLabel: Kode
NewPasswordLabel: Kata Sandi Baru
NewPasswordConfirm: Konfirmasi Kata Sandi
NextButtonText: Selanjutnya
ResendButtonText: Kirim Ulang Kode
InitMFAPrompt:
Title: Pengaturan 2 Faktor
Description: Otentikasi 2 faktor memberi Anda keamanan tambahan untuk akun pengguna Anda.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: Avanti
CancelButtonText: annulla
InviteUser:
Title: Attiva utente
Description: Verifica la tua email con il codice seguente e imposta la tua password.
CodeLabel: Codice
NewPasswordLabel: Nuova password
NewPasswordConfirm: Conferma password
NextButtonText: Avanti
ResendButtonText: Reinvia codice
InitMFAPrompt:
Title: Impostazione a 2 fattori
Description: L'autenticazione a due fattori offre un'ulteriore sicurezza al vostro account utente. Questo garantisce che solo voi possiate accedere al vostro account.

View File

@ -79,6 +79,15 @@ InitUserDone:
NextButtonText: 次へ
CancelButtonText: キャンセル
InviteUser:
Title: ユーザーの有効化
Description: 下のコードでメールアドレスを確認し、パスワードを設定してください。
CodeLabel: コード
NewPasswordLabel: 新しいパスワード
NewPasswordConfirm: パスワードの確認
NextButtonText: 次へ
ResendButtonText: コードを再送信
InitMFAPrompt:
Title: 二要素認証のセットアップ
Description: 二要素認証でアカウントのセキュリティを強化します。

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: следно
CancelButtonText: откажи
InviteUser:
Title: Активирање на корисникот
Description: Проверете го вашиот имејл со кодот подолу и поставете ја вашата лозинка.
CodeLabel: Код
NewPasswordLabel: Нова лозинка
NewPasswordConfirm: Потврди лозинка
NextButtonText: Следно
ResendButtonText: Повторно испрати код
InitMFAPrompt:
Title: Подесување на 2-факторска автентикација
Description: 2-факторската автентикација ви дава дополнителна безбедност за вашата корисничка сметка. Ова обезбедува само вие да имате пристап до вашата сметка.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: Volgende
CancelButtonText: Annuleren
InviteUser:
Title: Gebruiker activeren
Description: Verifieer uw e-mail met de onderstaande code en stel uw wachtwoord in.
CodeLabel: Code
NewPasswordLabel: Nieuw wachtwoord
NewPasswordConfirm: Wachtwoord bevestigen
NextButtonText: Volgende
ResendButtonText: Code opnieuw verzenden
InitMFAPrompt:
Title: 2-Factor Setup
Description: 2-factor authenticatie geeft u extra beveiliging voor uw gebruikersaccount. Hierdoor bent u de enige die toegang heeft tot uw account.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: dalej
CancelButtonText: anuluj
InviteUser:
Title: Aktywuj użytkownika
Description: Zweryfikuj swój adres e-mail za pomocą poniższego kodu i ustaw swoje hasło.
CodeLabel: Kod
NewPasswordLabel: Nowe hasło
NewPasswordConfirm: Potwierdź hasło
NextButtonText: Dalej
ResendButtonText: Wyślij ponownie kod
InitMFAPrompt:
Title: Konfiguracja 2-etapowego uwierzytelniania
Description: 2-etapowe uwierzytelnianie daje Ci dodatkową ochronę dla Twojego konta użytkownika. Dzięki temu masz pewność, że tylko Ty masz dostęp do swojego konta.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: próximo
CancelButtonText: cancelar
InviteUser:
Title: Ativar usuário
Description: Verifique seu e-mail com o código abaixo e defina sua senha.
CodeLabel: Código
NewPasswordLabel: Nova senha
NewPasswordConfirm: Confirmar senha
NextButtonText: Próximo
ResendButtonText: Reenviar código
InitMFAPrompt:
Title: Configuração de 2 fatores
Description: A autenticação de 2 fatores fornece uma segurança adicional para sua conta de usuário. Isso garante que apenas você tenha acesso à sua conta.

View File

@ -85,6 +85,15 @@ InitUserDone:
NextButtonText: далее
CancelButtonText: отмена
InviteUser:
Title: Активировать пользователя
Description: Проверьте свой адрес электронной почты с помощью кода ниже и установите свой пароль.
CodeLabel: Код
NewPasswordLabel: Новый пароль
NewPasswordConfirm: Подтвердить пароль
NextButtonText: Далее
ResendButtonText: Отправить код повторно
InitMFAPrompt:
Title: Установка двухфакторной аутентификации
Description: Двухфакторная аутентификация обеспечивает дополнительную защиту вашей учётной записи.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: Fortsätt
CancelButtonText: Avbryt
InviteUser:
Title: Aktivera användare
Description: Verifiera din e-post med koden nedan och sätt ditt lösenord.
CodeLabel: Kod
NewPasswordLabel: Nytt lösenord
NewPasswordConfirm: Bekräfta lösenord
NextButtonText: Nästa
ResendButtonText: Skicka koden igen
InitMFAPrompt:
Title: tvåfaktorinställningar
Description: 2-factor-identifiering ökar säkerheten för ditt konto. Enbart du som har tillgång till enheten kan logga in.

View File

@ -86,6 +86,15 @@ InitUserDone:
NextButtonText: 继续
CancelButtonText: 取消
InviteUser:
Title: 激活用户
Description: 使用以下代码验证您的电子邮件并设置您的密码。
CodeLabel: 代码
NewPasswordLabel: 新密码
NewPasswordConfirm: 确认密码
NextButtonText: 下一步
ResendButtonText: 重新发送代码
InitMFAPrompt:
Title: 两步验证设置
Description: 两步验证为您的账户提供了额外的安全保障。这确保只有你能访问你的账户。

View File

@ -0,0 +1,63 @@
{{template "main-top" .}}
<div class="lgn-head">
<h1>{{t "InviteUser.Title"}}</h1>
{{ template "user-profile" . }}
<p>{{t "InviteUser.Description"}}</p>
</div>
<form action="{{ inviteUserUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" />
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
<input type="text" name="loginName" value="{{if .DisplayLoginNameSuffix}}{{.LoginName}}{{else}}{{.UserName}}{{end}}" autocomplete="username" class="hidden" />
<div class="fields">
<div class="field">
<label class="lgn-label" for="code">{{t "InviteUser.CodeLabel"}}</label>
<input class="lgn-input" {{if .ErrMessage}}shake {{end}} type="text" id="code" name="code" value="{{.Code}}" autocomplete="one-time-code" autofocus
required>
</div>
<div class="field">
<label class="lgn-label" for="password">{{t "InviteUser.NewPasswordLabel"}}</label>
<input data-minlength="{{ .MinLength }}" data-has-uppercase="{{ .HasUppercase }}"
data-has-lowercase="{{ .HasLowercase }}" data-has-number="{{ .HasNumber }}"
data-has-symbol="{{ .HasSymbol }}" class="lgn-input" type="password" id="password" name="password"
autocomplete="new-password" autofocus required>
</div>
<div class="field">
<label class="lgn-label" for="passwordconfirm">{{t "InviteUser.NewPasswordConfirm"}}</label>
<input class="lgn-input" type="password" id="passwordconfirm" name="passwordconfirm"
autocomplete="new-password" autofocus required>
{{ template "password-complexity-policy-description" . }}
</div>
</div>
{{ template "error-message" .}}
<div class="lgn-actions lgn-reverse-order">
<!-- position element in header -->
<a class="lgn-icon-button lgn-left-action" href="{{ loginUrl }}">
<i class="lgn-icon-arrow-left-solid"></i>
</a>
<button type="submit" id="init-button" name="resend" value="false"
class="lgn-primary lgn-raised-button">{{t "InviteUser.NextButtonText"}}</button>
<span class="fill-space"></span>
<button type="submit" name="resend" value="true" class="lgn-stroked-button" formnovalidate>{{t "InviteUser.ResendButtonText"}}</button>
</div>
</form>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/password_policy_check.js" }}"></script>
<script src="{{ resourceUrl "scripts/init_password_check.js" }}"></script>
{{template "main-bottom" .}}

View File

@ -106,6 +106,7 @@ type idpUserLinksProvider interface {
type userEventProvider interface {
UserEventsByID(ctx context.Context, id string, changeDate time.Time, eventTypes []eventstore.EventType) ([]eventstore.Event, error)
PasswordCodeExists(ctx context.Context, userID string) (exists bool, err error)
InviteCodeExists(ctx context.Context, userID string) (exists bool, err error)
}
type userCommandProvider interface {
@ -1254,8 +1255,18 @@ func (repo *AuthRequestRepo) firstFactorChecked(ctx context.Context, request *do
if user.PasswordInitRequired {
if !user.IsEmailVerified {
// If the user was created through the user resource API,
// they can either have an invite code...
exists, err := repo.UserEventProvider.InviteCodeExists(ctx, user.ID)
logging.WithFields("userID", user.ID).OnError(err).Error("unable to check if invite code exists")
if err == nil && exists {
return &domain.VerifyInviteStep{}
}
// or were created with an explicit email verification mail
return &domain.VerifyEMailStep{InitPassword: true}
}
// If they were created with a verified mail, they might have never received mail to set their password,
// e.g. when created through a user resource API. In this case we'll just create and send one now.
exists, err := repo.UserEventProvider.PasswordCodeExists(ctx, user.ID)
logging.WithFields("userID", user.ID).OnError(err).Error("unable to check if password code exists")
if err == nil && !exists {

View File

@ -110,8 +110,9 @@ func (m *mockViewNoUser) UserByID(context.Context, string, string) (*user_view_m
}
type mockEventUser struct {
Events []eventstore.Event
CodeExists bool
Events []eventstore.Event
PwCodeExists bool
InvitationCodeExists bool
}
func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, changeDate time.Time, types []eventstore.EventType) ([]eventstore.Event, error) {
@ -119,7 +120,11 @@ func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, changeDat
}
func (m *mockEventUser) PasswordCodeExists(ctx context.Context, userID string) (bool, error) {
return m.CodeExists, nil
return m.PwCodeExists, nil
}
func (m *mockEventUser) InviteCodeExists(ctx context.Context, userID string) (bool, error) {
return m.InvitationCodeExists, nil
}
func (m *mockEventUser) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*query.CurrentState, error) {
@ -140,6 +145,10 @@ func (m *mockEventErrUser) PasswordCodeExists(ctx context.Context, userID string
return false, zerrors.ThrowInternal(nil, "id", "internal error")
}
func (m *mockEventErrUser) InviteCodeExists(ctx context.Context, userID string) (bool, error) {
return false, zerrors.ThrowInternal(nil, "id", "internal error")
}
type mockViewUser struct {
InitRequired bool
PasswordInitRequired bool
@ -1019,6 +1028,36 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
[]domain.NextStep{&domain.VerifyEMailStep{}},
nil,
},
{
"password not set (email not verified), invite code exists, invite step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
PasswordInitRequired: true,
},
userEventProvider: &mockEventUser{
InvitationCodeExists: true,
},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{
ShowFailures: true,
},
},
orgViewProvider: &mockViewOrg{State: domain.OrgStateActive},
idpUserLinksProvider: &mockIDPUserLinks{},
},
args{
&domain.AuthRequest{
UserID: "UserID",
LoginPolicy: &domain.LoginPolicy{
AllowUsernamePassword: true,
},
},
false,
},
[]domain.NextStep{&domain.VerifyInviteStep{}},
nil,
},
{
"password not set (email not verified), init password step",
fields{
@ -1056,7 +1095,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
IsEmailVerified: true,
},
userEventProvider: &mockEventUser{
CodeExists: true,
PwCodeExists: true,
},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{
@ -1088,7 +1127,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
IsEmailVerified: true,
},
userEventProvider: &mockEventUser{
CodeExists: false,
PwCodeExists: false,
},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{

View File

@ -93,3 +93,41 @@ func (repo *UserRepo) PasswordCodeExists(ctx context.Context, userID string) (ex
}
return model.exists, nil
}
type inviteCodeCheck struct {
userID string
exists bool
events int
}
func (p *inviteCodeCheck) Reduce() error {
p.exists = p.events > 0
return nil
}
func (p *inviteCodeCheck) AppendEvents(events ...eventstore.Event) {
p.events += len(events)
}
func (p *inviteCodeCheck) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(p.userID).
EventTypes(
user.HumanInviteCodeAddedType,
user.HumanInviteCodeSentType).
Builder()
}
func (repo *UserRepo) InviteCodeExists(ctx context.Context, userID string) (exists bool, err error) {
model := &inviteCodeCheck{
userID: userID,
}
err = repo.Eventstore.FilterToQueryReducer(ctx, model)
if err != nil {
return false, zerrors.ThrowPermissionDenied(err, "EVENT-GJ2os", "Errors.Internal")
}
return model.exists, nil
}

View File

@ -41,14 +41,6 @@ func newEncryptedCodeWithDefaultConfig(ctx context.Context, filter preparation.F
}, nil
}
func verifyEncryptedCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, creation time.Time, expiry time.Duration, crypted *crypto.CryptoValue, plain string) error {
gen, _, err := encryptedCodeGenerator(ctx, filter, typ, alg, emptyConfig)
if err != nil {
return err
}
return crypto.VerifyCode(creation, expiry, crypted, plain, gen.Alg())
}
func encryptedCodeGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.EncryptionAlgorithm, defaultConfig *crypto.GeneratorConfig) (crypto.Generator, *crypto.GeneratorConfig, error) {
config, err := cryptoGeneratorConfigWithDefault(ctx, filter, typ, defaultConfig)
if err != nil {

View File

@ -123,78 +123,6 @@ func Test_newCryptoCode(t *testing.T) {
}
}
func Test_verifyCryptoCode(t *testing.T) {
es := eventstoreExpect(t, expectFilter(
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
))
code, err := newEncryptedCode(context.Background(), es.Filter, domain.SecretGeneratorTypeVerifyEmailCode, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) //nolint:staticcheck
require.NoError(t, err)
type args struct {
typ domain.SecretGeneratorType
alg crypto.EncryptionAlgorithm
expiry time.Duration
crypted *crypto.CryptoValue
plain string
}
tests := []struct {
name string
eventsore *eventstore.Eventstore
args args
wantErr bool
}{
{
name: "filter config error",
eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
expiry: code.Expiry,
crypted: code.Crypted,
plain: code.Plain,
},
wantErr: true,
},
{
name: "success",
eventsore: eventstoreExpect(t, expectFilter(
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
)),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
expiry: code.Expiry,
crypted: code.Crypted,
plain: code.Plain,
},
},
{
name: "wrong plain",
eventsore: eventstoreExpect(t, expectFilter(
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
)),
args: args{
typ: domain.SecretGeneratorTypeVerifyEmailCode,
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
expiry: code.Expiry,
crypted: code.Crypted,
plain: "wrong",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := verifyEncryptedCode(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg, time.Now(), tt.args.expiry, tt.args.crypted, tt.args.plain) //nolint:staticcheck
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func Test_cryptoCodeGenerator(t *testing.T) {
type args struct {
typ domain.SecretGeneratorType

View File

@ -12,6 +12,9 @@ type Email struct {
Address domain.EmailAddress
Verified bool
// NoEmailVerification is used Verified field is false
NoEmailVerification bool
// ReturnCode is used if the Verified field is false
ReturnCode bool

View File

@ -145,6 +145,7 @@ type SecretGenerators struct {
DomainVerification *crypto.GeneratorConfig
OTPSMS *crypto.GeneratorConfig
OTPEmail *crypto.GeneratorConfig
InviteCode *crypto.GeneratorConfig
}
type ZitadelConfig struct {

View File

@ -388,6 +388,10 @@ func (c *Commands) newUserInitCode(ctx context.Context, filter preparation.Filte
return c.newEncryptedCode(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
}
func (c *Commands) newUserInviteCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*EncryptedCode, error) {
return c.newEncryptedCodeWithDefault(ctx, filter, domain.SecretGeneratorTypeInviteCode, alg, c.defaultSecretGenerators.InviteCode)
}
func userWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceOwner string) (*UserWriteModel, error) {
user := NewUserWriteModel(userID, resourceOwner)
events, err := filter(ctx, user.Query())

View File

@ -284,6 +284,9 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
}
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry, human.AuthRequestID)), nil
}
if human.Email.NoEmailVerification {
return cmds, nil
}
if !human.Email.Verified {
emailCode, err := c.newEmailCode(ctx, filter, codeAlg)
if err != nil {

View File

@ -626,6 +626,67 @@ func TestCommandSide_AddHuman(t *testing.T) {
wantEmailCode: "emailCode",
},
},
{
name: "add human (with password and unverified email), ok (no email code)",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
true,
true,
true,
),
),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectPush(
newAddHumanEvent("$plain$x$password", false, true, "", AllowedLanguage),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
userPasswordHasher: mockPasswordHasher("x"),
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
newCode: mockEncryptedCode("emailCode", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
Password: "password",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@test.ch",
Verified: false,
NoEmailVerification: true,
},
PreferredLanguage: AllowedLanguage,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: false,
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
name: "add human email verified, ok",
fields: fields{

View File

@ -1,6 +1,8 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
@ -122,6 +124,10 @@ func UserAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregat
return eventstore.AggregateFromWriteModel(wm, user.AggregateType, user.AggregateVersion)
}
func UserAggregateFromWriteModelCtx(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate {
return eventstore.AggregateFromWriteModelCtx(ctx, wm, user.AggregateType, user.AggregateVersion)
}
func isUserStateExists(state domain.UserState) bool {
return !hasUserState(state, domain.UserStateDeleted, domain.UserStateUnspecified)
}

View File

@ -0,0 +1,193 @@
package command
import (
"context"
"strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type CreateUserInvite struct {
UserID string
URLTemplate string
ReturnCode bool
ApplicationName string
}
func (c *Commands) CreateInviteCode(ctx context.Context, invite *CreateUserInvite) (details *domain.ObjectDetails, returnCode *string, err error) {
invite.UserID = strings.TrimSpace(invite.UserID)
if invite.UserID == "" {
return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4jio3", "Errors.User.UserIDMissing")
}
wm, err := c.userInviteCodeWriteModel(ctx, invite.UserID, "")
if err != nil {
return nil, nil, err
}
if err := c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, wm.AggregateID); err != nil {
return nil, nil, err
}
if !wm.UserState.Exists() {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound")
}
if !wm.CreationAllowed() {
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-EF34g", "Errors.User.AlreadyInitialised")
}
code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint
if err != nil {
return nil, nil, err
}
err = c.pushAppendAndReduce(ctx, wm, user.NewHumanInviteCodeAddedEvent(
ctx,
UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel),
code.Crypted,
code.Expiry,
invite.URLTemplate,
invite.ReturnCode,
invite.ApplicationName,
"",
))
if err != nil {
return nil, nil, err
}
if invite.ReturnCode {
returnCode = &code.Plain
}
return writeModelToObjectDetails(&wm.WriteModel), returnCode, nil
}
// ResendInviteCode resends the invite mail with a new code and an optional authRequestID.
// It will reuse the applicationName from the previous code.
func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner, authRequestID string) (objectDetails *domain.ObjectDetails, err error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2n8vs", "Errors.User.UserIDMissing")
}
existingCode, err := c.userInviteCodeWriteModel(ctx, userID, resourceOwner)
if err != nil {
return nil, err
}
if authz.GetCtxData(ctx).UserID != userID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingCode.ResourceOwner, userID); err != nil {
return nil, err
}
}
if !existingCode.UserState.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound")
}
if !existingCode.CreationAllowed() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Gg42s", "Errors.User.AlreadyInitialised")
}
if existingCode.InviteCode == nil || existingCode.CodeReturned {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound")
}
code, err := c.newUserInviteCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint
if err != nil {
return nil, err
}
if authRequestID == "" {
authRequestID = existingCode.AuthRequestID
}
err = c.pushAppendAndReduce(ctx, existingCode,
user.NewHumanInviteCodeAddedEvent(
ctx,
UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel),
code.Crypted,
code.Expiry,
existingCode.URLTemplate,
false,
existingCode.ApplicationName,
authRequestID,
))
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingCode.WriteModel), nil
}
func (c *Commands) InviteCodeSent(ctx context.Context, userID, orgID string) (err error) {
if userID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-Sgf31", "Errors.User.UserIDMissing")
}
existingCode, err := c.userInviteCodeWriteModel(ctx, userID, orgID)
if err != nil {
return err
}
if !existingCode.UserState.Exists() {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-HN34a", "Errors.User.NotFound")
}
if existingCode.InviteCode == nil || existingCode.CodeReturned {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wr3gq", "Errors.User.Code.NotFound")
}
userAgg := UserAggregateFromWriteModelCtx(ctx, &existingCode.WriteModel)
_, err = c.eventstore.Push(ctx, user.NewHumanInviteCodeSentEvent(ctx, userAgg))
return err
}
func (c *Commands) VerifyInviteCode(ctx context.Context, userID, code string) (details *domain.ObjectDetails, err error) {
return c.VerifyInviteCodeSetPassword(ctx, userID, code, "", "")
}
func (c *Commands) VerifyInviteCodeSetPassword(ctx context.Context, userID, code, password, userAgentID string) (details *domain.ObjectDetails, err error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Gk3f2", "Errors.User.UserIDMissing")
}
wm, err := c.userInviteCodeWriteModel(ctx, userID, "")
if err != nil {
return nil, err
}
if !wm.UserState.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-F5g2h", "Errors.User.NotFound")
}
userAgg := UserAggregateFromWriteModelCtx(ctx, &wm.WriteModel)
err = crypto.VerifyCode(wm.InviteCodeCreationDate, wm.InviteCodeExpiry, wm.InviteCode, code, c.userEncryption)
if err != nil {
_, err = c.eventstore.Push(ctx, user.NewHumanInviteCheckFailedEvent(ctx, userAgg))
logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanInviteCheckFailedEvent push failed")
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Wgn4q", "Errors.User.Code.Invalid")
}
commands := []eventstore.Command{
user.NewHumanInviteCheckSucceededEvent(ctx, userAgg),
user.NewHumanEmailVerifiedEvent(ctx, userAgg),
}
if password != "" {
passwordCommand, err := c.setPasswordCommand(
ctx,
userAgg,
wm.UserState,
password,
"",
userAgentID,
false,
nil,
)
if err != nil {
return nil, err
}
commands = append(commands, passwordCommand)
}
err = c.pushAppendAndReduce(ctx, wm, commands...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&wm.WriteModel), nil
}
func (c *Commands) userInviteCodeWriteModel(ctx context.Context, userID, orgID string) (writeModel *UserV2InviteWriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
writeModel = newUserV2InviteWriteModel(userID, orgID)
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
}
return writeModel, nil
}

View File

@ -0,0 +1,141 @@
package command
import (
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
)
type UserV2InviteWriteModel struct {
eventstore.WriteModel
InviteCode *crypto.CryptoValue
InviteCodeCreationDate time.Time
InviteCodeExpiry time.Duration
InviteCheckFailureCount uint8
ApplicationName string
AuthRequestID string
URLTemplate string
CodeReturned bool
EmailVerified bool
AuthMethodSet bool
UserState domain.UserState
}
func (wm *UserV2InviteWriteModel) CreationAllowed() bool {
return !wm.EmailVerified && !wm.AuthMethodSet
}
func newUserV2InviteWriteModel(userID, orgID string) *UserV2InviteWriteModel {
return &UserV2InviteWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: userID,
ResourceOwner: orgID,
},
}
}
func (wm *UserV2InviteWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *user.HumanAddedEvent:
wm.UserState = domain.UserStateActive
wm.AuthMethodSet = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) != ""
wm.EmptyInviteCode()
wm.ApplicationName = ""
wm.AuthRequestID = ""
case *user.HumanRegisteredEvent:
wm.UserState = domain.UserStateActive
wm.AuthMethodSet = crypto.SecretOrEncodedHash(e.Secret, e.EncodedHash) != ""
wm.EmptyInviteCode()
wm.ApplicationName = ""
wm.AuthRequestID = ""
case *user.HumanInviteCodeAddedEvent:
wm.SetInviteCode(e.Code, e.Expiry, e.CreationDate())
wm.URLTemplate = e.URLTemplate
wm.CodeReturned = e.CodeReturned
wm.ApplicationName = e.ApplicationName
wm.AuthRequestID = e.AuthRequestID
case *user.HumanInviteCheckSucceededEvent:
wm.EmptyInviteCode()
case *user.HumanInviteCheckFailedEvent:
wm.InviteCheckFailureCount++
if wm.InviteCheckFailureCount >= 3 { //TODO: config?
wm.UserState = domain.UserStateDeleted
}
case *user.HumanEmailVerifiedEvent:
wm.EmailVerified = true
wm.EmptyInviteCode()
case *user.UserLockedEvent:
wm.UserState = domain.UserStateLocked
case *user.UserUnlockedEvent:
wm.UserState = domain.UserStateActive
case *user.UserDeactivatedEvent:
wm.UserState = domain.UserStateInactive
case *user.UserReactivatedEvent:
wm.UserState = domain.UserStateActive
case *user.UserRemovedEvent:
wm.UserState = domain.UserStateDeleted
case *user.HumanPasswordChangedEvent:
wm.AuthMethodSet = true
case *user.UserIDPLinkAddedEvent:
wm.AuthMethodSet = true
case *user.HumanPasswordlessVerifiedEvent:
wm.AuthMethodSet = true
}
}
return wm.WriteModel.Reduce()
}
func (wm *UserV2InviteWriteModel) SetInviteCode(code *crypto.CryptoValue, expiry time.Duration, creationDate time.Time) {
wm.InviteCode = code
wm.InviteCodeExpiry = expiry
wm.InviteCodeCreationDate = creationDate
wm.InviteCheckFailureCount = 0
}
func (wm *UserV2InviteWriteModel) EmptyInviteCode() {
wm.InviteCode = nil
wm.InviteCodeExpiry = 0
wm.InviteCodeCreationDate = time.Time{}
wm.InviteCheckFailureCount = 0
}
func (wm *UserV2InviteWriteModel) Query() *eventstore.SearchQueryBuilder {
query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
user.UserV1AddedType,
user.HumanAddedType,
user.UserV1RegisteredType,
user.HumanRegisteredType,
user.HumanInviteCodeAddedType,
user.HumanInviteCheckSucceededType,
user.HumanInviteCheckFailedType,
user.UserV1EmailVerifiedType,
user.HumanEmailVerifiedType,
user.UserLockedType,
user.UserUnlockedType,
user.UserDeactivatedType,
user.UserReactivatedType,
user.UserRemovedType,
user.HumanPasswordChangedType,
user.UserV1PasswordChangedType,
user.UserIDPLinkAddedType,
user.HumanPasswordlessTokenVerifiedType,
).Builder()
if wm.ResourceOwner != "" {
query.ResourceOwner(wm.ResourceOwner)
}
return query
}
func (wm *UserV2InviteWriteModel) Aggregate() *user.Aggregate {
return user.NewAggregate(wm.AggregateID, wm.ResourceOwner)
}

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,7 @@ func (c *Commands) verifyUserPasskeyCode(ctx context.Context, userID, resourceOw
if err != nil {
return nil, err
}
err = verifyEncryptedCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg, wm.ChangeDate, wm.Expiration, wm.CryptoCode, code) //nolint:staticcheck
err = crypto.VerifyCode(wm.ChangeDate, wm.Expiration, wm.CryptoCode, code, alg)
if err != nil || wm.State != domain.PasswordlessInitCodeStateActive {
c.verifyUserPasskeyCodeFailed(ctx, wm)
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid")

View File

@ -143,7 +143,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
require.NoError(t, err)
userAgg := &user.NewAggregate("user1", "org1").Aggregate
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
idGenerator id.Generator
}
type args struct {
@ -163,7 +163,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
{
name: "code verification error",
fields: fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
@ -174,7 +174,6 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
),
),
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
expectPush(
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
),
@ -192,7 +191,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
{
name: "code verification ok, get human passwordless error",
fields: fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
@ -203,7 +202,6 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
),
),
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
expectFilterError(io.ErrClosedPipe),
),
},
@ -220,7 +218,7 @@ func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
webauthnConfig: webauthnConfig,
}
@ -242,7 +240,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
userAgg := &user.NewAggregate("user1", "org1").Aggregate
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
userID string
@ -260,7 +258,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
{
name: "filter error",
fields: fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe),
),
},
@ -274,7 +272,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
{
name: "code verification error",
fields: fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
@ -285,7 +283,6 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
),
),
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
expectPush(
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
),
@ -302,7 +299,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
{
name: "success",
fields: fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
@ -313,7 +310,6 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
),
),
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
),
},
args: args{
@ -328,7 +324,7 @@ func TestCommands_verifyUserPasskeyCode(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
}
got, err := c.verifyUserPasskeyCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.codeID, tt.args.code, alg)
require.ErrorIs(t, err, tt.wantErr)

View File

@ -17,6 +17,7 @@ const (
DomainClaimedMessageType = "DomainClaimed"
PasswordlessRegistrationMessageType = "PasswordlessRegistration"
PasswordChangeMessageType = "PasswordChange"
InviteUserMessageType = "InviteUser"
MessageTitle = "Title"
MessagePreHeader = "PreHeader"
MessageSubject = "Subject"
@ -26,16 +27,6 @@ const (
MessageFooterText = "Footer"
)
type MessageTexts struct {
InitCode CustomMessageText
PasswordReset CustomMessageText
VerifyEmail CustomMessageText
VerifyPhone CustomMessageText
DomainClaimed CustomMessageText
PasswordlessRegistration CustomMessageText
PasswordChange CustomMessageText
}
type CustomMessageText struct {
models.ObjectRoot
@ -71,5 +62,6 @@ func IsMessageTextType(textType string) bool {
textType == VerifyEmailOTPMessageType ||
textType == DomainClaimedMessageType ||
textType == PasswordlessRegistrationMessageType ||
textType == PasswordChangeMessageType
textType == PasswordChangeMessageType ||
textType == InviteUserMessageType
}

View File

@ -29,6 +29,7 @@ const (
NextStepProjectRequired
NextStepRedirectToExternalIDP
NextStepLoginSucceeded
NextStepVerifyInvite
)
type LoginStep struct{}
@ -191,3 +192,9 @@ type LoginSucceededStep struct{}
func (s *LoginSucceededStep) Type() NextStepType {
return NextStepLoginSucceeded
}
type VerifyInviteStep struct{}
func (s *VerifyInviteStep) Type() NextStepType {
return NextStepVerifyInvite
}

View File

@ -14,6 +14,7 @@ const (
SecretGeneratorTypeAppSecret
SecretGeneratorTypeOTPSMS
SecretGeneratorTypeOTPEmail
SecretGeneratorTypeInviteCode
secretGeneratorTypeCount
)

View File

@ -4,14 +4,11 @@ package domain
import (
"fmt"
"strings"
)
const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailsecret_generator_type_count"
const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesecret_generator_type_count"
var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 160}
const _SecretGeneratorTypeLowerName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailsecret_generator_type_count"
var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 171}
func (i SecretGeneratorType) String() string {
if i < 0 || i >= SecretGeneratorType(len(_SecretGeneratorTypeIndex)-1) {
@ -20,62 +17,21 @@ func (i SecretGeneratorType) String() string {
return _SecretGeneratorTypeName[_SecretGeneratorTypeIndex[i]:_SecretGeneratorTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _SecretGeneratorTypeNoOp() {
var x [1]struct{}
_ = x[SecretGeneratorTypeUnspecified-(0)]
_ = x[SecretGeneratorTypeInitCode-(1)]
_ = x[SecretGeneratorTypeVerifyEmailCode-(2)]
_ = x[SecretGeneratorTypeVerifyPhoneCode-(3)]
_ = x[SecretGeneratorTypeVerifyDomain-(4)]
_ = x[SecretGeneratorTypePasswordResetCode-(5)]
_ = x[SecretGeneratorTypePasswordlessInitCode-(6)]
_ = x[SecretGeneratorTypeAppSecret-(7)]
_ = x[SecretGeneratorTypeOTPSMS-(8)]
_ = x[SecretGeneratorTypeOTPEmail-(9)]
_ = x[secretGeneratorTypeCount-(10)]
}
var _SecretGeneratorTypeValues = []SecretGeneratorType{SecretGeneratorTypeUnspecified, SecretGeneratorTypeInitCode, SecretGeneratorTypeVerifyEmailCode, SecretGeneratorTypeVerifyPhoneCode, SecretGeneratorTypeVerifyDomain, SecretGeneratorTypePasswordResetCode, SecretGeneratorTypePasswordlessInitCode, SecretGeneratorTypeAppSecret, SecretGeneratorTypeOTPSMS, SecretGeneratorTypeOTPEmail, secretGeneratorTypeCount}
var _SecretGeneratorTypeValues = []SecretGeneratorType{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
var _SecretGeneratorTypeNameToValueMap = map[string]SecretGeneratorType{
_SecretGeneratorTypeName[0:11]: SecretGeneratorTypeUnspecified,
_SecretGeneratorTypeLowerName[0:11]: SecretGeneratorTypeUnspecified,
_SecretGeneratorTypeName[11:20]: SecretGeneratorTypeInitCode,
_SecretGeneratorTypeLowerName[11:20]: SecretGeneratorTypeInitCode,
_SecretGeneratorTypeName[20:37]: SecretGeneratorTypeVerifyEmailCode,
_SecretGeneratorTypeLowerName[20:37]: SecretGeneratorTypeVerifyEmailCode,
_SecretGeneratorTypeName[37:54]: SecretGeneratorTypeVerifyPhoneCode,
_SecretGeneratorTypeLowerName[37:54]: SecretGeneratorTypeVerifyPhoneCode,
_SecretGeneratorTypeName[54:67]: SecretGeneratorTypeVerifyDomain,
_SecretGeneratorTypeLowerName[54:67]: SecretGeneratorTypeVerifyDomain,
_SecretGeneratorTypeName[67:86]: SecretGeneratorTypePasswordResetCode,
_SecretGeneratorTypeLowerName[67:86]: SecretGeneratorTypePasswordResetCode,
_SecretGeneratorTypeName[86:108]: SecretGeneratorTypePasswordlessInitCode,
_SecretGeneratorTypeLowerName[86:108]: SecretGeneratorTypePasswordlessInitCode,
_SecretGeneratorTypeName[108:118]: SecretGeneratorTypeAppSecret,
_SecretGeneratorTypeLowerName[108:118]: SecretGeneratorTypeAppSecret,
_SecretGeneratorTypeName[118:124]: SecretGeneratorTypeOTPSMS,
_SecretGeneratorTypeLowerName[118:124]: SecretGeneratorTypeOTPSMS,
_SecretGeneratorTypeName[124:133]: SecretGeneratorTypeOTPEmail,
_SecretGeneratorTypeLowerName[124:133]: SecretGeneratorTypeOTPEmail,
_SecretGeneratorTypeName[133:160]: secretGeneratorTypeCount,
_SecretGeneratorTypeLowerName[133:160]: secretGeneratorTypeCount,
}
var _SecretGeneratorTypeNames = []string{
_SecretGeneratorTypeName[0:11],
_SecretGeneratorTypeName[11:20],
_SecretGeneratorTypeName[20:37],
_SecretGeneratorTypeName[37:54],
_SecretGeneratorTypeName[54:67],
_SecretGeneratorTypeName[67:86],
_SecretGeneratorTypeName[86:108],
_SecretGeneratorTypeName[108:118],
_SecretGeneratorTypeName[118:124],
_SecretGeneratorTypeName[124:133],
_SecretGeneratorTypeName[133:160],
_SecretGeneratorTypeName[0:11]: 0,
_SecretGeneratorTypeName[11:20]: 1,
_SecretGeneratorTypeName[20:37]: 2,
_SecretGeneratorTypeName[37:54]: 3,
_SecretGeneratorTypeName[54:67]: 4,
_SecretGeneratorTypeName[67:86]: 5,
_SecretGeneratorTypeName[86:108]: 6,
_SecretGeneratorTypeName[108:118]: 7,
_SecretGeneratorTypeName[118:124]: 8,
_SecretGeneratorTypeName[124:133]: 9,
_SecretGeneratorTypeName[133:144]: 10,
_SecretGeneratorTypeName[144:171]: 11,
}
// SecretGeneratorTypeString retrieves an enum value from the enum constants string name.
@ -84,10 +40,6 @@ func SecretGeneratorTypeString(s string) (SecretGeneratorType, error) {
if val, ok := _SecretGeneratorTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _SecretGeneratorTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to SecretGeneratorType values", s)
}
@ -96,13 +48,6 @@ func SecretGeneratorTypeValues() []SecretGeneratorType {
return _SecretGeneratorTypeValues
}
// SecretGeneratorTypeStrings returns a slice of all String values of the enum
func SecretGeneratorTypeStrings() []string {
strs := make([]string, len(_SecretGeneratorTypeNames))
copy(strs, _SecretGeneratorTypeNames)
return strs
}
// IsASecretGeneratorType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i SecretGeneratorType) IsASecretGeneratorType() bool {
for _, v := range _SecretGeneratorTypeValues {

View File

@ -775,3 +775,12 @@ func (i *Instance) CreateSchemaUser(ctx context.Context, orgID string, schemaID
logging.OnError(err).Fatal("create user")
return user
}
func (i *Instance) CreateInviteCode(ctx context.Context, userID string) *user_v2.CreateInviteCodeResponse {
user, err := i.Client.UserV2.CreateInviteCode(ctx, &user_v2.CreateInviteCodeRequest{
UserId: userID,
Verification: &user_v2.CreateInviteCodeRequest_ReturnCode{ReturnCode: &user_v2.ReturnInviteCode{}},
})
logging.OnError(err).Fatal("create invite code")
return user
}

View File

@ -19,6 +19,7 @@ type Commands interface {
HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error
PasswordChangeSent(ctx context.Context, orgID, userID string) error
HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string) error
InviteCodeSent(ctx context.Context, orgID, userID string) error
UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error
MilestonePushed(ctx context.Context, msType milestone.Type, endpoints []string, primaryDomain string) error
}

View File

@ -18,30 +18,30 @@ import (
gomock "go.uber.org/mock/gomock"
)
// MockCommands is a mock of Commands interface.
// MockCommands is a mock of Commands interface
type MockCommands struct {
ctrl *gomock.Controller
recorder *MockCommandsMockRecorder
}
// MockCommandsMockRecorder is the mock recorder for MockCommands.
// MockCommandsMockRecorder is the mock recorder for MockCommands
type MockCommandsMockRecorder struct {
mock *MockCommands
}
// NewMockCommands creates a new mock instance.
// NewMockCommands creates a new mock instance
func NewMockCommands(ctrl *gomock.Controller) *MockCommands {
mock := &MockCommands{ctrl: ctrl}
mock.recorder = &MockCommandsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockCommands) EXPECT() *MockCommandsMockRecorder {
return m.recorder
}
// HumanEmailVerificationCodeSent mocks base method.
// HumanEmailVerificationCodeSent mocks base method
func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", arg0, arg1, arg2)
@ -49,13 +49,13 @@ func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1
return ret0
}
// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent.
func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent
func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), arg0, arg1, arg2)
}
// HumanInitCodeSent mocks base method.
// HumanInitCodeSent mocks base method
func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanInitCodeSent", arg0, arg1, arg2)
@ -63,13 +63,13 @@ func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string
return ret0
}
// HumanInitCodeSent indicates an expected call of HumanInitCodeSent.
func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanInitCodeSent indicates an expected call of HumanInitCodeSent
func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), arg0, arg1, arg2)
}
// HumanOTPEmailCodeSent mocks base method.
// HumanOTPEmailCodeSent mocks base method
func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", arg0, arg1, arg2)
@ -77,13 +77,13 @@ func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 st
return ret0
}
// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent.
func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent
func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), arg0, arg1, arg2)
}
// HumanOTPSMSCodeSent mocks base method.
// HumanOTPSMSCodeSent mocks base method
func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", arg0, arg1, arg2)
@ -91,13 +91,13 @@ func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 stri
return ret0
}
// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent.
func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent
func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), arg0, arg1, arg2)
}
// HumanPasswordlessInitCodeSent mocks base method.
// HumanPasswordlessInitCodeSent mocks base method
func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1, arg2, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", arg0, arg1, arg2, arg3)
@ -105,13 +105,13 @@ func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1,
return ret0
}
// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent.
func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call {
// HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent
func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), arg0, arg1, arg2, arg3)
}
// HumanPhoneVerificationCodeSent mocks base method.
// HumanPhoneVerificationCodeSent mocks base method
func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", arg0, arg1, arg2)
@ -119,13 +119,27 @@ func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1
return ret0
}
// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent.
func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent
func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), arg0, arg1, arg2)
}
// MilestonePushed mocks base method.
// InviteCodeSent mocks base method
func (m *MockCommands) InviteCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InviteCodeSent", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// InviteCodeSent indicates an expected call of InviteCodeSent
func (mr *MockCommandsMockRecorder) InviteCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), arg0, arg1, arg2)
}
// MilestonePushed mocks base method
func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 milestone.Type, arg2 []string, arg3 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MilestonePushed", arg0, arg1, arg2, arg3)
@ -133,13 +147,13 @@ func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 milestone.Type
return ret0
}
// MilestonePushed indicates an expected call of MilestonePushed.
func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 any) *gomock.Call {
// MilestonePushed indicates an expected call of MilestonePushed
func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), arg0, arg1, arg2, arg3)
}
// OTPEmailSent mocks base method.
// OTPEmailSent mocks base method
func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OTPEmailSent", arg0, arg1, arg2)
@ -147,13 +161,13 @@ func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) err
return ret0
}
// OTPEmailSent indicates an expected call of OTPEmailSent.
func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 any) *gomock.Call {
// OTPEmailSent indicates an expected call of OTPEmailSent
func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), arg0, arg1, arg2)
}
// OTPSMSSent mocks base method.
// OTPSMSSent mocks base method
func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OTPSMSSent", arg0, arg1, arg2)
@ -161,13 +175,13 @@ func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string) error
return ret0
}
// OTPSMSSent indicates an expected call of OTPSMSSent.
func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2 any) *gomock.Call {
// OTPSMSSent indicates an expected call of OTPSMSSent
func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), arg0, arg1, arg2)
}
// PasswordChangeSent mocks base method.
// PasswordChangeSent mocks base method
func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PasswordChangeSent", arg0, arg1, arg2)
@ -175,13 +189,13 @@ func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 strin
return ret0
}
// PasswordChangeSent indicates an expected call of PasswordChangeSent.
func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 any) *gomock.Call {
// PasswordChangeSent indicates an expected call of PasswordChangeSent
func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), arg0, arg1, arg2)
}
// PasswordCodeSent mocks base method.
// PasswordCodeSent mocks base method
func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PasswordCodeSent", arg0, arg1, arg2)
@ -189,13 +203,13 @@ func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string)
return ret0
}
// PasswordCodeSent indicates an expected call of PasswordCodeSent.
func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2 any) *gomock.Call {
// PasswordCodeSent indicates an expected call of PasswordCodeSent
func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), arg0, arg1, arg2)
}
// UsageNotificationSent mocks base method.
// UsageNotificationSent mocks base method
func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.NotificationDueEvent) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UsageNotificationSent", arg0, arg1)
@ -203,13 +217,13 @@ func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.N
return ret0
}
// UsageNotificationSent indicates an expected call of UsageNotificationSent.
func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 any) *gomock.Call {
// UsageNotificationSent indicates an expected call of UsageNotificationSent
func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), arg0, arg1)
}
// UserDomainClaimedSent mocks base method.
// UserDomainClaimedSent mocks base method
func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserDomainClaimedSent", arg0, arg1, arg2)
@ -217,8 +231,8 @@ func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 st
return ret0
}
// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent.
func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 any) *gomock.Call {
// UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent
func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), arg0, arg1, arg2)
}

View File

@ -106,6 +106,10 @@ func (u *userNotifier) Reducers() []handler.AggregateReducer {
Event: user.HumanOTPEmailCodeAddedType,
Reduce: u.reduceOTPEmailCodeAdded,
},
{
Event: user.HumanInviteCodeAddedType,
Reduce: u.reduceInviteCodeAdded,
},
},
},
{
@ -718,6 +722,61 @@ func (u *userNotifier) reducePhoneCodeAdded(event eventstore.Event) (*handler.St
}), nil
}
func (u *userNotifier) reduceInviteCodeAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*user.HumanInviteCodeAddedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Eeg3s", "reduce.wrong.event.type %s", user.HumanInviteCodeAddedType)
}
if e.CodeReturned {
return handler.NewNoOpStatement(e), nil
}
return handler.NewStatement(event, func(ex handler.Executer, projectionName string) error {
ctx := HandlerContext(event.Aggregate())
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
user.HumanInviteCodeAddedType, user.HumanInviteCodeSentType)
if err != nil {
return err
}
if alreadyHandled {
return nil
}
code, err := crypto.DecryptString(e.Code, u.queries.UserDataCrypto)
if err != nil {
return err
}
colors, err := u.queries.ActiveLabelPolicyByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return err
}
template, err := u.queries.MailTemplateByOrg(ctx, e.Aggregate().ResourceOwner, false)
if err != nil {
return err
}
notifyUser, err := u.queries.GetNotifyUserByID(ctx, true, e.Aggregate().ID)
if err != nil {
return err
}
translator, err := u.queries.GetTranslatorWithOrgTexts(ctx, notifyUser.ResourceOwner, domain.InviteUserMessageType)
if err != nil {
return err
}
ctx, err = u.queries.Origin(ctx, e)
if err != nil {
return err
}
notify := types.SendEmail(ctx, u.channels, string(template.Template), translator, notifyUser, colors, e)
err = notify.SendInviteCode(ctx, notifyUser, code, e.ApplicationName, e.URLTemplate, e.AuthRequestID)
if err != nil {
return err
}
return u.commands.InviteCodeSent(ctx, e.Aggregate().ID, e.Aggregate().ResourceOwner)
}), nil
}
func (u *userNotifier) checkIfCodeAlreadyHandledOrExpired(ctx context.Context, event eventstore.Event, expiry time.Duration, data map[string]interface{}, eventTypes ...eventstore.EventType) (bool, error) {
if event.CreatedAt().Add(expiry).Before(time.Now().UTC()) {
return true, nil

View File

@ -69,3 +69,10 @@ PasswordChange:
Паролата на вашия потребител е променена, ако тази промяна не е направена от
вас, моля, незабавно нулирайте паролата си.
ButtonText: Влизам
InviteUser:
Title: Покана за {{.ApplicationName}}
PreHeader: Покана за {{.ApplicationName}}
Subject: Покана за {{.ApplicationName}}
Greeting: 'Здравейте {{.DisplayName}},'
Text: Вашият потребител е бил поканен за {{.ApplicationName}}. Моля, кликнете върху бутона по-долу, за да завършите процеса на покана. Ако не сте поискали този имейл, моля, игнорирайте го.
ButtonText: Приеми поканата

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Dobrý den, {{.DisplayName}},
Text: Heslo vašeho uživatele bylo změněno. Pokud tato změna nebyla provedena Vámi pak doporučujeme okamžitě resetovat/změnit vaše heslo.
ButtonText: Přihlásit se
InviteUser:
Title: Pozvánka do {{.ApplicationName}}
PreHeader: Pozvánka do {{.ApplicationName}}
Subject: Pozvánka do {{.ApplicationName}}
Greeting: Dobrý den, {{.DisplayName}},
Text: Váš uživatel byl pozván do {{.ApplicationName}}. Klikněte prosím na tlačítko níže, abyste dokončili proces pozvání. Pokud jste o tento e-mail nepožádali, prosím, ignorujte ho.
ButtonText: Přijmout pozvání

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Hallo {{.DisplayName}},
Text: Dein Passwort wurde geändert. Wenn diese Änderung nicht von dir gemacht wurde, empfehlen wir das sofortige Zurücksetzen deines Passworts.
ButtonText: Login
InviteUser:
Title: Einladung zu {{.ApplicationName}}
PreHeader: Einladung zu {{.ApplicationName}}
Subject: Einladung zu {{.ApplicationName}}
Greeting: Hallo {{.DisplayName}},
Text: Ihr Benutzer wurde zu {{.ApplicationName}} eingeladen. Bitte klicken Sie auf die Schaltfläche unten, um den Einladungsprozess abzuschließen. Wenn Sie diese E-Mail nicht angefordert haben, ignorieren Sie sie bitte.
ButtonText: Einladung annehmen

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Hello {{.DisplayName}},
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
InviteUser:
Title: Invitation to {{.ApplicationName}}
PreHeader: Invitation to {{.ApplicationName}}
Subject: Invitation to {{.ApplicationName}}
Greeting: Hello {{.DisplayName}},
Text: Your user has been invited to {{.ApplicationName}}. Please click the button below to finish the invite process. If you didn't ask for this mail, please ignore it.
ButtonText: Accept invite

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Hola {{.DisplayName}},
Text: La contraseña de tu usuario ha sido cambiada, si este cambio no fue hecho por ti, por favor proceder a restablecer inmediatamente tu contraseña.
ButtonText: Iniciar sesión
InviteUser:
Title: Invitación a {{.ApplicationName}}
PreHeader: Invitación a {{.ApplicationName}}
Subject: Invitación a {{.ApplicationName}}
Greeting: Hola {{.DisplayName}},
Text: Tu usuario ha sido invitado a {{.ApplicationName}}. Haz clic en el botón de abajo para finalizar el proceso de invitación. Si no solicitaste este correo electrónico, por favor ignóralo.
ButtonText: Aceptar invitación

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Bonjour {{.DisplayName}},
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
InviteUser:
Title: Invitation à {{.ApplicationName}}
PreHeader: Invitation à {{.ApplicationName}}
Subject: Invitation à {{.ApplicationName}}
Greeting: Bonjour {{.DisplayName}},
Text: Votre utilisateur a été invité à {{.ApplicationName}}. Veuillez cliquer sur le bouton ci-dessous pour terminer le processus d'invitation. Si vous n'avez pas demandé cet e-mail, veuillez l'ignorer.
ButtonText: Accepter l'invitation

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: 'Halo {{.DisplayName}},'
Text: 'Kata sandi pengguna Anda telah berubah. '
ButtonText: Login
InviteUser:
Title: Undangan ke {{.ApplicationName}}
PreHeader: Undangan ke {{.ApplicationName}}
Subject: Undangan ke {{.ApplicationName}}
Greeting: 'Halo {{.DisplayName}},'
Text: Pengguna Anda telah diundang ke {{.ApplicationName}}. Silakan klik tombol di bawah ini untuk menyelesaikan proses undangan. Jika Anda tidak meminta email ini, harap abaikan.
ButtonText: Terima undangan

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Ciao {{.DisplayName}},
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
InviteUser:
Title: Invito a {{.ApplicationName}}
PreHeader: Invito a {{.ApplicationName}}
Subject: Invito a {{.ApplicationName}}
Greeting: 'Ciao {{.DisplayName}},'
Text: Il tuo utente è stato invitato a {{.ApplicationName}}. Clicca sul pulsante qui sotto per completare il processo di invito. Se non hai richiesto questa email, ignorala.
ButtonText: Accetta invito

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: こんにちは {{.DisplayName}} さん、
Text: ユーザーのパスワードが変更されました。この変更があなたによって行われなかった場合は、すぐにパスワードをリセットすることをお勧めします。
ButtonText: ログイン
InviteUser:
Title: '{{.ApplicationName}}への招待'
PreHeader: '{{.ApplicationName}}への招待'
Subject: '{{.ApplicationName}}への招待'
Greeting: こんにちは {{.DisplayName}} さん、
Text: あなたのユーザーは{{.ApplicationName}}に招待されました。下のボタンをクリックして、招待プロセスを完了してください。このメールをリクエストしていない場合は、無視してください。
ButtonText: 招待を受け入れる

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Здраво {{.DisplayName}},
Text: Лозинката на вашиот корисник е променета. Ако оваа промена не е извршена од вас, ве молиме веднаш ресетирајте ја вашата лозинка.
ButtonText: Најава
InviteUser:
Title: Покана за {{.ApplicationName}}
PreHeader: Покана за {{.ApplicationName}}
Subject: Покана за {{.ApplicationName}}
Greeting: Здраво {{.DisplayName}},
Text: Вашиот корисник е бил поканет за {{.ApplicationName}}. Ве молиме кликнете на копчето подолу за да го завршите процесот на покана. Ако не сте побарале овој мејл, ве молиме игнорирајте го.
ButtonText: Прифати покана

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Hallo {{.DisplayName}},
Text: Het wachtwoord van uw gebruiker is veranderd. Als deze wijziging niet door u is gedaan, wordt u geadviseerd om direct uw wachtwoord te resetten.
ButtonText: Inloggen
InviteUser:
Title: Uitnodiging voor {{.ApplicationName}}
PreHeader: Uitnodiging voor {{.ApplicationName}}
Subject: Uitnodiging voor {{.ApplicationName}}
Greeting: Hallo {{.DisplayName}},
Text: Uw gebruiker is uitgenodigd voor {{.ApplicationName}}. Klik op de onderstaande knop om het uitnodigingsproces te voltooien. Als u deze e-mail niet hebt aangevraagd, negeer deze dan.
ButtonText: Uitnodiging accepteren

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Witaj {{.DisplayName}},
Text: Hasło Twojego użytkownika zostało zmienione, jeśli ta zmiana nie została dokonana przez Ciebie, zalecamy natychmiastowe zresetowanie hasła.
ButtonText: Zaloguj się
InviteUser:
Title: Zaproszenie do {{.ApplicationName}}
PreHeader: Zaproszenie do {{.ApplicationName}}
Subject: Zaproszenie do {{.ApplicationName}}
Greeting: Witaj {{.DisplayName}},
Text: Twój użytkownik został zaproszony do {{.ApplicationName}}. Kliknij poniższy przycisk, aby zakończyć proces zaproszenia. Jeśli nie zażądałeś tego e-maila, zignoruj go.
ButtonText: Akceptuj zaproszenie

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Olá {{.DisplayName}},
Text: A senha do seu usuário foi alterada. Se esta alteração não foi feita por você, recomendamos que você redefina sua senha imediatamente.
ButtonText: Fazer login
InviteUser:
Title: Convite para {{.ApplicationName}}
PreHeader: Convite para {{.ApplicationName}}
Subject: Convite para {{.ApplicationName}}
Greeting: Olá {{.DisplayName}},
Text: Seu usuário foi convidado para {{.ApplicationName}}. Clique no botão abaixo para concluir o processo de convite. Se você não solicitou este e-mail, por favor, ignore-o.
ButtonText: Aceitar convite

View File

@ -2,28 +2,28 @@ InitCode:
Title: Регистрация пользователя
PreHeader: Регистрация пользователя
Subject: Регистрация пользователя
Greeting: Здравствуйте {{.FirstName}} {{.LastName}},
Greeting: Здравствуйте {{.DisplayName}},
Text: Используйте логин {{.PreferredLoginName}} для входа. Пожалуйста, нажмите кнопку ниже для завершения процесса регистрации. (Код {{.Code}}) Если вы не запрашивали это письмо, пожалуйста, проигнорируйте его.
ButtonText: Завершить регистрацию
PasswordReset:
Title: Сброс пароля
PreHeader: Сброс пароля
Subject: Сброс пароля
Greeting: Здравствуйте {{.FirstName}} {{.LastName}},
Greeting: Здравствуйте {{.DisplayName}},
Text: Мы получили запрос на сброс пароля. Пожалуйста, нажмите кнопку ниже для сброса вашего пароля. (Код {{.Code}}) Если вы не запрашивали это письмо, пожалуйста, проигнорируйте его.
ButtonText: Сбросить пароль
VerifyEmail:
Title: Подтверждение email
PreHeader: Подтверждение email
Subject: Подтверждение email
Greeting: Здравствуйте {{.FirstName}} {{.LastName}},
Greeting: Здравствуйте {{.DisplayName}},
Text: Добавлен новый email. Пожалуйста, нажмите кнопку ниже для подтверждения вашего email. (Код {{.Code}}) Если вы не запрашивали это письмо, пожалуйста, проигнорируйте его.
ButtonText: Подтвердить email
VerifyPhone:
Title: Подтверждение телефона
PreHeader: Подтверждение телефона
Subject: Подтверждение телефона
Greeting: Здравствуйте {{.FirstName}} {{.LastName}},
Greeting: Здравствуйте {{.DisplayName}},
Text: Добавлен новый номер телефона. Пожалуйста, используйте следующий код, чтобы подтвердить его. Код {{.Code}}
ButtonText: Подтвердить телефон
VerifyEmailOTP:
@ -42,20 +42,27 @@ DomainClaimed:
Title: Утверждение домена
PreHeader: Изменение email / логина
Subject: Домен был утвержден
Greeting: Здравствуйте {{.FirstName}} {{.LastName}},
Greeting: Здравствуйте {{.DisplayName}},
Text: Домен {{.Domain}} был утвержден организацией. Ваш текущий пользователь {{.Username}} не является частью этой организации. Вам необходимо изменить свой email при входе в систему. Мы создали временный логин ({{.TempUsername}}) для входа.
ButtonText: Вход
PasswordlessRegistration:
Title: Добавление входа без пароля
PreHeader: Добавление входа без пароля
Subject: Добавление входа без пароля
Greeting: Здравствуйте {{.FirstName}} {{.LastName}},
Greeting: Здравствуйте {{.DisplayName}},
Text: Мы получили запрос на добавление токена для входа без пароля. Пожалуйста, используйте кнопку ниже, чтобы добавить свой токен или устройство для входа без пароля.
ButtonText: Добавить вход без пароля
PasswordChange:
Title: Смена пароля пользователя
PreHeader: Смена пароля
Subject: Пароль пользователя изменен
Greeting: Здравствуйте {{.FirstName}} {{.LastName}},
Greeting: Здравствуйте {{.DisplayName}},
Text: Пароль пользователя был изменен. Если это изменение сделано не вами, советуем немедленно сбросить пароль.
ButtonText: Вход
InviteUser:
Title: Приглашение в {{.ApplicationName}}
PreHeader: Приглашение в {{.ApplicationName}}
Subject: Приглашение в {{.ApplicationName}}
Greeting: Здравствуйте, {{.DisplayName}},
Text: Ваш пользователь был приглашен в {{.ApplicationName}}. Пожалуйста, нажмите кнопку ниже, чтобы завершить процесс приглашения. Если вы не запрашивали это письмо, пожалуйста, игнорируйте его.
ButtonText: Принять приглашение

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: Hej {{.DisplayName}},
Text: Lösenordet för din användare har ändrats. Om denna ändring inte gjordes av dig, vänligen återställ ditt lösenord omedelbart.
ButtonText: Logga in
InviteUser:
Title: Inbjudan till {{.ApplicationName}}
PreHeader: Inbjudan till {{.ApplicationName}}
Subject: Inbjudan till {{.ApplicationName}}
Greeting: Hej {{.DisplayName}},
Text: Din användare har blivit inbjuden till {{.ApplicationName}}. Klicka på knappen nedan för att slutföra inbjudansprocessen. Om du inte har begärt detta e-postmeddelande, ignorera det.
ButtonText: Acceptera inbjudan

View File

@ -59,3 +59,10 @@ PasswordChange:
Greeting: 你好 {{.DisplayName}},
Text: 您的用户的密码已经改变,如果这个改变不是由您做的,请注意立即重新设置您的密码。
ButtonText: 登录
InviteUser:
Title: '{{.ApplicationName}}邀请'
PreHeader: '{{.ApplicationName}}邀请'
Subject: '{{.ApplicationName}}邀请'
Greeting: 您好,{{.DisplayName}},
Text: 您的用户已被邀请加入{{.ApplicationName}}。请点击下面的按钮完成邀请过程。如果您没有请求此邮件,请忽略它。
ButtonText: 接受邀请

View File

@ -0,0 +1,31 @@
package types
import (
"context"
"strings"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
func (notify Notify) SendInviteCode(ctx context.Context, user *query.NotifyUser, code, applicationName, urlTmpl, authRequestID string) error {
var url string
if applicationName == "" {
applicationName = "ZITADEL"
}
if urlTmpl == "" {
url = login.InviteUserLink(http_utils.DomainContext(ctx).Origin(), user.ID, user.PreferredLoginName, code, user.ResourceOwner, authRequestID)
} else {
var buf strings.Builder
if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil {
return err
}
url = buf.String()
}
args := make(map[string]interface{})
args["Code"] = code
args["ApplicationName"] = applicationName
return notify(url, args, domain.InviteUserMessageType, true)
}

View File

@ -33,6 +33,7 @@ type MessageTexts struct {
DomainClaimed MessageText
PasswordlessRegistration MessageText
PasswordChange MessageText
InviteUser MessageText
}
type MessageText struct {
@ -346,6 +347,8 @@ func (m *MessageTexts) GetMessageTextByType(msgType string) *MessageText {
return &m.PasswordlessRegistration
case domain.PasswordChangeMessageType:
return &m.PasswordChange
case domain.InviteUserMessageType:
return &m.InviteUser
}
return nil
}

View File

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

View File

@ -137,4 +137,8 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, MachineSecretCheckSucceededType, MachineSecretCheckSucceededEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, MachineSecretCheckFailedType, MachineSecretCheckFailedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, MachineSecretHashUpdatedType, eventstore.GenericEventMapper[MachineSecretHashUpdatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, HumanInviteCodeAddedType, eventstore.GenericEventMapper[HumanInviteCodeAddedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, HumanInviteCodeSentType, eventstore.GenericEventMapper[HumanInviteCodeSentEvent])
eventstore.RegisterFilterEventMapper(AggregateType, HumanInviteCheckSucceededType, eventstore.GenericEventMapper[HumanInviteCheckSucceededEvent])
eventstore.RegisterFilterEventMapper(AggregateType, HumanInviteCheckFailedType, eventstore.GenericEventMapper[HumanInviteCheckFailedEvent])
}

View File

@ -21,6 +21,10 @@ const (
HumanInitialCodeSentType = humanEventPrefix + "initialization.code.sent"
HumanInitializedCheckSucceededType = humanEventPrefix + "initialization.check.succeeded"
HumanInitializedCheckFailedType = humanEventPrefix + "initialization.check.failed"
HumanInviteCodeAddedType = humanEventPrefix + "invite.code.added"
HumanInviteCodeSentType = humanEventPrefix + "invite.code.sent"
HumanInviteCheckSucceededType = humanEventPrefix + "invite.check.succeeded"
HumanInviteCheckFailedType = humanEventPrefix + "invite.check.failed"
HumanSignedOutType = humanEventPrefix + "signed.out"
)
@ -379,6 +383,137 @@ func HumanInitializedCheckFailedEventMapper(event eventstore.Event) (eventstore.
}, nil
}
type HumanInviteCodeAddedEvent struct {
*eventstore.BaseEvent `json:"-"`
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
URLTemplate string `json:"urlTemplate,omitempty"`
CodeReturned bool `json:"codeReturned,omitempty"`
ApplicationName string `json:"applicationName,omitempty"`
AuthRequestID string `json:"authRequestID,omitempty"`
}
func (e *HumanInviteCodeAddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *HumanInviteCodeAddedEvent) Payload() interface{} {
return e
}
func (e *HumanInviteCodeAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *HumanInviteCodeAddedEvent) TriggerOrigin() string {
return e.TriggeredAtOrigin
}
func NewHumanInviteCodeAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
code *crypto.CryptoValue,
expiry time.Duration,
urlTemplate string,
codeReturned bool,
applicationName string,
authRequestID string,
) *HumanInviteCodeAddedEvent {
return &HumanInviteCodeAddedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanInviteCodeAddedType,
),
Code: code,
Expiry: expiry,
TriggeredAtOrigin: http.DomainContext(ctx).Origin(),
URLTemplate: urlTemplate,
CodeReturned: codeReturned,
ApplicationName: applicationName,
AuthRequestID: authRequestID,
}
}
type HumanInviteCodeSentEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *HumanInviteCodeSentEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *HumanInviteCodeSentEvent) Payload() interface{} {
return nil
}
func (e *HumanInviteCodeSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewHumanInviteCodeSentEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanInviteCodeSentEvent {
return &HumanInviteCodeSentEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanInviteCodeSentType,
),
}
}
type HumanInviteCheckSucceededEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *HumanInviteCheckSucceededEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *HumanInviteCheckSucceededEvent) Payload() interface{} {
return nil
}
func (e *HumanInviteCheckSucceededEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewHumanInviteCheckSucceededEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanInviteCheckSucceededEvent {
return &HumanInviteCheckSucceededEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanInviteCheckSucceededType,
),
}
}
type HumanInviteCheckFailedEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *HumanInviteCheckFailedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *HumanInviteCheckFailedEvent) Payload() interface{} {
return nil
}
func (e *HumanInviteCheckFailedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewHumanInviteCheckFailedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *HumanInviteCheckFailedEvent {
return &HumanInviteCheckFailedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
HumanInviteCheckFailedType,
),
}
}
type HumanSignedOutEvent struct {
eventstore.BaseEvent `json:"-"`

View File

@ -704,6 +704,13 @@ EventTypes:
check:
succeeded: Проверката за инициализация е успешна
failed: Проверката на инициализацията е неуспешна
invite:
code:
added: Генериран е код за покана
sent: Изпратен е код за покана
check:
succeeded: Проверката на поканата е успешна
failed: Проверката на поканата е неуспешна
username:
reserved: Потребителското име е запазено
released: Потребителското име е освободено

View File

@ -685,6 +685,13 @@ EventTypes:
check:
succeeded: Kontrola inicializace byla úspěšná
failed: Kontrola inicializace selhala
invite:
code:
added: Vygenerován pozvánkový kód
sent: Pozvánkový kód byl odeslán
check:
succeeded: Kontrola pozvánky byla úspěšná
failed: Kontrola pozvánky selhala
username:
reserved: Uživatelské jméno rezervováno
released: Uživatelské jméno uvolněno

View File

@ -687,6 +687,13 @@ EventTypes:
check:
succeeded: Benutzerinitialisierung erfolgreich
failed: Benutzerinitialisierung fehlgeschlagen
invite:
code:
added: Einladungscode generiert
sent: Einladungscode gesendet
check:
succeeded: Einladungsprüfung erfolgreich
failed: Einladungsprüfung fehlgeschlagen
username:
reserved: Benutzername reserviert
released: Benutzername freigegeben

View File

@ -687,6 +687,13 @@ EventTypes:
check:
succeeded: Initialization check succeeded
failed: Initialization check failed
invite:
code:
added: Invitation code generated
sent: Invitation code sent
check:
succeeded: Invitation check succeeded
failed: Invitation check failed
username:
reserved: Username reserved
released: Username released

View File

@ -687,6 +687,13 @@ EventTypes:
check:
succeeded: Comprobación exitosa de la inicialización
failed: Fallo en la comprobación de la inicialización
invite:
code:
added: Código de invitación generado
sent: Código de invitación enviado
check:
succeeded: Comprobación de invitación correcta
failed: Comprobación de invitación fallida
username:
reserved: Nombre de usuario reservado
released: Nombre de usuario liberado

View File

@ -685,6 +685,13 @@ EventTypes:
check:
succeeded: Vérification de l'initialisation réussie
failed: La vérification de l'initialisation a échoué
invite:
code:
added: Code d'invitation généré
sent: Code d'invitation envoyé
check:
succeeded: Vérification de l'invitation réussie
failed: Vérification de l'invitation échouée
username:
reserved: Nom d'utilisateur réservé
released: Nom d'utilisateur libéré

View File

@ -680,6 +680,13 @@ EventTypes:
check:
succeeded: Pemeriksaan inisialisasi berhasil
failed: Pemeriksaan inisialisasi gagal
invite:
code:
added: Kode undangan dihasilkan
sent: Kode undangan dikirim
check:
succeeded: Pemeriksaan undangan berhasil
failed: Pemeriksaan undangan gagal
username:
reserved: Nama pengguna dicadangkan
released: Nama pengguna dirilis

View File

@ -686,6 +686,13 @@ EventTypes:
check:
succeeded: Controllo dell'inizializzazione riuscito
failed: Controllo dell'inizializzazione fallito
invite:
code:
added: Codice invito generato
sent: Codice invito inviato
check:
succeeded: Controllo invito riuscito
failed: Controllo invito fallito
username:
reserved: Nome utente riservato
released: Nome utente rilasciato

Some files were not shown because too many files have changed in this diff Show More