mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
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:
parent
02c78a19c6
commit
a07b2f4677
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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": "Персонализираните текстове са запазени."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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." }
|
||||
},
|
||||
|
@ -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."
|
||||
|
@ -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": "カスタムテキストが保存されました。"
|
||||
|
@ -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": "Прилагодените текстови се зачувани."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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": "Тексты сохранены."
|
||||
|
@ -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."
|
||||
|
@ -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": "自定义文本已保存。"
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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(),
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
154
internal/api/ui/login/invite_user_handler.go
Normal file
154
internal/api/ui/login/invite_user_handler.go
Normal 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)
|
||||
}
|
@ -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"))
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -81,6 +81,14 @@ InitUserDone:
|
||||
Description: Имейлът е потвърден и паролата е успешно зададена
|
||||
NextButtonText: следващия
|
||||
CancelButtonText: анулиране
|
||||
InviteUser:
|
||||
Title: Активиране на потребителя
|
||||
Description: Проверете своя имейл с кода по-долу и задайте паролата си.
|
||||
CodeLabel: Код
|
||||
NewPasswordLabel: Нова парола
|
||||
NewPasswordConfirm: Потвърди парола
|
||||
NextButtonText: Напред
|
||||
ResendButtonText: Изпрати отново код
|
||||
InitMFAPrompt:
|
||||
Title: 2-факторна настройка
|
||||
Description: >-
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -79,6 +79,15 @@ InitUserDone:
|
||||
NextButtonText: 次へ
|
||||
CancelButtonText: キャンセル
|
||||
|
||||
InviteUser:
|
||||
Title: ユーザーの有効化
|
||||
Description: 下のコードでメールアドレスを確認し、パスワードを設定してください。
|
||||
CodeLabel: コード
|
||||
NewPasswordLabel: 新しいパスワード
|
||||
NewPasswordConfirm: パスワードの確認
|
||||
NextButtonText: 次へ
|
||||
ResendButtonText: コードを再送信
|
||||
|
||||
InitMFAPrompt:
|
||||
Title: 二要素認証のセットアップ
|
||||
Description: 二要素認証でアカウントのセキュリティを強化します。
|
||||
|
@ -86,6 +86,15 @@ InitUserDone:
|
||||
NextButtonText: следно
|
||||
CancelButtonText: откажи
|
||||
|
||||
InviteUser:
|
||||
Title: Активирање на корисникот
|
||||
Description: Проверете го вашиот имејл со кодот подолу и поставете ја вашата лозинка.
|
||||
CodeLabel: Код
|
||||
NewPasswordLabel: Нова лозинка
|
||||
NewPasswordConfirm: Потврди лозинка
|
||||
NextButtonText: Следно
|
||||
ResendButtonText: Повторно испрати код
|
||||
|
||||
InitMFAPrompt:
|
||||
Title: Подесување на 2-факторска автентикација
|
||||
Description: 2-факторската автентикација ви дава дополнителна безбедност за вашата корисничка сметка. Ова обезбедува само вие да имате пристап до вашата сметка.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -85,6 +85,15 @@ InitUserDone:
|
||||
NextButtonText: далее
|
||||
CancelButtonText: отмена
|
||||
|
||||
InviteUser:
|
||||
Title: Активировать пользователя
|
||||
Description: Проверьте свой адрес электронной почты с помощью кода ниже и установите свой пароль.
|
||||
CodeLabel: Код
|
||||
NewPasswordLabel: Новый пароль
|
||||
NewPasswordConfirm: Подтвердить пароль
|
||||
NextButtonText: Далее
|
||||
ResendButtonText: Отправить код повторно
|
||||
|
||||
InitMFAPrompt:
|
||||
Title: Установка двухфакторной аутентификации
|
||||
Description: Двухфакторная аутентификация обеспечивает дополнительную защиту вашей учётной записи.
|
||||
|
@ -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.
|
||||
|
@ -86,6 +86,15 @@ InitUserDone:
|
||||
NextButtonText: 继续
|
||||
CancelButtonText: 取消
|
||||
|
||||
InviteUser:
|
||||
Title: 激活用户
|
||||
Description: 使用以下代码验证您的电子邮件并设置您的密码。
|
||||
CodeLabel: 代码
|
||||
NewPasswordLabel: 新密码
|
||||
NewPasswordConfirm: 确认密码
|
||||
NextButtonText: 下一步
|
||||
ResendButtonText: 重新发送代码
|
||||
|
||||
InitMFAPrompt:
|
||||
Title: 两步验证设置
|
||||
Description: 两步验证为您的账户提供了额外的安全保障。这确保只有你能访问你的账户。
|
||||
|
63
internal/api/ui/login/static/templates/invite_user.html
Normal file
63
internal/api/ui/login/static/templates/invite_user.html
Normal 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" .}}
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -145,6 +145,7 @@ type SecretGenerators struct {
|
||||
DomainVerification *crypto.GeneratorConfig
|
||||
OTPSMS *crypto.GeneratorConfig
|
||||
OTPEmail *crypto.GeneratorConfig
|
||||
InviteCode *crypto.GeneratorConfig
|
||||
}
|
||||
|
||||
type ZitadelConfig struct {
|
||||
|
@ -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())
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
}
|
||||
|
193
internal/command/user_v2_invite.go
Normal file
193
internal/command/user_v2_invite.go
Normal 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
|
||||
}
|
141
internal/command/user_v2_invite_model.go
Normal file
141
internal/command/user_v2_invite_model.go
Normal 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)
|
||||
}
|
1207
internal/command/user_v2_invite_test.go
Normal file
1207
internal/command/user_v2_invite_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ const (
|
||||
SecretGeneratorTypeAppSecret
|
||||
SecretGeneratorTypeOTPSMS
|
||||
SecretGeneratorTypeOTPEmail
|
||||
SecretGeneratorTypeInviteCode
|
||||
|
||||
secretGeneratorTypeCount
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -69,3 +69,10 @@ PasswordChange:
|
||||
Паролата на вашия потребител е променена, ако тази промяна не е направена от
|
||||
вас, моля, незабавно нулирайте паролата си.
|
||||
ButtonText: Влизам
|
||||
InviteUser:
|
||||
Title: Покана за {{.ApplicationName}}
|
||||
PreHeader: Покана за {{.ApplicationName}}
|
||||
Subject: Покана за {{.ApplicationName}}
|
||||
Greeting: 'Здравейте {{.DisplayName}},'
|
||||
Text: Вашият потребител е бил поканен за {{.ApplicationName}}. Моля, кликнете върху бутона по-долу, за да завършите процеса на покана. Ако не сте поискали този имейл, моля, игнорирайте го.
|
||||
ButtonText: Приеми поканата
|
@ -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í
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -59,3 +59,10 @@ PasswordChange:
|
||||
Greeting: こんにちは {{.DisplayName}} さん、
|
||||
Text: ユーザーのパスワードが変更されました。この変更があなたによって行われなかった場合は、すぐにパスワードをリセットすることをお勧めします。
|
||||
ButtonText: ログイン
|
||||
InviteUser:
|
||||
Title: '{{.ApplicationName}}への招待'
|
||||
PreHeader: '{{.ApplicationName}}への招待'
|
||||
Subject: '{{.ApplicationName}}への招待'
|
||||
Greeting: こんにちは {{.DisplayName}} さん、
|
||||
Text: あなたのユーザーは{{.ApplicationName}}に招待されました。下のボタンをクリックして、招待プロセスを完了してください。このメールをリクエストしていない場合は、無視してください。
|
||||
ButtonText: 招待を受け入れる
|
@ -59,3 +59,10 @@ PasswordChange:
|
||||
Greeting: Здраво {{.DisplayName}},
|
||||
Text: Лозинката на вашиот корисник е променета. Ако оваа промена не е извршена од вас, ве молиме веднаш ресетирајте ја вашата лозинка.
|
||||
ButtonText: Најава
|
||||
InviteUser:
|
||||
Title: Покана за {{.ApplicationName}}
|
||||
PreHeader: Покана за {{.ApplicationName}}
|
||||
Subject: Покана за {{.ApplicationName}}
|
||||
Greeting: Здраво {{.DisplayName}},
|
||||
Text: Вашиот корисник е бил поканет за {{.ApplicationName}}. Ве молиме кликнете на копчето подолу за да го завршите процесот на покана. Ако не сте побарале овој мејл, ве молиме игнорирајте го.
|
||||
ButtonText: Прифати покана
|
@ -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
|
@ -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
|
@ -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
|
@ -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: Принять приглашение
|
@ -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
|
@ -59,3 +59,10 @@ PasswordChange:
|
||||
Greeting: 你好 {{.DisplayName}},
|
||||
Text: 您的用户的密码已经改变,如果这个改变不是由您做的,请注意立即重新设置您的密码。
|
||||
ButtonText: 登录
|
||||
InviteUser:
|
||||
Title: '{{.ApplicationName}}邀请'
|
||||
PreHeader: '{{.ApplicationName}}邀请'
|
||||
Subject: '{{.ApplicationName}}邀请'
|
||||
Greeting: 您好,{{.DisplayName}},
|
||||
Text: 您的用户已被邀请加入{{.ApplicationName}}。请点击下面的按钮完成邀请过程。如果您没有请求此邮件,请忽略它。
|
||||
ButtonText: 接受邀请
|
31
internal/notification/types/invite_code.go
Normal file
31
internal/notification/types/invite_code.go
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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:"-"`
|
||||
|
||||
|
@ -704,6 +704,13 @@ EventTypes:
|
||||
check:
|
||||
succeeded: Проверката за инициализация е успешна
|
||||
failed: Проверката на инициализацията е неуспешна
|
||||
invite:
|
||||
code:
|
||||
added: Генериран е код за покана
|
||||
sent: Изпратен е код за покана
|
||||
check:
|
||||
succeeded: Проверката на поканата е успешна
|
||||
failed: Проверката на поканата е неуспешна
|
||||
username:
|
||||
reserved: Потребителското име е запазено
|
||||
released: Потребителското име е освободено
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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é
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user