mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:57:31 +00:00
feat: user v3 contact email and phone (#8644)
# Which Problems Are Solved Endpoints to maintain email and phone contact on user v3 are not implemented. # How the Problems Are Solved Add 3 endpoints with SetContactEmail, VerifyContactEmail and ResendContactEmailCode. Add 3 endpoints with SetContactPhone, VerifyContactPhone and ResendContactPhoneCode. Refactor the logic how contact is managed in the user creation and update. # Additional Changes None # Additional Context - part of https://github.com/zitadel/zitadel/issues/6433 --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -6,17 +6,13 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
domain_schema "github.com/zitadel/zitadel/internal/domain/schema"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user/schemauser"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type CreateSchemaUser struct {
|
||||
Details *domain.ObjectDetails
|
||||
|
||||
SchemaID string
|
||||
schemaRevision uint64
|
||||
|
||||
@@ -25,9 +21,9 @@ type CreateSchemaUser struct {
|
||||
Data json.RawMessage
|
||||
|
||||
Email *Email
|
||||
ReturnCodeEmail string
|
||||
ReturnCodeEmail *string
|
||||
Phone *Phone
|
||||
ReturnCodePhone string
|
||||
ReturnCodePhone *string
|
||||
}
|
||||
|
||||
func (s *CreateSchemaUser) Valid(ctx context.Context, c *Commands) (err error) {
|
||||
@@ -99,44 +95,36 @@ func (c *Commands) getSchemaRoleForWrite(ctx context.Context, resourceOwner, use
|
||||
return domain_schema.RoleOwner, nil
|
||||
}
|
||||
|
||||
func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser, alg crypto.EncryptionAlgorithm) (err error) {
|
||||
func (c *Commands) CreateSchemaUser(ctx context.Context, user *CreateSchemaUser) (*domain.ObjectDetails, error) {
|
||||
if err := user.Valid(ctx, c); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writeModel, err := c.getSchemaUserExists(ctx, user.ResourceOwner, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if writeModel.Exists() {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.AlreadyExists")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userAgg := UserV3AggregateFromWriteModel(&writeModel.WriteModel)
|
||||
events := []eventstore.Command{
|
||||
schemauser.NewCreatedEvent(ctx,
|
||||
userAgg,
|
||||
user.SchemaID, user.schemaRevision, user.Data,
|
||||
),
|
||||
events, codeEmail, codePhone, err := writeModel.NewCreated(ctx,
|
||||
user.SchemaID,
|
||||
user.schemaRevision,
|
||||
user.Data,
|
||||
user.Email,
|
||||
user.Phone,
|
||||
func(ctx context.Context) (*EncryptedCode, error) {
|
||||
return c.newEmailCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Email != nil {
|
||||
events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, writeModel, events, userAgg, user.Email, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if codeEmail != "" {
|
||||
user.ReturnCodeEmail = &codeEmail
|
||||
}
|
||||
if user.Phone != nil {
|
||||
events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, writeModel, events, userAgg, user.Phone, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if codePhone != "" {
|
||||
user.ReturnCodePhone = &codePhone
|
||||
}
|
||||
|
||||
if err := c.pushAppendAndReduce(ctx, writeModel, events...); err != nil {
|
||||
return err
|
||||
}
|
||||
user.Details = writeModelToObjectDetails(&writeModel.WriteModel)
|
||||
return nil
|
||||
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
|
||||
}
|
||||
|
||||
func (c *Commands) DeleteSchemaUser(ctx context.Context, resourceOwner, id string) (*domain.ObjectDetails, error) {
|
||||
@@ -147,50 +135,38 @@ func (c *Commands) DeleteSchemaUser(ctx context.Context, resourceOwner, id strin
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !writeModel.Exists() {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-syHyCsGmvM", "Errors.User.NotFound")
|
||||
}
|
||||
if err := c.checkPermissionDeleteUser(ctx, writeModel.ResourceOwner, writeModel.AggregateID); err != nil {
|
||||
|
||||
events, err := writeModel.NewDelete(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.pushAppendAndReduce(ctx, writeModel,
|
||||
schemauser.NewDeletedEvent(ctx, UserV3AggregateFromWriteModel(&writeModel.WriteModel)),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModelToObjectDetails(&writeModel.WriteModel), nil
|
||||
|
||||
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
|
||||
}
|
||||
|
||||
type ChangeSchemaUser struct {
|
||||
Details *domain.ObjectDetails
|
||||
|
||||
SchemaID *string
|
||||
schemaWriteModel *UserSchemaWriteModel
|
||||
|
||||
ResourceOwner string
|
||||
ID string
|
||||
Data json.RawMessage
|
||||
|
||||
SchemaUser *SchemaUser
|
||||
|
||||
Email *Email
|
||||
ReturnCodeEmail string
|
||||
ReturnCodeEmail *string
|
||||
Phone *Phone
|
||||
ReturnCodePhone string
|
||||
ReturnCodePhone *string
|
||||
}
|
||||
|
||||
func (s *ChangeSchemaUser) Valid(ctx context.Context, c *Commands) (err error) {
|
||||
type SchemaUser struct {
|
||||
SchemaID string
|
||||
Data json.RawMessage
|
||||
}
|
||||
|
||||
func (s *ChangeSchemaUser) Valid() (err error) {
|
||||
if s.ID == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-gEJR1QOGHb", "Errors.IDMissing")
|
||||
}
|
||||
if s.SchemaID != nil {
|
||||
s.schemaWriteModel, err = c.getSchemaWriteModelByID(ctx, "", *s.SchemaID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !s.schemaWriteModel.Exists() {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-VLDTtxT3If", "Errors.UserSchema.NotExists")
|
||||
}
|
||||
}
|
||||
|
||||
if s.Email != nil && s.Email.Address != "" {
|
||||
if err := s.Email.Validate(); err != nil {
|
||||
return err
|
||||
@@ -206,92 +182,56 @@ func (s *ChangeSchemaUser) Valid(ctx context.Context, c *Commands) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ChangeSchemaUser) ValidData(ctx context.Context, c *Commands, existingUser *UserV3WriteModel) (err error) {
|
||||
// get role for permission check in schema through extension
|
||||
role, err := c.getSchemaRoleForWrite(ctx, existingUser.ResourceOwner, existingUser.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.schemaWriteModel == nil {
|
||||
s.schemaWriteModel, err = c.getSchemaWriteModelByID(ctx, "", existingUser.SchemaID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
schema, err := domain_schema.NewSchema(role, bytes.NewReader(s.schemaWriteModel.Schema))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if data not changed but a new schema or revision should be used
|
||||
data := s.Data
|
||||
if s.Data == nil {
|
||||
data = existingUser.Data
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
if err := json.Unmarshal(data, &v); err != nil {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-7o3ZGxtXUz", "Errors.User.Invalid")
|
||||
}
|
||||
|
||||
if err := schema.Validate(v); err != nil {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-SlKXqLSeL6", "Errors.UserSchema.Data.Invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commands) ChangeSchemaUser(ctx context.Context, user *ChangeSchemaUser, alg crypto.EncryptionAlgorithm) (err error) {
|
||||
if err := user.Valid(ctx, c); err != nil {
|
||||
return err
|
||||
func (c *Commands) ChangeSchemaUser(ctx context.Context, user *ChangeSchemaUser) (*domain.ObjectDetails, error) {
|
||||
if err := user.Valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writeModel, err := c.getSchemaUserWriteModelByID(ctx, user.ResourceOwner, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if !writeModel.Exists() {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.NotFound")
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Nn8CRVlkeZ", "Errors.User.NotFound")
|
||||
}
|
||||
|
||||
userAgg := UserV3AggregateFromWriteModel(&writeModel.WriteModel)
|
||||
events := make([]eventstore.Command, 0)
|
||||
if user.Data != nil || user.SchemaID != nil {
|
||||
if err := user.ValidData(ctx, c, writeModel); err != nil {
|
||||
return err
|
||||
}
|
||||
updateEvent := writeModel.NewUpdatedEvent(ctx,
|
||||
userAgg,
|
||||
user.schemaWriteModel.AggregateID,
|
||||
user.schemaWriteModel.SchemaRevision,
|
||||
user.Data,
|
||||
)
|
||||
if updateEvent != nil {
|
||||
events = append(events, updateEvent)
|
||||
}
|
||||
schemaID := writeModel.SchemaID
|
||||
if user.SchemaUser != nil && user.SchemaUser.SchemaID != "" {
|
||||
schemaID = user.SchemaUser.SchemaID
|
||||
}
|
||||
if user.Email != nil {
|
||||
events, user.ReturnCodeEmail, err = c.updateSchemaUserEmail(ctx, writeModel, events, userAgg, user.Email, alg)
|
||||
|
||||
var schemaWM *UserSchemaWriteModel
|
||||
if user.SchemaUser != nil {
|
||||
schemaWriteModel, err := c.getSchemaWriteModelByID(ctx, "", schemaID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Phone != nil {
|
||||
events, user.ReturnCodePhone, err = c.updateSchemaUserPhone(ctx, writeModel, events, userAgg, user.Phone, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
if !schemaWriteModel.Exists() {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-VLDTtxT3If", "Errors.UserSchema.NotExists")
|
||||
}
|
||||
schemaWM = schemaWriteModel
|
||||
}
|
||||
if len(events) == 0 {
|
||||
user.Details = writeModelToObjectDetails(&writeModel.WriteModel)
|
||||
return nil
|
||||
|
||||
events, codeEmail, codePhone, err := writeModel.NewUpdate(ctx,
|
||||
schemaWM,
|
||||
user.SchemaUser,
|
||||
user.Email,
|
||||
user.Phone,
|
||||
func(ctx context.Context) (*EncryptedCode, error) {
|
||||
return c.newEmailCode(ctx, c.eventstore.Filter, c.userEncryption) //nolint:staticcheck
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.pushAppendAndReduce(ctx, writeModel, events...); err != nil {
|
||||
return err
|
||||
|
||||
if codeEmail != "" {
|
||||
user.ReturnCodeEmail = &codeEmail
|
||||
}
|
||||
user.Details = writeModelToObjectDetails(&writeModel.WriteModel)
|
||||
return nil
|
||||
if codePhone != "" {
|
||||
user.ReturnCodePhone = &codePhone
|
||||
}
|
||||
return c.pushAppendAndReduceDetails(ctx, writeModel, events...)
|
||||
}
|
||||
|
||||
func (c *Commands) checkPermissionUpdateUserState(ctx context.Context, resourceOwner, userID string) error {
|
||||
@@ -368,7 +308,7 @@ func (c *Commands) ActivateSchemaUser(ctx context.Context, resourceOwner, id str
|
||||
if id == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-17XupGvxBJ", "Errors.IDMissing")
|
||||
}
|
||||
writeModel, err := c.getSchemaUserExists(ctx, "", id)
|
||||
writeModel, err := c.getSchemaUserExists(ctx, resourceOwner, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -386,65 +326,8 @@ func (c *Commands) ActivateSchemaUser(ctx context.Context, resourceOwner, id str
|
||||
return writeModelToObjectDetails(&writeModel.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) updateSchemaUserEmail(ctx context.Context, existing *UserV3WriteModel, events []eventstore.Command, agg *eventstore.Aggregate, email *Email, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) {
|
||||
if existing.Email == string(email.Address) {
|
||||
return events, plainCode, nil
|
||||
}
|
||||
|
||||
events = append(events, schemauser.NewEmailUpdatedEvent(ctx,
|
||||
agg,
|
||||
email.Address,
|
||||
))
|
||||
if email.Verified {
|
||||
events = append(events, schemauser.NewEmailVerifiedEvent(ctx, agg))
|
||||
} else {
|
||||
cryptoCode, err := c.newEmailCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if email.ReturnCode {
|
||||
plainCode = cryptoCode.Plain
|
||||
}
|
||||
events = append(events, schemauser.NewEmailCodeAddedEvent(ctx, agg,
|
||||
cryptoCode.Crypted,
|
||||
cryptoCode.Expiry,
|
||||
email.URLTemplate,
|
||||
email.ReturnCode,
|
||||
))
|
||||
}
|
||||
return events, plainCode, nil
|
||||
}
|
||||
|
||||
func (c *Commands) updateSchemaUserPhone(ctx context.Context, existing *UserV3WriteModel, events []eventstore.Command, agg *eventstore.Aggregate, phone *Phone, alg crypto.EncryptionAlgorithm) (_ []eventstore.Command, plainCode string, err error) {
|
||||
if existing.Phone == string(phone.Number) {
|
||||
return events, plainCode, nil
|
||||
}
|
||||
|
||||
events = append(events, schemauser.NewPhoneUpdatedEvent(ctx,
|
||||
agg,
|
||||
phone.Number,
|
||||
))
|
||||
if phone.Verified {
|
||||
events = append(events, schemauser.NewPhoneVerifiedEvent(ctx, agg))
|
||||
} else {
|
||||
cryptoCode, err := c.newPhoneCode(ctx, c.eventstore.Filter, alg) //nolint:staticcheck
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if phone.ReturnCode {
|
||||
plainCode = cryptoCode.Plain
|
||||
}
|
||||
events = append(events, schemauser.NewPhoneCodeAddedEvent(ctx, agg,
|
||||
cryptoCode.Crypted,
|
||||
cryptoCode.Expiry,
|
||||
phone.ReturnCode,
|
||||
))
|
||||
}
|
||||
return events, plainCode, nil
|
||||
}
|
||||
|
||||
func (c *Commands) getSchemaUserExists(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
|
||||
writeModel := NewExistsUserV3WriteModel(resourceOwner, id)
|
||||
writeModel := NewExistsUserV3WriteModel(resourceOwner, id, c.checkPermission)
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -452,7 +335,23 @@ func (c *Commands) getSchemaUserExists(ctx context.Context, resourceOwner, id st
|
||||
}
|
||||
|
||||
func (c *Commands) getSchemaUserWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
|
||||
writeModel := NewUserV3WriteModel(resourceOwner, id)
|
||||
writeModel := NewUserV3WriteModel(resourceOwner, id, c.checkPermission)
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModel, nil
|
||||
}
|
||||
|
||||
func (c *Commands) getSchemaUserEmailWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
|
||||
writeModel := NewUserV3EmailWriteModel(resourceOwner, id, c.checkPermission)
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModel, nil
|
||||
}
|
||||
|
||||
func (c *Commands) getSchemaUserPhoneWriteModelByID(ctx context.Context, resourceOwner, id string) (*UserV3WriteModel, error) {
|
||||
writeModel := NewUserV3PhoneWriteModel(resourceOwner, id, c.checkPermission)
|
||||
if err := c.eventstore.FilterToQueryReducer(ctx, writeModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
Reference in New Issue
Block a user