mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:17:32 +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:
@@ -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" .}}
|
Reference in New Issue
Block a user