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:
Livio Amstutz
2020-08-27 17:18:23 +02:00
committed by GitHub
parent 3f714679d1
commit 34ec2508d3
73 changed files with 19105 additions and 17845 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -15,4 +15,5 @@ type Repository interface {
UserGrantRepository
PolicyRepository
OrgRepository
IAMRepository
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View 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)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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"))
}

View File

@@ -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)

View 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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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>

View 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" .}}

View 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" .}}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"
)

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -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

View File

@@ -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")