mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
feat: add domain verification notification (#649)
* fix: dont (re)generate client secret with auth type none * fix(cors): allow Origin from request * feat: add origin allow list and fix some core issues * rename migration * fix UserIDsByDomain * feat: send email to users after domain claim * username * check origin on userinfo * update oidc pkg * fix: add migration 1.6 * change username * change username * remove unique email aggregate * change username in mgmt * search global user by login name * fix test * change user search in angular * fix tests * merge * userview in angular * fix merge * Update pkg/grpc/management/proto/management.proto Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * Update internal/notification/static/i18n/de.yaml Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> * fix Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
@@ -80,7 +80,8 @@ func (u *User) ProcessUser(event *models.Event) (err error) {
|
||||
return err
|
||||
}
|
||||
err = user.AppendEvent(event)
|
||||
case es_model.DomainClaimed:
|
||||
case es_model.DomainClaimed,
|
||||
es_model.UserUserNameChanged:
|
||||
user, err = u.view.UserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -19,8 +19,8 @@ func (v *View) SearchUsers(request *usr_model.UserSearchRequest) ([]*model.UserV
|
||||
return view.SearchUsers(v.Db, userTable, request)
|
||||
}
|
||||
|
||||
func (v *View) GetGlobalUserByEmail(email string) (*model.UserView, error) {
|
||||
return view.GetGlobalUserByEmail(v.Db, userTable, email)
|
||||
func (v *View) GetGlobalUserByLoginName(loginName string) (*model.UserView, error) {
|
||||
return view.GetGlobalUserByLoginName(v.Db, userTable, loginName)
|
||||
}
|
||||
|
||||
func (v *View) UsersByOrgID(orgID string) ([]*model.UserView, error) {
|
||||
|
@@ -69,6 +69,10 @@ func (s *Server) UpdateMyUserProfile(ctx context.Context, request *auth.UpdateUs
|
||||
return profileFromModel(profile), nil
|
||||
}
|
||||
|
||||
func (s *Server) ChangeMyUserName(ctx context.Context, request *auth.ChangeUserNameRequest) (*empty.Empty, error) {
|
||||
return &empty.Empty{}, s.repo.ChangeMyUsername(ctx, request.UserName)
|
||||
}
|
||||
|
||||
func (s *Server) ChangeMyUserEmail(ctx context.Context, request *auth.UpdateUserEmailRequest) (*auth.UserEmail, error) {
|
||||
email, err := s.repo.ChangeMyEmail(ctx, updateEmailToModel(ctx, request))
|
||||
if err != nil {
|
||||
|
@@ -5,8 +5,7 @@ import (
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
|
||||
grpc_util "github.com/caos/zitadel/internal/api/grpc"
|
||||
"github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/pkg/grpc/management"
|
||||
)
|
||||
@@ -19,8 +18,8 @@ func (s *Server) GetUserByID(ctx context.Context, id *management.UserID) (*manag
|
||||
return userViewFromModel(user), nil
|
||||
}
|
||||
|
||||
func (s *Server) GetUserByEmailGlobal(ctx context.Context, email *management.Email) (*management.UserView, error) {
|
||||
user, err := s.user.GetGlobalUserByEmail(ctx, email.Email)
|
||||
func (s *Server) GetUserByLoginNameGlobal(ctx context.Context, loginName *management.LoginName) (*management.UserView, error) {
|
||||
user, err := s.user.GetUserByLoginNameGlobal(ctx, loginName.LoginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -29,8 +28,7 @@ func (s *Server) GetUserByEmailGlobal(ctx context.Context, email *management.Ema
|
||||
|
||||
func (s *Server) SearchUsers(ctx context.Context, in *management.UserSearchRequest) (*management.UserSearchResponse, error) {
|
||||
request := userSearchRequestsToModel(in)
|
||||
orgID := grpc_util.GetHeader(ctx, http.ZitadelOrgID)
|
||||
request.AppendMyOrgQuery(orgID)
|
||||
request.AppendMyOrgQuery(authz.GetCtxData(ctx).OrgID)
|
||||
response, err := s.user.SearchUsers(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -106,6 +104,10 @@ func (s *Server) GetUserProfile(ctx context.Context, in *management.UserID) (*ma
|
||||
return profileViewFromModel(profile), nil
|
||||
}
|
||||
|
||||
func (s *Server) ChangeUserUserName(ctx context.Context, request *management.UpdateUserUserNameRequest) (*empty.Empty, error) {
|
||||
return &empty.Empty{}, s.user.ChangeUsername(ctx, request.Id, request.UserName)
|
||||
}
|
||||
|
||||
func (s *Server) UpdateUserProfile(ctx context.Context, request *management.UpdateUserProfileRequest) (*management.UserProfile, error) {
|
||||
profile, err := s.user.ChangeProfile(ctx, updateProfileToModel(request))
|
||||
if err != nil {
|
||||
|
@@ -59,6 +59,7 @@ func NewProvider(ctx context.Context, config OPHandlerConfig, repo repository.Re
|
||||
middleware.NoCacheInterceptor(http_utils.CopyHeadersToContext(handlerFunc))
|
||||
}
|
||||
}
|
||||
config.OPConfig.CodeMethodS256 = true
|
||||
provider, err := op.NewDefaultOP(
|
||||
ctx,
|
||||
config.OPConfig,
|
||||
|
@@ -239,8 +239,11 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthR
|
||||
if !user.IsEmailVerified {
|
||||
steps = append(steps, &model.VerifyEMailStep{})
|
||||
}
|
||||
if user.UsernameChangeRequired {
|
||||
steps = append(steps, &model.ChangeUsernameStep{})
|
||||
}
|
||||
|
||||
if user.PasswordChangeRequired || !user.IsEmailVerified {
|
||||
if user.PasswordChangeRequired || !user.IsEmailVerified || user.UsernameChangeRequired {
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
|
@@ -220,6 +220,14 @@ func (repo *UserRepo) RemoveMyMfaOTP(ctx context.Context) error {
|
||||
return repo.UserEvents.RemoveOTP(ctx, authz.GetCtxData(ctx).UserID)
|
||||
}
|
||||
|
||||
func (repo *UserRepo) ChangeMyUsername(ctx context.Context, username string) error {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
orgPolicy, err := repo.OrgEvents.GetOrgIAMPolicy(ctx, ctxData.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.UserEvents.ChangeUsername(ctx, ctxData.UserID, username, orgPolicy)
|
||||
}
|
||||
func (repo *UserRepo) ResendInitVerificationMail(ctx context.Context, userID string) error {
|
||||
_, err := repo.UserEvents.CreateInitializeUserCodeByID(ctx, userID)
|
||||
return err
|
||||
@@ -299,6 +307,15 @@ func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, li
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (repo *UserRepo) ChangeUsername(ctx context.Context, userID, username string) error {
|
||||
policyResourceOwner := authz.GetCtxData(ctx).OrgID
|
||||
orgPolicy, err := repo.OrgEvents.GetOrgIAMPolicy(ctx, policyResourceOwner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.UserEvents.ChangeUsername(ctx, userID, username, orgPolicy)
|
||||
}
|
||||
|
||||
func checkIDs(ctx context.Context, obj es_models.ObjectRoot) error {
|
||||
if obj.AggregateID != authz.GetCtxData(ctx).UserID {
|
||||
return errors.ThrowPermissionDenied(nil, "EVENT-kFi9w", "object does not belong to user")
|
||||
|
@@ -83,7 +83,8 @@ func (u *User) ProcessUser(event *models.Event) (err error) {
|
||||
return err
|
||||
}
|
||||
err = user.AppendEvent(event)
|
||||
case es_model.DomainClaimed:
|
||||
case es_model.DomainClaimed,
|
||||
es_model.UserUserNameChanged:
|
||||
user, err = u.view.UserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -65,7 +65,9 @@ func (u *UserSession) Reduce(event *models.Event) (err error) {
|
||||
es_model.MfaOtpRemoved,
|
||||
es_model.UserProfileChanged,
|
||||
es_model.UserLocked,
|
||||
es_model.UserDeactivated:
|
||||
es_model.UserDeactivated,
|
||||
es_model.DomainClaimed,
|
||||
es_model.UserUserNameChanged:
|
||||
sessions, err := u.view.UserSessionsByUserID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -35,8 +35,8 @@ func (v *View) SearchUsers(request *usr_model.UserSearchRequest) ([]*model.UserV
|
||||
return view.SearchUsers(v.Db, userTable, request)
|
||||
}
|
||||
|
||||
func (v *View) GetGlobalUserByEmail(email string) (*model.UserView, error) {
|
||||
return view.GetGlobalUserByEmail(v.Db, userTable, email)
|
||||
func (v *View) GetGlobalUserByLoginName(email string) (*model.UserView, error) {
|
||||
return view.GetGlobalUserByLoginName(v.Db, userTable, email)
|
||||
}
|
||||
|
||||
func (v *View) IsUserUnique(userName, email string) (bool, error) {
|
||||
|
@@ -15,4 +15,5 @@ type Repository interface {
|
||||
UserGrantRepository
|
||||
PolicyRepository
|
||||
OrgRepository
|
||||
IAMRepository
|
||||
}
|
||||
|
@@ -21,9 +21,14 @@ type UserRepository interface {
|
||||
VerifyEmail(ctx context.Context, userID, code string) error
|
||||
ResendEmailVerificationMail(ctx context.Context, userID string) error
|
||||
|
||||
VerifyInitCode(ctx context.Context, userID, code, password string) error
|
||||
ResendInitVerificationMail(ctx context.Context, userID string) error
|
||||
|
||||
AddMfaOTP(ctx context.Context, userID string) (*model.OTP, error)
|
||||
VerifyMfaOTPSetup(ctx context.Context, userID, code string) error
|
||||
|
||||
ChangeUsername(ctx context.Context, userID, username string) error
|
||||
|
||||
SignOut(ctx context.Context, agentID string) error
|
||||
|
||||
UserByID(ctx context.Context, userID string) (*model.UserView, error)
|
||||
@@ -56,5 +61,7 @@ type myUserRepo interface {
|
||||
VerifyMyMfaOTPSetup(ctx context.Context, code string) error
|
||||
RemoveMyMfaOTP(ctx context.Context) error
|
||||
|
||||
ChangeMyUsername(ctx context.Context, username string) error
|
||||
|
||||
MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error)
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ const (
|
||||
NextStepMfaPrompt
|
||||
NextStepMfaVerify
|
||||
NextStepRedirectToCallback
|
||||
NextStepChangeUsername
|
||||
)
|
||||
|
||||
type UserSessionState int32
|
||||
@@ -74,6 +75,12 @@ func (s *InitPasswordStep) Type() NextStepType {
|
||||
return NextStepInitPassword
|
||||
}
|
||||
|
||||
type ChangeUsernameStep struct{}
|
||||
|
||||
func (s *ChangeUsernameStep) Type() NextStepType {
|
||||
return NextStepChangeUsername
|
||||
}
|
||||
|
||||
type VerifyEMailStep struct{}
|
||||
|
||||
func (s *VerifyEMailStep) Type() NextStepType {
|
||||
|
@@ -81,6 +81,7 @@ type Endpoints struct {
|
||||
InitCode string
|
||||
PasswordReset string
|
||||
VerifyEmail string
|
||||
DomainClaimed string
|
||||
}
|
||||
|
||||
type Providers struct {
|
||||
@@ -94,4 +95,5 @@ type TemplateData struct {
|
||||
PasswordReset templates.TemplateData
|
||||
VerifyEmail templates.TemplateData
|
||||
VerifyPhone templates.TemplateData
|
||||
DomainClaimed templates.TemplateData
|
||||
}
|
||||
|
@@ -132,8 +132,8 @@ func (repo *UserRepo) UserChanges(ctx context.Context, id string, lastSequence u
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (repo *UserRepo) GetGlobalUserByEmail(ctx context.Context, email string) (*usr_model.UserView, error) {
|
||||
user, err := repo.View.GetGlobalUserByEmail(email)
|
||||
func (repo *UserRepo) GetUserByLoginNameGlobal(ctx context.Context, loginName string) (*usr_model.UserView, error) {
|
||||
user, err := repo.View.GetGlobalUserByLoginName(loginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -179,6 +179,14 @@ func (repo *UserRepo) ChangeProfile(ctx context.Context, profile *usr_model.Prof
|
||||
return repo.UserEvents.ChangeProfile(ctx, profile)
|
||||
}
|
||||
|
||||
func (repo *UserRepo) ChangeUsername(ctx context.Context, userID, userName string) error {
|
||||
orgPolicy, err := repo.OrgEvents.GetOrgIAMPolicy(ctx, authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.UserEvents.ChangeUsername(ctx, userID, userName, orgPolicy)
|
||||
}
|
||||
|
||||
func (repo *UserRepo) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) {
|
||||
user, err := repo.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
|
@@ -80,7 +80,8 @@ func (u *User) ProcessUser(event *models.Event) (err error) {
|
||||
return err
|
||||
}
|
||||
err = user.AppendEvent(event)
|
||||
case es_model.DomainClaimed:
|
||||
case es_model.DomainClaimed,
|
||||
es_model.UserUserNameChanged:
|
||||
user, err = u.view.UserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -19,8 +19,8 @@ func (v *View) SearchUsers(request *usr_model.UserSearchRequest) ([]*model.UserV
|
||||
return view.SearchUsers(v.Db, userTable, request)
|
||||
}
|
||||
|
||||
func (v *View) GetGlobalUserByEmail(email string) (*model.UserView, error) {
|
||||
return view.GetGlobalUserByEmail(v.Db, userTable, email)
|
||||
func (v *View) GetGlobalUserByLoginName(loginName string) (*model.UserView, error) {
|
||||
return view.GetGlobalUserByLoginName(v.Db, userTable, loginName)
|
||||
}
|
||||
|
||||
func (v *View) UsersByOrgID(orgID string) ([]*model.UserView, error) {
|
||||
|
@@ -16,7 +16,7 @@ type UserRepository interface {
|
||||
UnlockUser(ctx context.Context, id string) (*model.User, error)
|
||||
SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error)
|
||||
UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool) (*model.UserChanges, error)
|
||||
GetGlobalUserByEmail(ctx context.Context, email string) (*model.UserView, error)
|
||||
GetUserByLoginNameGlobal(ctx context.Context, email string) (*model.UserView, error)
|
||||
IsUserUnique(ctx context.Context, userName, email string) (bool, error)
|
||||
UserMfas(ctx context.Context, userID string) ([]*model.MultiFactor, error)
|
||||
|
||||
@@ -26,6 +26,8 @@ type UserRepository interface {
|
||||
ProfileByID(ctx context.Context, userID string) (*model.Profile, error)
|
||||
ChangeProfile(ctx context.Context, profile *model.Profile) (*model.Profile, error)
|
||||
|
||||
ChangeUsername(ctx context.Context, id, username string) error
|
||||
|
||||
EmailByID(ctx context.Context, userID string) (*model.Email, error)
|
||||
ChangeEmail(ctx context.Context, email *model.Email) (*model.Email, error)
|
||||
CreateEmailVerificationCode(ctx context.Context, userID string) error
|
||||
|
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/caos/logging"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
@@ -56,6 +58,8 @@ func (n *Notification) Reduce(event *models.Event) (err error) {
|
||||
err = n.handlePhoneVerificationCode(event)
|
||||
case es_model.UserPasswordCodeAdded:
|
||||
err = n.handlePasswordCode(event)
|
||||
case es_model.DomainClaimed:
|
||||
err = n.handleDomainClaimed(event)
|
||||
default:
|
||||
return n.view.ProcessedNotificationSequence(event.Sequence)
|
||||
}
|
||||
@@ -137,6 +141,27 @@ func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err err
|
||||
return n.userEvents.PhoneVerificationCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handleDomainClaimed(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.DomainClaimed, es_model.DomainClaimedSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
data := make(map[string]string)
|
||||
if err := json.Unmarshal(event.Data, &data); err != nil {
|
||||
logging.Log("HANDLE-Gghq2").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(err, "HANDLE-7hgj3", "could not unmarshal event")
|
||||
}
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendDomainClaimed(n.statikDir, n.i18n, user, data["userName"], n.systemDefaults)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.DomainClaimedSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) checkIfCodeAlreadyHandled(userID string, sequence uint64, addedType, sentType models.EventType) (bool, error) {
|
||||
events, err := n.getUserEvents(userID, sequence)
|
||||
if err != nil {
|
||||
|
@@ -69,6 +69,17 @@ func (u *NotifyUser) ProcessUser(event *models.Event) (err error) {
|
||||
return err
|
||||
}
|
||||
err = user.AppendEvent(event)
|
||||
case es_model.DomainClaimed,
|
||||
es_model.UserUserNameChanged:
|
||||
user, err = u.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = user.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.fillLoginNames(user)
|
||||
case es_model.UserRemoved:
|
||||
err = u.view.DeleteNotifyUser(event.AggregateID, event.Sequence)
|
||||
default:
|
||||
|
@@ -26,4 +26,10 @@ VerifyPhone:
|
||||
Greeting: Hallo {{.FirstName}} {{.LastName}},
|
||||
Text: Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst {{.Code}}
|
||||
ButtonText: Telefon verifizieren
|
||||
|
||||
DomainClaimed:
|
||||
Title: Zitadel - Domain wurde beansprucht
|
||||
PreHeader: Email / Username ändern
|
||||
Subject: Domain wurde beansprucht
|
||||
Greeting: Hallo {{.FirstName}} {{.LastName}},
|
||||
Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt.
|
||||
ButtonText: Login
|
||||
|
@@ -26,4 +26,10 @@ VerifyPhone:
|
||||
Greeting: Hello {{.FirstName}} {{.LastName}},
|
||||
Text: A new phonenumber has been added. Please use the following code to verify it {{.Code}}
|
||||
ButtonText: Verify phone
|
||||
|
||||
DomainClaimed:
|
||||
Title: Zitadel - Domain has been claimed
|
||||
PreHeader: Change email / username
|
||||
Subject: Domain has been claimed
|
||||
Greeting: Hello {{.FirstName}} {{.LastName}},
|
||||
Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.Username}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login.
|
||||
ButtonText: Login
|
||||
|
37
internal/notification/types/domain_claimed.go
Normal file
37
internal/notification/types/domain_claimed.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/i18n"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type DomainClaimedData struct {
|
||||
templates.TemplateData
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendDomainClaimed(dir http.FileSystem, i18n *i18n.Translator, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults) error {
|
||||
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.DomainClaimed, &UrlData{UserID: user.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var args = map[string]interface{}{
|
||||
"FirstName": user.FirstName,
|
||||
"LastName": user.LastName,
|
||||
"Username": user.LastEmail,
|
||||
"TempUsername": username,
|
||||
"Domain": strings.Split(user.LastEmail, "@")[1],
|
||||
}
|
||||
systemDefaults.Notifications.TemplateData.DomainClaimed.Translate(i18n, args, user.PreferredLanguage)
|
||||
data := &DomainClaimedData{TemplateData: systemDefaults.Notifications.TemplateData.DomainClaimed, URL: url}
|
||||
template, err := templates.GetParsedTemplate(dir, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(user, systemDefaults.Notifications.TemplateData.DomainClaimed.Subject, template, systemDefaults.Notifications, true)
|
||||
}
|
@@ -8,7 +8,7 @@ Errors:
|
||||
OrgIamPolicyNil: Organisation Policy is empty
|
||||
EmailAsUsernameNotAllowed: Email is not allowed as username
|
||||
Invalid: Userdata is invalid
|
||||
DomainNotAllowedAsUsername: Domain is already reserved
|
||||
DomainNotAllowedAsUsername: Domain is already reserved and cannot be used
|
||||
AlreadyInactive: User already inactive
|
||||
NotInactive: User is not inactive
|
||||
ShouldBeActiveOrInitial: User is not active or inital
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/api/http/middleware"
|
||||
auth_repository "github.com/caos/zitadel/internal/auth/repository"
|
||||
"github.com/caos/zitadel/internal/auth/repository/eventsourcing"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/form"
|
||||
@@ -23,7 +24,7 @@ type Login struct {
|
||||
router http.Handler
|
||||
renderer *Renderer
|
||||
parser *form.Parser
|
||||
authRepo *eventsourcing.EsRepository
|
||||
authRepo auth_repository.Repository
|
||||
zitadelURL string
|
||||
oidcAuthCallbackURL string
|
||||
}
|
||||
|
@@ -53,6 +53,8 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str
|
||||
tmplRegister: "register.html",
|
||||
tmplLogoutDone: "logout_done.html",
|
||||
tmplRegisterOrg: "register_org.html",
|
||||
tmplChangeUsername: "change_username.html",
|
||||
tmplChangeUsernameDone: "change_username_done.html",
|
||||
}
|
||||
funcs := map[string]interface{}{
|
||||
"resourceUrl": func(file string) string {
|
||||
@@ -112,6 +114,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str
|
||||
"orgRegistrationUrl": func() string {
|
||||
return path.Join(r.pathPrefix, EndpointRegisterOrg)
|
||||
},
|
||||
"changeUsernameUrl": func() string {
|
||||
return path.Join(r.pathPrefix, EndpointChangeUsername)
|
||||
},
|
||||
"selectedLanguage": func(l string) bool {
|
||||
return false
|
||||
},
|
||||
@@ -179,6 +184,8 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
|
||||
l.renderMfaPrompt(w, r, authReq, step, err)
|
||||
case *model.InitUserStep:
|
||||
l.renderInitUser(w, r, authReq, "", "", step.PasswordSet, nil)
|
||||
case *model.ChangeUsernameStep:
|
||||
l.renderChangeUsername(w, r, authReq, nil)
|
||||
default:
|
||||
l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-ds3QF", "step no possible"))
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ const (
|
||||
EndpointLogin = "/login"
|
||||
EndpointLoginName = "/loginname"
|
||||
EndpointUserSelection = "/userselection"
|
||||
EndpointChangeUsername = "/username/change"
|
||||
EndpointPassword = "/password"
|
||||
EndpointInitPassword = "/password/init"
|
||||
EndpointChangePassword = "/password/change"
|
||||
@@ -40,6 +41,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
|
||||
router.HandleFunc(EndpointLoginName, login.handleLoginName).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointLoginName, login.handleLoginNameCheck).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointUserSelection, login.handleSelectUser).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointChangeUsername, login.handleChangeUsername).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointPassword, login.handlePasswordCheck).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointInitPassword, login.handleInitPassword).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointInitPassword, login.handleInitPasswordCheck).Methods(http.MethodPost)
|
||||
|
46
internal/ui/login/handler/username_change_handler.go
Normal file
46
internal/ui/login/handler/username_change_handler.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/caos/zitadel/internal/auth_request/model"
|
||||
)
|
||||
|
||||
const (
|
||||
tmplChangeUsername = "changeusername"
|
||||
tmplChangeUsernameDone = "changeusernamedone"
|
||||
)
|
||||
|
||||
type changeUsernameData struct {
|
||||
Username string `schema:"username"`
|
||||
}
|
||||
|
||||
func (l *Login) renderChangeUsername(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest, err error) {
|
||||
var errType, errMessage string
|
||||
if err != nil {
|
||||
errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
data := l.getUserData(r, authReq, "Change Username", errType, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplChangeUsername], data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handleChangeUsername(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(changeUsernameData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.authRepo.ChangeUsername(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Username)
|
||||
if err != nil {
|
||||
l.renderChangeUsername(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderChangeUsernameDone(w, r, authReq)
|
||||
}
|
||||
|
||||
func (l *Login) renderChangeUsernameDone(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) {
|
||||
var errType, errMessage string
|
||||
data := l.getUserData(r, authReq, "Username Change Done", errType, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, l.renderer.Templates[tmplChangeUsernameDone], data, nil)
|
||||
}
|
@@ -21,6 +21,15 @@ UserSelection:
|
||||
SessionState0: aktiv
|
||||
SessionState1: inaktiv
|
||||
|
||||
UsernameChange:
|
||||
Title: Usernamen ändern
|
||||
Description: Wähle deinen neuen Benutzernamen
|
||||
Username: Benutzernamen
|
||||
|
||||
UsernameChangeDone:
|
||||
Title: Username geändert
|
||||
Description: Der Username wurde erfolgreich geändert.
|
||||
|
||||
MfaVerify:
|
||||
Title: Multifaktor verifizieren
|
||||
Description: Verifiziere deinen Multifaktor
|
||||
@@ -153,6 +162,7 @@ Errors:
|
||||
NotMatchingUserID: User stimm nicht mit User in Auth Request überein
|
||||
UserIDMissing: UserID ist leer
|
||||
Invalid: Userdaten sind ungültig
|
||||
DomainNotAllowedAsUsername: Domäne ist bereits reserviert und kann nicht verwendet werden
|
||||
Password:
|
||||
ConfirmationWrong: Passwort Bestätigung stimmt nicht überein
|
||||
Empty: Passwort ist leer
|
||||
|
@@ -21,6 +21,15 @@ Password:
|
||||
HasNumber: Number
|
||||
HasSymbol: Symbol
|
||||
|
||||
UsernameChange:
|
||||
Title: Change Username
|
||||
Description: Set your new username
|
||||
Username: Username
|
||||
|
||||
UsernameChangeDone:
|
||||
Title: Username changed
|
||||
Description: Your username was changed successfully.
|
||||
|
||||
MfaVerify:
|
||||
Title: Verify Multificator
|
||||
Description: Verify your multifactor
|
||||
@@ -155,6 +164,7 @@ Errors:
|
||||
NotMatchingUserID: User and user in authrequest don't match
|
||||
UserIDMissing: UserID is empty
|
||||
Invalid: Invalid userdata
|
||||
DomainNotAllowedAsUsername: Domain is already reserved and cannot be used
|
||||
Password:
|
||||
ConfirmationWrong: Passwordconfirmation is wrong
|
||||
Empty: Password is empty
|
||||
|
@@ -2,6 +2,7 @@ function disableSubmit(checks, button) {
|
||||
let form = document.getElementsByTagName('form')[0];
|
||||
let inputs = form.getElementsByTagName('input');
|
||||
for (i = 0; i < inputs.length; i++) {
|
||||
button.disabled = true;
|
||||
inputs[i].addEventListener('input', function () {
|
||||
if (checks != undefined) {
|
||||
if (checks() === false) {
|
||||
|
@@ -41,7 +41,7 @@
|
||||
{{ template "error-message" .}}
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" id="change-password-button" name="resend" value="false" class="primary right" disabled>{{t "Actions.Next"}}</buttontype="submit">
|
||||
<button type="submit" id="change-password-button" name="resend" value="false" class="primary right">{{t "Actions.Next"}}</button>
|
||||
<a href="{{ loginUrl }}">
|
||||
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
|
||||
</a>
|
||||
|
35
internal/ui/login/static/templates/change_username.html
Normal file
35
internal/ui/login/static/templates/change_username.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
{{ template "user-profile" . }}
|
||||
|
||||
<p>{{t "UsernameChange.Description"}}</p>
|
||||
|
||||
<form action="{{ changeUsernameUrl }}" method="POST">
|
||||
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<label class="label" for="username">{{t "UsernameChange.Username"}}</label>
|
||||
<input class="input" type="text" id="username" name="username" autocomplete="username" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ template "error-message" .}}
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" id="submit-button" value="false" class="primary right">{{t "Actions.Next"}}</button>
|
||||
<a href="{{ loginUrl }}">
|
||||
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
|
||||
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
|
||||
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
|
21
internal/ui/login/static/templates/change_username_done.html
Normal file
21
internal/ui/login/static/templates/change_username_done.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
{{ template "user-profile" . }}
|
||||
|
||||
<p>{{t "UsernameChangeDone.Description"}}</p>
|
||||
|
||||
<form action="{{ loginUrl }}" method="POST">
|
||||
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary right" type="submit">{{t "Actions.Next"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
|
@@ -47,8 +47,7 @@
|
||||
id="init-button"
|
||||
name="resend"
|
||||
value="false"
|
||||
class="primary right"
|
||||
{{ if not .PasswordSet }} disabled {{ end }}>{{t "Actions.Next"}}</button>
|
||||
class="primary right">{{t "Actions.Next"}}</button>
|
||||
<button type="submit" name="resend" value="true" class="secondary right" formnovalidate>{{t "Actions.Resend" }}</button>
|
||||
<a href="{{ loginUrl }}">
|
||||
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
|
||||
|
@@ -20,7 +20,7 @@
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary right" id="submit-button" type="submit" disabled>{{t "Actions.Next"}}</button>
|
||||
<button class="primary right" id="submit-button" type="submit">{{t "Actions.Next"}}</button>
|
||||
<button class="secondary right" name="register" value="true" formnovalidate>{{t "Actions.Register"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@@ -21,7 +21,7 @@
|
||||
{{ template "error-message" .}}
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" id="submit-button" name="resend" value="false" class="primary right" disabled>{{t "Actions.Next"}}</button>
|
||||
<button type="submit" id="submit-button" name="resend" value="false" class="primary right">{{t "Actions.Next"}}</button>
|
||||
{{ if .UserID }}
|
||||
<button type="submit" name="resend" value="true" class="secondary right" formnovalidate>{{t "Actions.Resend"}}</button>
|
||||
{{ end }}
|
||||
|
@@ -34,7 +34,7 @@
|
||||
{{end}}
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary right" id="submit-button" type="submit" disabled>{{t "Actions.Next"}}</button>
|
||||
<button class="primary right" id="submit-button" type="submit">{{t "Actions.Next"}}</button>
|
||||
<a href="{{ mfaPromptChangeUrl .AuthReqID .MfaType }}">
|
||||
<button class="secondary" type="button">{{t "Actions.Back"}}</button>
|
||||
</a>
|
||||
|
@@ -21,7 +21,7 @@
|
||||
{{ template "error-message" .}}
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary right" id="submit-button" type="submit" disabled>{{t "Actions.Next"}}</button>
|
||||
<button class="primary right" id="submit-button" type="submit">{{t "Actions.Next"}}</button>
|
||||
<a href="{{ loginUrl }}">
|
||||
<button class="secondary" type="button">{{t "Actions.Cancel"}}</button>
|
||||
</a>
|
||||
|
@@ -21,7 +21,7 @@
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="actions">
|
||||
<button id="submit-button" class="primary right" type="submit" disabled>{{t "Actions.Next"}}</button>
|
||||
<button id="submit-button" class="primary right" type="submit">{{t "Actions.Next"}}</button>
|
||||
<a href="{{ loginNameChangeUrl .AuthReqID }}">
|
||||
<button class="secondary" type="button">{{t "Actions.Back"}}</button>
|
||||
</a>
|
||||
|
@@ -61,7 +61,7 @@
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary right" id="register-button" type="submit" disabled>{{t "Actions.Next"}}</button>
|
||||
<button class="primary right" id="register-button" type="submit">{{t "Actions.Next"}}</button>
|
||||
<a href="{{ loginNameChangeUrl .AuthReqID }}">
|
||||
<button class="secondary" type="button">{{t "Actions.Back"}}</button>
|
||||
</a>
|
||||
|
@@ -59,7 +59,7 @@
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary right" id="register-button" type="submit" disabled>{{t "Actions.Save"}}</button>
|
||||
<button class="primary right" id="register-button" type="submit">{{t "Actions.Save"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@@ -1,10 +1,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"golang.org/x/text/language"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
|
||||
req_model "github.com/caos/zitadel/internal/auth_request/model"
|
||||
"github.com/caos/zitadel/internal/model"
|
||||
)
|
||||
@@ -17,6 +19,7 @@ type UserView struct {
|
||||
ResourceOwner string
|
||||
PasswordSet bool
|
||||
PasswordChangeRequired bool
|
||||
UsernameChangeRequired bool
|
||||
PasswordChanged time.Time
|
||||
LastLogin time.Time
|
||||
UserName string
|
||||
|
@@ -670,11 +670,11 @@ func (es *UserEventstore) ChangeEmail(ctx context.Context, email *usr_model.Emai
|
||||
repoNew := model.EmailFromModel(email)
|
||||
repoEmailCode := model.EmailCodeFromModel(emailCode)
|
||||
|
||||
updateAggregates, err := EmailChangeAggregate(ctx, es.AggregateCreator(), repoExisting, repoNew, repoEmailCode)
|
||||
updateAggregate, err := EmailChangeAggregate(ctx, es.AggregateCreator(), repoExisting, repoNew, repoEmailCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregates...)
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1112,6 +1112,48 @@ func (es *UserEventstore) PrepareDomainClaimed(ctx context.Context, userIDs []st
|
||||
return aggregates, nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) DomainClaimedSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-0posw", "Errors.User.UserIDMissing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := DomainClaimedSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ChangeUsername(ctx context.Context, userID, username string, orgIamPolicy *org_model.OrgIAMPolicy) error {
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldUsername := user.UserName
|
||||
user.UserName = username
|
||||
if err := user.CheckOrgIAMPolicy(orgIamPolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
repoUser := model.UserFromModel(user)
|
||||
aggregates, err := UsernameChangedAggregates(ctx, es.AggregateCreator(), repoUser, oldUsername, orgIamPolicy.UserLoginMustBeDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoUser.AppendEvents, aggregates...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) generateTemporaryLoginName() (string, error) {
|
||||
id, err := es.idGenerator.Next()
|
||||
if err != nil {
|
||||
|
@@ -5,7 +5,6 @@ import "github.com/caos/zitadel/internal/eventstore/models"
|
||||
const (
|
||||
UserAggregate models.AggregateType = "user"
|
||||
UserUserNameAggregate models.AggregateType = "user.username"
|
||||
UserEmailAggregate models.AggregateType = "user.email"
|
||||
|
||||
UserAdded models.EventType = "user.added"
|
||||
UserRegistered models.EventType = "user.selfregistered"
|
||||
@@ -16,8 +15,6 @@ const (
|
||||
|
||||
UserUserNameReserved models.EventType = "user.username.reserved"
|
||||
UserUserNameReleased models.EventType = "user.username.released"
|
||||
UserEmailReserved models.EventType = "user.email.reserved"
|
||||
UserEmailReleased models.EventType = "user.email.released"
|
||||
|
||||
UserLocked models.EventType = "user.locked"
|
||||
UserUnlocked models.EventType = "user.unlocked"
|
||||
@@ -44,8 +41,9 @@ const (
|
||||
UserPhoneCodeAdded models.EventType = "user.phone.code.added"
|
||||
UserPhoneCodeSent models.EventType = "user.phone.code.sent"
|
||||
|
||||
UserProfileChanged models.EventType = "user.profile.changed"
|
||||
UserAddressChanged models.EventType = "user.address.changed"
|
||||
UserProfileChanged models.EventType = "user.profile.changed"
|
||||
UserAddressChanged models.EventType = "user.address.changed"
|
||||
UserUserNameChanged models.EventType = "user.username.changed"
|
||||
|
||||
MfaOtpAdded models.EventType = "user.mfa.otp.added"
|
||||
MfaOtpVerified models.EventType = "user.mfa.otp.verified"
|
||||
@@ -56,5 +54,6 @@ const (
|
||||
|
||||
SignedOut models.EventType = "user.signed.out"
|
||||
|
||||
DomainClaimed models.EventType = "user.domain.claimed"
|
||||
DomainClaimed models.EventType = "user.domain.claimed"
|
||||
DomainClaimedSent models.EventType = "user.domain.claimed.sent"
|
||||
)
|
||||
|
@@ -134,7 +134,8 @@ func (u *User) AppendEvent(event *es_models.Event) (err error) {
|
||||
case UserAdded,
|
||||
UserRegistered,
|
||||
UserProfileChanged,
|
||||
DomainClaimed:
|
||||
DomainClaimed,
|
||||
UserUserNameChanged:
|
||||
u.setData(event)
|
||||
case UserDeactivated:
|
||||
u.appendDeactivatedEvent()
|
||||
|
@@ -33,14 +33,6 @@ func UserUserNameUniqueQuery(userName string) *es_models.SearchQuery {
|
||||
SetLimit(1)
|
||||
}
|
||||
|
||||
func UserEmailUniqueQuery(email string) *es_models.SearchQuery {
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(model.UserEmailAggregate).
|
||||
AggregateIDFilter(email).
|
||||
OrderDesc().
|
||||
SetLimit(1)
|
||||
}
|
||||
|
||||
func UserAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User) (*es_models.Aggregate, error) {
|
||||
if user == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dis83", "Errors.Internal")
|
||||
@@ -111,11 +103,7 @@ func UserCreateAggregate(ctx context.Context, aggCreator *es_models.AggregateCre
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*es_models.Aggregate{
|
||||
agg,
|
||||
uniqueAggregates[0],
|
||||
uniqueAggregates[1],
|
||||
}, nil
|
||||
return append(uniqueAggregates, agg), nil
|
||||
}
|
||||
|
||||
func UserRegisterAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, resourceOwner string, initCode *model.InitUserCode, userLoginMustBeDomain bool) ([]*es_models.Aggregate, error) {
|
||||
@@ -148,11 +136,7 @@ func UserRegisterAggregate(ctx context.Context, aggCreator *es_models.AggregateC
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*es_models.Aggregate{
|
||||
agg,
|
||||
uniqueAggregates[0],
|
||||
uniqueAggregates[1],
|
||||
}, nil
|
||||
return append(uniqueAggregates, agg), nil
|
||||
}
|
||||
|
||||
func getUniqueUserAggregates(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, resourceOwner string, userLoginMustBeDomain bool) ([]*es_models.Aggregate, error) {
|
||||
@@ -161,13 +145,8 @@ func getUniqueUserAggregates(ctx context.Context, aggCreator *es_models.Aggregat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
emailAggregate, err := reservedUniqueEmailAggregate(ctx, aggCreator, resourceOwner, user.EmailAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*es_models.Aggregate{
|
||||
userNameAggregate,
|
||||
emailAggregate,
|
||||
}, nil
|
||||
}
|
||||
func reservedUniqueUserNameAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, resourceOwner, userName string, userLoginMustBeDomain bool) (*es_models.Aggregate, error) {
|
||||
@@ -206,36 +185,18 @@ func releasedUniqueUserNameAggregate(ctx context.Context, aggCreator *es_models.
|
||||
return aggregate.SetPrecondition(UserUserNameUniqueQuery(username), isEventValidation(aggregate, model.UserUserNameReleased)), nil
|
||||
}
|
||||
|
||||
func reservedUniqueEmailAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, resourceOwner, email string) (aggregate *es_models.Aggregate, err error) {
|
||||
aggregate, err = aggCreator.NewAggregate(ctx, email, model.UserEmailAggregate, model.UserVersion, 0)
|
||||
if resourceOwner != "" {
|
||||
aggregate, err = aggCreator.NewAggregate(ctx, email, model.UserEmailAggregate, model.UserVersion, 0, es_models.OverwriteResourceOwner(resourceOwner))
|
||||
}
|
||||
func changeUniqueUserNameAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, resourceOwner, oldUsername, username string, userLoginMustBeDomain bool) ([]*es_models.Aggregate, error) {
|
||||
aggregates := make([]*es_models.Aggregate, 2)
|
||||
var err error
|
||||
aggregates[0], err = releasedUniqueUserNameAggregate(ctx, aggCreator, resourceOwner, oldUsername)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregate, err = aggregate.AppendEvent(model.UserEmailReserved, nil)
|
||||
aggregates[1], err = reservedUniqueUserNameAggregate(ctx, aggCreator, resourceOwner, username, userLoginMustBeDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aggregate.SetPrecondition(UserEmailUniqueQuery(email), isEventValidation(aggregate, model.UserEmailReserved)), nil
|
||||
}
|
||||
|
||||
func releasedUniqueEmailAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, resourceOwner, email string) (aggregate *es_models.Aggregate, err error) {
|
||||
aggregate, err = aggCreator.NewAggregate(ctx, email, model.UserEmailAggregate, model.UserVersion, 0)
|
||||
if resourceOwner != "" {
|
||||
aggregate, err = aggCreator.NewAggregate(ctx, email, model.UserEmailAggregate, model.UserVersion, 0, es_models.OverwriteResourceOwner(resourceOwner))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregate, err = aggregate.AppendEvent(model.UserEmailReleased, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aggregate.SetPrecondition(UserEmailUniqueQuery(email), isEventValidation(aggregate, model.UserEmailReleased)), nil
|
||||
return aggregates, nil
|
||||
}
|
||||
|
||||
func UserDeactivateAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
@@ -401,7 +362,7 @@ func ProfileChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mo
|
||||
}
|
||||
}
|
||||
|
||||
func EmailChangeAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, existing *model.User, email *model.Email, code *model.EmailCode) ([]*es_models.Aggregate, error) {
|
||||
func EmailChangeAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, existing *model.User, email *model.Email, code *model.EmailCode) (*es_models.Aggregate, error) {
|
||||
if email == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dki8s", "Errors.Internal")
|
||||
}
|
||||
@@ -412,17 +373,6 @@ func EmailChangeAggregate(ctx context.Context, aggCreator *es_models.AggregateCr
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-s90pw", "Errors.NoChangesFound")
|
||||
}
|
||||
aggregates := make([]*es_models.Aggregate, 0, 4)
|
||||
reserveEmailAggregate, err := reservedUniqueEmailAggregate(ctx, aggCreator, "", email.EmailAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, reserveEmailAggregate)
|
||||
releaseEmailAggregate, err := releasedUniqueEmailAggregate(ctx, aggCreator, "", existing.EmailAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, releaseEmailAggregate)
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -446,7 +396,7 @@ func EmailChangeAggregate(ctx context.Context, aggCreator *es_models.AggregateCr
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return append(aggregates, agg), nil
|
||||
return agg, nil
|
||||
}
|
||||
|
||||
func EmailVerifiedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) es_sdk.AggregateFunc {
|
||||
@@ -673,7 +623,10 @@ func SignOutAggregates(aggCreator *es_models.AggregateCreator, existingUsers []*
|
||||
}
|
||||
|
||||
func DomainClaimedAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, existingUser *model.User, tempName string) ([]*es_models.Aggregate, error) {
|
||||
aggregates := make([]*es_models.Aggregate, 3)
|
||||
aggregates, err := changeUniqueUserNameAggregate(ctx, aggCreator, existingUser.ResourceOwner, existingUser.UserName, tempName, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAggregate, err := UserAggregateOverwriteContext(ctx, aggCreator, existingUser, existingUser.ResourceOwner, existingUser.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -682,18 +635,41 @@ func DomainClaimedAggregate(ctx context.Context, aggCreator *es_models.Aggregate
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates[0] = userAggregate
|
||||
releasedUniqueAggregate, err := releasedUniqueUserNameAggregate(ctx, aggCreator, existingUser.ResourceOwner, existingUser.UserName)
|
||||
return append(aggregates, userAggregate), nil
|
||||
}
|
||||
|
||||
func DomainClaimedSentAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.DomainClaimedSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func UsernameChangedAggregates(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, oldUsername string, userLoginMustBeDomain bool) ([]*es_models.Aggregate, error) {
|
||||
aggregates, err := changeUniqueUserNameAggregate(ctx, aggCreator, user.ResourceOwner, oldUsername, user.UserName, userLoginMustBeDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates[1] = releasedUniqueAggregate
|
||||
reservedUniqueAggregate, err := reservedUniqueUserNameAggregate(ctx, aggCreator, existingUser.ResourceOwner, tempName, false)
|
||||
userAggregate, err := UserAggregate(ctx, aggCreator, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates[2] = reservedUniqueAggregate
|
||||
return aggregates, nil
|
||||
userAggregate, err = userAggregate.AppendEvent(model.UserUserNameChanged, map[string]interface{}{"userName": user.UserName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !userLoginMustBeDomain {
|
||||
validationQuery := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(org_es_model.OrgAggregate).
|
||||
AggregateIDsFilter()
|
||||
|
||||
validation := addUserNameValidation(user.UserName)
|
||||
userAggregate.SetPrecondition(validationQuery, validation)
|
||||
}
|
||||
return append(aggregates, userAggregate), nil
|
||||
}
|
||||
|
||||
func isEventValidation(aggregate *es_models.Aggregate, eventType es_models.EventType) func(...*es_models.Event) error {
|
||||
|
@@ -136,7 +136,7 @@ func TestUserCreateAggregate(t *testing.T) {
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.UserAdded},
|
||||
checkData: []bool{true},
|
||||
aggregatesLen: 3,
|
||||
aggregatesLen: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -166,7 +166,7 @@ func TestUserCreateAggregate(t *testing.T) {
|
||||
eventLen: 2,
|
||||
eventTypes: []models.EventType{model.UserAdded, model.InitializedUserCodeAdded},
|
||||
checkData: []bool{true, true},
|
||||
aggregatesLen: 3,
|
||||
aggregatesLen: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -184,7 +184,7 @@ func TestUserCreateAggregate(t *testing.T) {
|
||||
eventLen: 2,
|
||||
eventTypes: []models.EventType{model.UserAdded, model.UserPhoneCodeAdded},
|
||||
checkData: []bool{true, true},
|
||||
aggregatesLen: 3,
|
||||
aggregatesLen: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -201,7 +201,7 @@ func TestUserCreateAggregate(t *testing.T) {
|
||||
eventLen: 2,
|
||||
eventTypes: []models.EventType{model.UserAdded, model.UserEmailVerified},
|
||||
checkData: []bool{true, false},
|
||||
aggregatesLen: 3,
|
||||
aggregatesLen: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -219,7 +219,7 @@ func TestUserCreateAggregate(t *testing.T) {
|
||||
eventLen: 2,
|
||||
eventTypes: []models.EventType{model.UserAdded, model.UserPhoneVerified},
|
||||
checkData: []bool{true, false},
|
||||
aggregatesLen: 3,
|
||||
aggregatesLen: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -231,17 +231,17 @@ func TestUserCreateAggregate(t *testing.T) {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.aggregatesLen, len(aggregates))
|
||||
}
|
||||
|
||||
if !tt.res.wantErr && len(aggregates[0].Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(aggregates[0].Events))
|
||||
if !tt.res.wantErr && len(aggregates[1].Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(aggregates[1].Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if !tt.res.wantErr && aggregates[0].Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], aggregates[0].Events[i].Type.String())
|
||||
if !tt.res.wantErr && aggregates[1].Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], aggregates[1].Events[i].Type.String())
|
||||
}
|
||||
if !tt.res.wantErr && tt.res.checkData[i] && aggregates[0].Events[i].Data == nil {
|
||||
if !tt.res.wantErr && tt.res.checkData[i] && aggregates[1].Events[i].Data == nil {
|
||||
t.Errorf("should have data in event")
|
||||
}
|
||||
if !tt.res.wantErr && !tt.res.checkData[i] && aggregates[0].Events[i].Data != nil {
|
||||
if !tt.res.wantErr && !tt.res.checkData[i] && aggregates[1].Events[i].Data != nil {
|
||||
t.Errorf("should not have data in event")
|
||||
}
|
||||
}
|
||||
@@ -352,14 +352,14 @@ func TestUserRegisterAggregate(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aggregates, err := UserRegisterAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.new, tt.args.resourceOwner, tt.args.initCode, false)
|
||||
|
||||
if tt.res.errFunc == nil && len(aggregates[0].Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(aggregates[0].Events))
|
||||
if tt.res.errFunc == nil && len(aggregates[1].Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(aggregates[1].Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && aggregates[0].Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], aggregates[0].Events[i].Type.String())
|
||||
if tt.res.errFunc == nil && aggregates[1].Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], aggregates[1].Events[i].Type.String())
|
||||
}
|
||||
if tt.res.errFunc == nil && aggregates[0].Events[i].Data == nil {
|
||||
if tt.res.errFunc == nil && aggregates[1].Events[i].Data == nil {
|
||||
t.Errorf("should have data in event")
|
||||
}
|
||||
}
|
||||
@@ -1226,16 +1226,16 @@ func TestChangeEmailAggregate(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aggregates, err := EmailChangeAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.existing, tt.args.email, tt.args.code)
|
||||
aggregate, err := EmailChangeAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.existing, tt.args.email, tt.args.code)
|
||||
|
||||
if tt.res.errFunc == nil && len(aggregates[2].Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(aggregates[1].Events))
|
||||
if tt.res.errFunc == nil && len(aggregate.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(aggregate.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && aggregates[2].Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], aggregates[1].Events[i].Type.String())
|
||||
if tt.res.errFunc == nil && aggregate.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], aggregate.Events[i].Type.String())
|
||||
}
|
||||
if tt.res.errFunc == nil && aggregates[2].Events[i].Data == nil {
|
||||
if tt.res.errFunc == nil && aggregate.Events[i].Data == nil {
|
||||
t.Errorf("should have data in event")
|
||||
}
|
||||
}
|
||||
|
@@ -36,6 +36,7 @@ type UserView struct {
|
||||
State int32 `json:"-" gorm:"column:user_state"`
|
||||
PasswordSet bool `json:"-" gorm:"column:password_set"`
|
||||
PasswordChangeRequired bool `json:"-" gorm:"column:password_change_required"`
|
||||
UsernameChangeRequired bool `json:"-" gorm:"column:username_change_required"`
|
||||
PasswordChanged time.Time `json:"-" gorm:"column:password_change"`
|
||||
LastLogin time.Time `json:"-" gorm:"column:last_login"`
|
||||
UserName string `json:"userName" gorm:"column:user_name"`
|
||||
@@ -72,6 +73,7 @@ func UserFromModel(user *model.UserView) *UserView {
|
||||
State: int32(user.State),
|
||||
PasswordSet: user.PasswordSet,
|
||||
PasswordChangeRequired: user.PasswordChangeRequired,
|
||||
UsernameChangeRequired: user.UsernameChangeRequired,
|
||||
PasswordChanged: user.PasswordChanged,
|
||||
LastLogin: user.LastLogin,
|
||||
UserName: user.UserName,
|
||||
@@ -109,6 +111,7 @@ func UserToModel(user *UserView) *model.UserView {
|
||||
State: model.UserState(user.State),
|
||||
PasswordSet: user.PasswordSet,
|
||||
PasswordChangeRequired: user.PasswordChangeRequired,
|
||||
UsernameChangeRequired: user.UsernameChangeRequired,
|
||||
PasswordChanged: user.PasswordChanged,
|
||||
LastLogin: user.LastLogin,
|
||||
PreferredLoginName: user.PreferredLoginName,
|
||||
@@ -181,8 +184,13 @@ func (u *UserView) AppendEvent(event *models.Event) (err error) {
|
||||
case es_model.UserPasswordChanged:
|
||||
err = u.setPasswordData(event)
|
||||
case es_model.UserProfileChanged,
|
||||
es_model.UserAddressChanged,
|
||||
es_model.DomainClaimed:
|
||||
es_model.UserAddressChanged:
|
||||
err = u.setData(event)
|
||||
case es_model.DomainClaimed:
|
||||
u.UsernameChangeRequired = true
|
||||
err = u.setData(event)
|
||||
case es_model.UserUserNameChanged:
|
||||
u.UsernameChangeRequired = false
|
||||
err = u.setData(event)
|
||||
case es_model.UserEmailChanged:
|
||||
u.IsEmailVerified = false
|
||||
|
@@ -94,9 +94,9 @@ func SearchUsers(db *gorm.DB, table string, req *usr_model.UserSearchRequest) ([
|
||||
return users, count, nil
|
||||
}
|
||||
|
||||
func GetGlobalUserByEmail(db *gorm.DB, table, email string) (*model.UserView, error) {
|
||||
func GetGlobalUserByLoginName(db *gorm.DB, table, loginName string) (*model.UserView, error) {
|
||||
user := new(model.UserView)
|
||||
query := repository.PrepareGetByKey(table, model.UserSearchKey(usr_model.UserSearchKeyEmail), email)
|
||||
query := repository.PrepareGetByQuery(table, &model.UserSearchQuery{Key: usr_model.UserSearchKeyLoginNames, Value: loginName, Method: global_model.SearchMethodListContains})
|
||||
err := query(db, user)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
return nil, caos_errs.ThrowNotFound(nil, "VIEW-8uWer", "Errors.User.NotFound")
|
||||
|
Reference in New Issue
Block a user