feat: invite user link (#8578)

# Which Problems Are Solved

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

# How the Problems Are Solved

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

# Additional Changes

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

# Additional Context

- closes #8310
- TODO: login V2 will have to implement invite flow:
https://github.com/zitadel/typescript/issues/166
This commit is contained in:
Livio Spring
2024-09-11 10:53:55 +00:00
committed by GitHub
parent 02c78a19c6
commit a07b2f4677
114 changed files with 3898 additions and 293 deletions
@@ -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();
}