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();
}
+34
View File
@@ -72,6 +72,8 @@ import {
GetCustomPasswordChangeMessageTextResponse,
GetCustomPasswordlessRegistrationMessageTextRequest,
GetCustomPasswordlessRegistrationMessageTextResponse,
GetCustomInviteUserMessageTextRequest,
GetCustomInviteUserMessageTextResponse,
GetCustomPasswordResetMessageTextRequest,
GetCustomPasswordResetMessageTextResponse,
GetCustomVerifyEmailMessageTextRequest,
@@ -96,6 +98,8 @@ import {
GetDefaultPasswordChangeMessageTextResponse,
GetDefaultPasswordlessRegistrationMessageTextRequest,
GetDefaultPasswordlessRegistrationMessageTextResponse,
GetDefaultInviteUserMessageTextRequest,
GetDefaultInviteUserMessageTextResponse,
GetDefaultPasswordResetMessageTextRequest,
GetDefaultPasswordResetMessageTextResponse,
GetDefaultVerifyEmailMessageTextRequest,
@@ -224,6 +228,8 @@ import {
SetDefaultPasswordChangeMessageTextResponse,
SetDefaultPasswordlessRegistrationMessageTextRequest,
SetDefaultPasswordlessRegistrationMessageTextResponse,
SetDefaultInviteUserMessageTextRequest,
SetDefaultInviteUserMessageTextResponse,
SetDefaultPasswordResetMessageTextRequest,
SetDefaultPasswordResetMessageTextResponse,
SetDefaultVerifyEmailMessageTextRequest,
@@ -311,6 +317,8 @@ import {
ResetCustomPasswordChangeMessageTextToDefaultResponse,
ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest,
ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse,
ResetCustomInviteUserMessageTextToDefaultRequest,
ResetCustomInviteUserMessageTextToDefaultResponse,
ResetCustomPasswordResetMessageTextToDefaultRequest,
ResetCustomPasswordResetMessageTextToDefaultResponse,
ResetCustomVerifyEmailMessageTextToDefaultRequest,
@@ -722,6 +730,32 @@ export class AdminService {
return this.grpcService.admin.resetCustomPasswordChangeMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public getDefaultInviteUserMessageText(
req: GetDefaultInviteUserMessageTextRequest,
): Promise<GetDefaultInviteUserMessageTextResponse.AsObject> {
return this.grpcService.admin.getDefaultInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public getCustomInviteUserMessageText(
req: GetCustomInviteUserMessageTextRequest,
): Promise<GetCustomInviteUserMessageTextResponse.AsObject> {
return this.grpcService.admin.getCustomInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public setDefaultInviteUserMessageText(
req: SetDefaultInviteUserMessageTextRequest,
): Promise<SetDefaultInviteUserMessageTextResponse.AsObject> {
return this.grpcService.admin.setDefaultInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public resetCustomInviteUserMessageTextToDefault(
lang: string,
): Promise<ResetCustomInviteUserMessageTextToDefaultResponse.AsObject> {
const req = new ResetCustomInviteUserMessageTextToDefaultRequest();
req.setLanguage(lang);
return this.grpcService.admin.resetCustomInviteUserMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public SetUpOrg(org: SetUpOrgRequest.Org, human: SetUpOrgRequest.Human): Promise<SetUpOrgResponse.AsObject> {
const req = new SetUpOrgRequest();
+34
View File
@@ -133,6 +133,8 @@ import {
GetCustomLoginTextsResponse,
GetCustomPasswordChangeMessageTextRequest,
GetCustomPasswordChangeMessageTextResponse,
GetCustomInviteUserMessageTextRequest,
GetCustomInviteUserMessageTextResponse,
GetCustomPasswordlessRegistrationMessageTextRequest,
GetCustomPasswordlessRegistrationMessageTextResponse,
GetCustomPasswordResetMessageTextRequest,
@@ -155,6 +157,8 @@ import {
GetDefaultLoginTextsResponse,
GetDefaultPasswordChangeMessageTextRequest,
GetDefaultPasswordChangeMessageTextResponse,
GetDefaultInviteUserMessageTextRequest,
GetDefaultInviteUserMessageTextResponse,
GetDefaultPasswordComplexityPolicyRequest,
GetDefaultPasswordComplexityPolicyResponse,
GetDefaultPasswordlessRegistrationMessageTextRequest,
@@ -386,6 +390,8 @@ import {
ResetCustomLoginTextsToDefaultResponse,
ResetCustomPasswordChangeMessageTextToDefaultRequest,
ResetCustomPasswordChangeMessageTextToDefaultResponse,
ResetCustomInviteUserMessageTextToDefaultRequest,
ResetCustomInviteUserMessageTextToDefaultResponse,
ResetCustomPasswordlessRegistrationMessageTextToDefaultRequest,
ResetCustomPasswordlessRegistrationMessageTextToDefaultResponse,
ResetCustomPasswordResetMessageTextToDefaultRequest,
@@ -423,6 +429,8 @@ import {
SetCustomLoginTextsResponse,
SetCustomPasswordChangeMessageTextRequest,
SetCustomPasswordChangeMessageTextResponse,
SetCustomInviteUserMessageTextRequest,
SetCustomInviteUserMessageTextResponse,
SetCustomPasswordlessRegistrationMessageTextRequest,
SetCustomPasswordlessRegistrationMessageTextResponse,
SetCustomPasswordResetMessageTextRequest,
@@ -804,6 +812,32 @@ export class ManagementService {
return this.grpcService.mgmt.resetCustomPasswordChangeMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public getDefaultInviteUserMessageText(
req: GetDefaultInviteUserMessageTextRequest,
): Promise<GetDefaultInviteUserMessageTextResponse.AsObject> {
return this.grpcService.mgmt.getDefaultInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public getCustomInviteUserMessageText(
req: GetCustomInviteUserMessageTextRequest,
): Promise<GetCustomInviteUserMessageTextResponse.AsObject> {
return this.grpcService.mgmt.getCustomInviteUserMessageText(req, null).then((resp) => resp.toObject());
}
public setCustomInviteUserMessageText(
req: SetCustomInviteUserMessageTextRequest,
): Promise<SetCustomInviteUserMessageTextResponse.AsObject> {
return this.grpcService.mgmt.setCustomInviteUserMessageCustomText(req, null).then((resp) => resp.toObject());
}
public resetCustomInviteUserMessageTextToDefault(
lang: string,
): Promise<ResetCustomInviteUserMessageTextToDefaultResponse.AsObject> {
const req = new ResetCustomInviteUserMessageTextToDefaultRequest();
req.setLanguage(lang);
return this.grpcService.mgmt.resetCustomInviteUserMessageTextToDefault(req, null).then((resp) => resp.toObject());
}
public updateUserName(userId: string, username: string): Promise<UpdateUserNameResponse.AsObject> {
const req = new UpdateUserNameRequest();
req.setUserId(userId);