Skip to content

Commit e06e657

Browse files
wim07101993Copilot
andauthored
refactor: make create user compositable (#269)
- Extracted logic from service layer to domain layer - Created `UserAction` which can be used to combine multiple action on user spanning on transaction --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent b92cdbf commit e06e657

2 files changed

Lines changed: 167 additions & 82 deletions

File tree

internal/domain/user.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package domain
22

33
import (
44
"context"
5+
"encoding/json"
56
"time"
67

8+
"github.com/ianlancetaylor/jsonschema"
9+
"github.com/zitadel/nextgen/internal/maputil"
710
"github.com/zitadel/nextgen/internal/storage/database"
811
)
912

@@ -54,13 +57,35 @@ type CreateUser struct {
5457
Attributes []*CreateAttribute
5558
}
5659

57-
func NewCreateUser(projectID string, teamID *string, schemaURL string, attributes map[string]any, schema map[string]any) (*CreateUser, error) {
60+
func NewCreateUser(projectID string, teamID *string, schemabs []byte, muser map[string]any) (*CreateUser, error) {
61+
schemaURL, err := SchemaFromUserMap(muser)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
var jschema jsonschema.Schema
67+
err = json.Unmarshal(schemabs, &jschema)
68+
if err != nil {
69+
return nil, ErrInternal(err).WithMessage("failed to unmarshal json schema")
70+
}
71+
72+
err = jschema.Validate(muser)
73+
if err != nil {
74+
return nil, ErrUserInvalid().WithParent(err).WithMessage("user is not valid according to schema")
75+
}
76+
77+
var mschema map[string]any
78+
err = json.Unmarshal(schemabs, &mschema)
79+
if err != nil {
80+
return nil, ErrInternal(err).WithMessage("failed to unmarshal schema map")
81+
}
82+
5883
id, err := newID(PrefixUser)
5984
if err != nil {
6085
return nil, ErrInternal(err).WithMessage("failed to create user id")
6186
}
6287

63-
attrs, err := FlattenMapToCreateAttributes(attributes, schema, "")
88+
attrs, err := FlattenMapToCreateAttributes(muser, mschema, "")
6489
if err != nil {
6590
return nil, ErrInternal(err).WithMessage("failed to flatten user attributes")
6691
}
@@ -74,6 +99,15 @@ func NewCreateUser(projectID string, teamID *string, schemaURL string, attribute
7499
}, nil
75100
}
76101

102+
func SchemaFromUserMap(user map[string]any) (string, error) {
103+
schemaURL, ok := maputil.Get[string](user, "$schema")
104+
if !ok {
105+
return "", ErrUserInvalid().
106+
WithDetails("No $schema provided for the user. A schema must be provided when creating a new user. Against this schema, the user will be validated")
107+
}
108+
return schemaURL, nil
109+
}
110+
77111
type UserRepository interface {
78112
Repository
79113

internal/service/user.go

Lines changed: 131 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import (
66
"errors"
77
"time"
88

9-
"github.com/ianlancetaylor/jsonschema"
109
"github.com/zitadel/nextgen/internal/crypto"
1110
"github.com/zitadel/nextgen/internal/domain"
12-
"github.com/zitadel/nextgen/internal/maputil"
1311
"github.com/zitadel/nextgen/internal/storage/database"
1412
)
1513

@@ -21,6 +19,11 @@ type CreateUserInput struct {
2119
User map[string]any
2220
}
2321

22+
type UserAction interface {
23+
Prepare(ctx context.Context, db database.QueryExecutor) error
24+
Apply(ctx context.Context, db database.QueryExecutor) error
25+
}
26+
2427
type SetPasswordInput struct {
2528
ProjectID string
2629
UserID string
@@ -67,63 +70,53 @@ func NewUserService(
6770
}
6871
}
6972

70-
func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput) (_ map[string]any, err error) {
71-
// FETCH SCHEMA
72-
73-
schemaURL, ok := maputil.Get[string](input.User, "$schema")
74-
if !ok {
75-
return nil, domain.ErrUserInvalid().
76-
WithDetails("No $schema provided for the user. A schema must be provided when creating a new user. Against this schema, the user will be validated")
77-
}
78-
79-
schemaEntity, err := s.schemaRepo.GetByID(ctx, s.pool, input.ProjectID, schemaURL)
80-
if err != nil {
81-
if _, ok := errors.AsType[*database.NoRowFoundError](err); ok {
82-
return nil, domain.ErrUserInvalid().WithDetails("$schema is not known to the system. First create a schema, then create users.")
73+
func (s *UserService) ApplyActions(ctx context.Context, actions ...UserAction) (err error) {
74+
for _, action := range actions {
75+
err = action.Prepare(ctx, s.pool)
76+
if err != nil {
77+
return err
8378
}
84-
return nil, domain.ErrInternal(err).WithMessage("failed to get schema from database")
8579
}
8680

87-
// VALIDATE USER
88-
89-
var schema jsonschema.Schema
90-
err = json.Unmarshal(schemaEntity.Schema, &schema)
81+
tx, err := s.pool.Begin(ctx, nil)
9182
if err != nil {
92-
return nil, domain.ErrInternal(err).WithMessage("failed to unmarshal json schema")
83+
return domain.ErrInternal(err).WithMessage("failed to create transaction")
9384
}
85+
defer func() {
86+
if err != nil {
87+
_ = tx.Rollback(ctx)
88+
}
89+
}()
9490

95-
err = schema.Validate(input.User)
96-
if err != nil {
97-
return nil, domain.ErrUserInvalid().
98-
WithParent(err).
99-
WithMessage("user is not valid according to schema")
91+
for _, action := range actions {
92+
err = action.Apply(ctx, tx)
93+
if err != nil {
94+
return err
95+
}
10096
}
10197

102-
// PREPARE DOMAIN USER
103-
104-
var schemaMap map[string]any
105-
err = json.Unmarshal(schemaEntity.Schema, &schemaMap)
98+
err = tx.Commit(ctx)
10699
if err != nil {
107-
return nil, domain.ErrInternal(err).WithMessage("failed to unmarshal schema map")
100+
return domain.ErrInternal(err).WithMessage("failed to commit transaction")
108101
}
102+
return nil
103+
}
104+
105+
func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput) (_ map[string]any, err error) {
106+
// CreateUser does not need a transaction, so we don't wrap it in an `ApplyActions` call
109107

110-
createUser, err := domain.NewCreateUser(input.ProjectID, input.TeamID, schemaURL, input.User, schemaMap)
108+
action := NewCreateUserAction(input, s.userRepo, s.schemaRepo)
109+
err = action.Prepare(ctx, s.pool)
111110
if err != nil {
112111
return nil, err
113112
}
114113

115-
// SAVE USER
116-
117-
err = s.userRepo.Create(ctx, s.pool, createUser)
114+
err = action.Apply(ctx, s.pool)
118115
if err != nil {
119-
if _, ok := errors.AsType[*database.UniqueError](err); ok {
120-
return nil, domain.ErrUserAlreadyExists().WithParent(err)
121-
}
122-
return nil, domain.ErrInternal(err).WithMessage("failed to create user in the database")
116+
return nil, err
123117
}
124118

125-
input.User["id"] = createUser.ID
126-
return input.User, nil
119+
return action.User, nil
127120
}
128121

129122
func (s *UserService) GetUserByID(ctx context.Context, input GetUserInput) (map[string]any, error) {
@@ -145,45 +138,8 @@ func (s *UserService) GetUserByID(ctx context.Context, input GetUserInput) (map[
145138
}
146139

147140
func (s *UserService) SetPassword(ctx context.Context, input SetPasswordInput) (err error) {
148-
hash, err := domain.HashPassword(input.Password, s.hasher)
149-
if err != nil {
150-
return err
151-
}
152-
153-
tx, err := s.pool.Begin(ctx, nil)
154-
if err != nil {
155-
return domain.ErrInternal(err).WithMessage("failed to create transaction")
156-
}
157-
defer func() {
158-
if err != nil {
159-
_ = tx.Rollback(ctx)
160-
}
161-
}()
162-
163-
err = s.passwordRepo.DeleteByUserID(ctx, tx, input.ProjectID, input.UserID)
164-
if err != nil {
165-
return domain.ErrInternal(err).WithMessage("failed to remove old password from database")
166-
}
167-
168-
err = s.passwordRepo.Create(ctx, tx, &domain.CreateUserPassword{
169-
ProjectID: input.ProjectID,
170-
UserID: input.UserID,
171-
EncodedHash: hash,
172-
ChangeRequired: input.IsPasswordChangeRequired,
173-
VerificationID: nil, // TODO what should I do with this?
174-
})
175-
if err != nil {
176-
if _, ok := errors.AsType[*database.ForeignKeyError](err); ok {
177-
return domain.ErrUserNotFound()
178-
}
179-
return domain.ErrInternal(err).WithMessage("failed to set initial password")
180-
}
181-
182-
err = tx.Commit(ctx)
183-
if err != nil {
184-
return domain.ErrInternal(err).WithMessage("failed to commit transaction while setting password")
185-
}
186-
return nil
141+
action := NewSetUserPasswordAction(input, s.hasher, s.passwordRepo)
142+
return s.ApplyActions(ctx, action)
187143
}
188144

189145
func (s *UserService) GetMyUser(ctx context.Context, input GetMyUserInput) ([]byte, error) {
@@ -213,3 +169,98 @@ func (s *UserService) GetMyUser(ctx context.Context, input GetMyUserInput) ([]by
213169

214170
return userbs, nil
215171
}
172+
173+
// ---- CreateUser opts -------------------------------------------------------------
174+
175+
type CreateUserAction struct {
176+
CreateUserInput
177+
178+
userRepo domain.UserRepository
179+
schemaRepo domain.JSONSchemaRepository
180+
181+
createUser *domain.CreateUser
182+
}
183+
184+
func NewCreateUserAction(input CreateUserInput, userRepo domain.UserRepository, schemaRepo domain.JSONSchemaRepository) *CreateUserAction {
185+
return &CreateUserAction{
186+
CreateUserInput: input,
187+
userRepo: userRepo,
188+
schemaRepo: schemaRepo,
189+
}
190+
}
191+
192+
func (o *CreateUserAction) Prepare(ctx context.Context, db database.QueryExecutor) error {
193+
schemaURL, err := domain.SchemaFromUserMap(o.User)
194+
if err != nil {
195+
return err
196+
}
197+
198+
schemaEntity, err := o.schemaRepo.GetByID(ctx, db, o.ProjectID, schemaURL)
199+
if err != nil {
200+
if _, ok := errors.AsType[*database.NoRowFoundError](err); ok {
201+
return domain.ErrUserInvalid().WithDetails("$schema is not known to the system. First create a schema, then create users.")
202+
}
203+
return domain.ErrInternal(err).WithMessage("failed to get schema from database")
204+
}
205+
206+
o.createUser, err = domain.NewCreateUser(o.ProjectID, o.TeamID, schemaEntity.Schema, o.User)
207+
if err != nil {
208+
return err
209+
}
210+
211+
o.User["id"] = o.createUser.ID
212+
return nil
213+
}
214+
func (o *CreateUserAction) Apply(ctx context.Context, db database.QueryExecutor) error {
215+
err := o.userRepo.Create(ctx, db, o.createUser)
216+
if err != nil {
217+
if _, ok := errors.AsType[*database.UniqueError](err); ok {
218+
return domain.ErrUserAlreadyExists().WithParent(err)
219+
}
220+
return domain.ErrInternal(err).WithMessage("failed to create user in the database")
221+
}
222+
223+
return nil
224+
}
225+
226+
type SetPasswordUserAction struct {
227+
SetPasswordInput
228+
229+
hasher crypto.Hasher
230+
passwordRepo domain.UserPasswordRepository
231+
232+
hash string
233+
}
234+
235+
func NewSetUserPasswordAction(input SetPasswordInput, hasher crypto.Hasher, passwordRepo domain.UserPasswordRepository) *SetPasswordUserAction {
236+
return &SetPasswordUserAction{
237+
SetPasswordInput: input,
238+
hasher: hasher,
239+
passwordRepo: passwordRepo,
240+
}
241+
}
242+
func (o *SetPasswordUserAction) Prepare(_ context.Context, _ database.QueryExecutor) (err error) {
243+
o.hash, err = domain.HashPassword(o.Password, o.hasher)
244+
return err
245+
}
246+
247+
func (o *SetPasswordUserAction) Apply(ctx context.Context, db database.QueryExecutor) error {
248+
err := o.passwordRepo.DeleteByUserID(ctx, db, o.ProjectID, o.UserID)
249+
if err != nil {
250+
return domain.ErrInternal(err).WithMessage("failed to remove old password from database")
251+
}
252+
253+
err = o.passwordRepo.Create(ctx, db, &domain.CreateUserPassword{
254+
ProjectID: o.ProjectID,
255+
UserID: o.UserID,
256+
EncodedHash: o.hash,
257+
ChangeRequired: o.IsPasswordChangeRequired,
258+
})
259+
if err != nil {
260+
if _, ok := errors.AsType[*database.ForeignKeyError](err); ok {
261+
return domain.ErrUserNotFound()
262+
}
263+
return domain.ErrInternal(err).WithMessage("failed to set initial password")
264+
}
265+
return nil
266+
}

0 commit comments

Comments
 (0)