From 8537b95e3e13c2ee9d59904a8d6d232311f8aca0 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 22 Oct 2024 13:27:08 +0200 Subject: [PATCH] chore: Restructure code into domain package --- backend/api/errors.go | 4 +- backend/api/graph/admin.resolvers.go | 35 ++--- backend/api/graph/authentication.resolvers.go | 13 +- backend/api/graph/generated/generated.go | 39 ++--- backend/api/graph/helper/account.go | 17 ++- backend/api/graph/helper/authentication.go | 4 +- backend/api/graph/helper/common.go | 8 +- backend/api/graph/helper/organisation.go | 17 ++- backend/api/graph/middleware/sentry.go | 4 +- backend/api/graph/model/model_date.go | 10 +- backend/api/graph/model/model_date_test.go | 6 +- backend/api/graph/model/models_gen.go | 16 +- .../admin/mutation_create_account_test.go | 5 +- .../mutation_create_organisation_test.go | 3 +- .../admin/mutation_delete_account_test.go | 5 +- .../mutation_delete_organisation_test.go | 3 +- .../admin/mutation_update_account_test.go | 5 +- .../mutation_update_organisation_test.go | 3 +- backend/api/handler/error_presenter.go | 4 +- backend/api/http/middleware/auth_context.go | 10 +- backend/api/http/middleware/refresh_tokens.go | 8 +- backend/api/resolver_dependencies.go | 3 +- backend/cli/ctl/cmd_account.go | 7 +- backend/cli/ctl/cmd_account_list.go | 6 +- backend/cli/ctl/main.go | 3 +- backend/cli/ctl/time_source.go | 4 +- .../account_create_cmd.go} | 45 +++--- .../account_delete_cmd.go} | 2 +- .../account_update_cmd.go} | 31 ++-- .../{cmd_login.go => command/login_cmd.go} | 2 +- .../organisation_create_cmd.go} | 10 +- .../organisation_delete_cmd.go} | 2 +- .../organisation_update_cmd.go} | 10 +- backend/domain/command/validate.go | 7 + backend/domain/enums.go | 5 - backend/domain/errors.go | 43 ------ backend/domain/helper.go | 30 ---- backend/domain/{ => model}/account.go | 7 +- backend/domain/{ => model}/organisation.go | 15 +- .../account_query.go} | 9 +- backend/domain/query/organisation_query.go | 25 ++++ backend/domain/query_organisation.go | 10 -- backend/domain/{ => types}/date.go | 2 +- backend/domain/{ => types}/date_test.go | 138 +++++++++--------- backend/domain/{ => types}/error_codes.go | 2 +- backend/domain/types/errors.go | 127 ++++++++++++++++ backend/domain/{ => types}/role.go | 2 +- backend/domain/{ => types}/time_source.go | 2 +- backend/finder/account_finder.go | 15 +- backend/finder/finder.go | 6 +- backend/finder/organisation_finder.go | 15 +- backend/go.mod | 2 +- backend/gqlgen.yml | 2 +- backend/handler/account_create_handler.go | 4 +- backend/handler/account_delete_handler.go | 13 +- backend/handler/account_update_handler.go | 11 +- backend/handler/handler.go | 5 +- backend/handler/login_handler.go | 10 +- .../handler/organisation_create_handler.go | 4 +- .../handler/organisation_delete_handler.go | 11 +- .../handler/organisation_update_handler.go | 11 +- .../repository/account_repository.go | 38 +++-- .../repository/mappings_account_gen.go | 7 +- .../repository/mappings_organisation_gen.go | 3 +- .../repository/organisation_repository.go | 17 ++- .../security/authentication/auth_context.go | 4 +- .../authentication/generate_auth_token.go | 4 +- .../authentication/generate_csrf_token.go | 4 +- backend/security/authorization/checks.go | 10 +- backend/security/authorization/checks_test.go | 42 +++--- backend/security/authorization/commands.go | 31 ++-- backend/security/authorization/queries.go | 26 ++-- .../security/authorization/queries_test.go | 27 ++-- .../authorization/types.go} | 2 +- backend/test/auth/fixed_login_data.go | 18 +-- 75 files changed, 605 insertions(+), 505 deletions(-) rename backend/domain/{cmd_account_create.go => command/account_create_cmd.go} (54%) rename backend/domain/{cmd_account_delete.go => command/account_delete_cmd.go} (95%) rename backend/domain/{cmd_account_update.go => command/account_update_cmd.go} (59%) rename backend/domain/{cmd_login.go => command/login_cmd.go} (95%) rename backend/domain/{cmd_organisation_create.go => command/organisation_create_cmd.go} (77%) rename backend/domain/{cmd_organisation_delete.go => command/organisation_delete_cmd.go} (94%) rename backend/domain/{cmd_organisation_update.go => command/organisation_update_cmd.go} (59%) create mode 100644 backend/domain/command/validate.go delete mode 100644 backend/domain/enums.go delete mode 100644 backend/domain/errors.go delete mode 100644 backend/domain/helper.go rename backend/domain/{ => model}/account.go (91%) rename backend/domain/{ => model}/organisation.go (63%) rename backend/domain/{query_account.go => query/account_query.go} (75%) create mode 100644 backend/domain/query/organisation_query.go delete mode 100644 backend/domain/query_organisation.go rename backend/domain/{ => types}/date.go (99%) rename backend/domain/{ => types}/date_test.go (61%) rename backend/domain/{ => types}/error_codes.go (98%) create mode 100644 backend/domain/types/errors.go rename backend/domain/{ => types}/role.go (98%) rename backend/domain/{ => types}/time_source.go (80%) rename backend/{domain/filter.go => security/authorization/types.go} (84%) diff --git a/backend/api/errors.go b/backend/api/errors.go index 96682a0..165ea4f 100644 --- a/backend/api/errors.go +++ b/backend/api/errors.go @@ -4,7 +4,7 @@ import ( "errors" "myvendor.mytld/myproject/backend/api/graph/model" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) var ErrAuthTokenInvalid = TypedError{"authTokenInvalid", "auth token invalid"} @@ -43,7 +43,7 @@ func ResultFromErr(err error) (*model.Result, error) { } func FieldsErrorFromErr(err error) *model.FieldsError { - var fieldErr domain.FieldResolvableError + var fieldErr types.FieldResolvableError if errors.As(err, &fieldErr) { return &model.FieldsError{ Errors: []*model.FieldError{ diff --git a/backend/api/graph/admin.resolvers.go b/backend/api/graph/admin.resolvers.go index 8647a46..74517e6 100644 --- a/backend/api/graph/admin.resolvers.go +++ b/backend/api/graph/admin.resolvers.go @@ -8,17 +8,18 @@ import ( "context" "github.com/gofrs/uuid" - "myvendor.mytld/myproject/backend/api/graph/generated" "myvendor.mytld/myproject/backend/api/graph/helper" "myvendor.mytld/myproject/backend/api/graph/model" - domain_model "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/query" + domain_model "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" ) // CreateAccount is the resolver for the createAccount field. func (r *mutationResolver) CreateAccount(ctx context.Context, role domain_model.Role, emailAddress string, password string, organisationID *uuid.UUID) (*model.Account, error) { - cmd, err := domain_model.NewAccountCreateCmd(emailAddress, domain_model.Role(role), password) + cmd, err := command.NewAccountCreateCmd(emailAddress, domain_model.Role(role), password) if err != nil { return nil, err } @@ -32,7 +33,7 @@ func (r *mutationResolver) CreateAccount(ctx context.Context, role domain_model. return nil, err } - record, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ + record, err := r.finder.QueryAccount(ctx, query.AccountQuery{ AccountID: cmd.AccountID, }) if err != nil { @@ -44,14 +45,14 @@ func (r *mutationResolver) CreateAccount(ctx context.Context, role domain_model. // UpdateAccount is the resolver for the updateAccount field. func (r *mutationResolver) UpdateAccount(ctx context.Context, id uuid.UUID, role domain_model.Role, emailAddress string, password *string, organisationID *uuid.UUID) (*model.Account, error) { // Fetch previous record to get organisation id - prevRecord, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ + prevRecord, err := r.finder.QueryAccount(ctx, query.AccountQuery{ AccountID: id, }) if err != nil { return nil, err } - cmd, err := domain_model.NewAccountUpdateCmd(r.Config, prevRecord.OrganisationID, id, emailAddress, role, helper.ToVal(password)) + cmd, err := command.NewAccountUpdateCmd(r.Config, prevRecord.OrganisationID, id, emailAddress, role, helper.ToVal(password)) if err != nil { return nil, err } @@ -64,7 +65,7 @@ func (r *mutationResolver) UpdateAccount(ctx context.Context, id uuid.UUID, role return nil, err } - record, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ + record, err := r.finder.QueryAccount(ctx, query.AccountQuery{ AccountID: id, }) if err != nil { @@ -75,14 +76,14 @@ func (r *mutationResolver) UpdateAccount(ctx context.Context, id uuid.UUID, role // DeleteAccount is the resolver for the deleteAccount field. func (r *mutationResolver) DeleteAccount(ctx context.Context, id uuid.UUID) (*model.Account, error) { - record, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ + record, err := r.finder.QueryAccount(ctx, query.AccountQuery{ AccountID: id, }) if err != nil { return nil, err } - cmd := domain_model.NewAccountDeleteCmd(id, record.OrganisationID) + cmd := command.NewAccountDeleteCmd(id, record.OrganisationID) err = r.handler.AccountDelete(ctx, cmd) if err != nil { return nil, err @@ -92,7 +93,7 @@ func (r *mutationResolver) DeleteAccount(ctx context.Context, id uuid.UUID) (*mo // CreateOrganisation is the resolver for the createOrganisation field. func (r *mutationResolver) CreateOrganisation(ctx context.Context, name string) (*model.Organisation, error) { - cmd, err := domain_model.NewOrganisationCreateCmd() + cmd, err := command.NewOrganisationCreateCmd() if err != nil { return nil, err } @@ -101,7 +102,7 @@ func (r *mutationResolver) CreateOrganisation(ctx context.Context, name string) if err != nil { return nil, err } - record, err := r.finder.QueryOrganisation(ctx, domain_model.OrganisationQuery{ + record, err := r.finder.QueryOrganisation(ctx, query.OrganisationQuery{ OrganisationID: cmd.OrganisationID, }) if err != nil { @@ -112,7 +113,7 @@ func (r *mutationResolver) CreateOrganisation(ctx context.Context, name string) // UpdateOrganisation is the resolver for the updateOrganisation field. func (r *mutationResolver) UpdateOrganisation(ctx context.Context, id uuid.UUID, name string) (*model.Organisation, error) { - cmd := domain_model.OrganisationUpdateCmd{ + cmd := command.OrganisationUpdateCmd{ OrganisationID: id, Name: name, } @@ -120,7 +121,7 @@ func (r *mutationResolver) UpdateOrganisation(ctx context.Context, id uuid.UUID, if err != nil { return nil, err } - record, err := r.finder.QueryOrganisation(ctx, domain_model.OrganisationQuery{ + record, err := r.finder.QueryOrganisation(ctx, query.OrganisationQuery{ OrganisationID: id, }) if err != nil { @@ -131,14 +132,14 @@ func (r *mutationResolver) UpdateOrganisation(ctx context.Context, id uuid.UUID, // DeleteOrganisation is the resolver for the deleteOrganisation field. func (r *mutationResolver) DeleteOrganisation(ctx context.Context, id uuid.UUID) (*model.Organisation, error) { - record, err := r.finder.QueryOrganisation(ctx, domain_model.OrganisationQuery{ + record, err := r.finder.QueryOrganisation(ctx, query.OrganisationQuery{ OrganisationID: id, }) if err != nil { return nil, err } - cmd := domain_model.NewOrganisationDeleteCmd(id) + cmd := command.NewOrganisationDeleteCmd(id) err = r.handler.OrganisationDelete(ctx, cmd) if err != nil { return nil, err @@ -148,7 +149,7 @@ func (r *mutationResolver) DeleteOrganisation(ctx context.Context, id uuid.UUID) // Account is the resolver for the Account field. func (r *queryResolver) Account(ctx context.Context, id uuid.UUID) (*model.Account, error) { - record, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ + record, err := r.finder.QueryAccount(ctx, query.AccountQuery{ AccountID: id, }) if err == repository.ErrNotFound { @@ -188,7 +189,7 @@ func (r *queryResolver) AllAccountsMeta(ctx context.Context, page *int, perPage // Organisation is the resolver for the Organisation field. func (r *queryResolver) Organisation(ctx context.Context, id uuid.UUID) (*model.Organisation, error) { - record, err := r.finder.QueryOrganisation(ctx, domain_model.OrganisationQuery{ + record, err := r.finder.QueryOrganisation(ctx, query.OrganisationQuery{ OrganisationID: id, }) if err == repository.ErrNotFound { diff --git a/backend/api/graph/authentication.resolvers.go b/backend/api/graph/authentication.resolvers.go index 9a4678b..7af8d68 100644 --- a/backend/api/graph/authentication.resolvers.go +++ b/backend/api/graph/authentication.resolvers.go @@ -9,11 +9,12 @@ import ( logger "github.com/apex/log" fog_errors "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/api" "myvendor.mytld/myproject/backend/api/graph/helper" "myvendor.mytld/myproject/backend/api/graph/model" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/query" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/handler" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" @@ -23,12 +24,12 @@ import ( func (r *mutationResolver) Login(ctx context.Context, credentials model.LoginCredentials) (*model.LoginResult, error) { defer helper.ConstantTime(r.SensitiveOperationConstantTime).Wait(ctx) - cmd := domain.NewLoginCmd(credentials.EmailAddress, credentials.Password) + cmd := command.NewLoginCmd(credentials.EmailAddress, credentials.Password) if credentials.KeepMeLoggedIn != nil && *credentials.KeepMeLoggedIn { cmd.ExtendedExpiry = true } - account, err := r.finder.QueryAccountNotAuthorized(ctx, domain.AccountQueryNotAuthorized{ + account, err := r.finder.QueryAccountNotAuthorized(ctx, query.AccountQueryNotAuthorized{ Opts: helper.AccountQueryOptsFromSelection(ctx, "account"), EmailAddress: &cmd.EmailAddress, }) @@ -46,7 +47,7 @@ func (r *mutationResolver) Login(ctx context.Context, credentials model.LoginCre if fog_errors.Is(err, handler.ErrLoginInvalidCredentials) { return &model.LoginResult{ Error: &model.Error{ - Code: domain.ErrorCodeInvalidCredentials, + Code: types.ErrorCodeInvalidCredentials, }, }, nil } @@ -91,7 +92,7 @@ func (r *queryResolver) LoginStatus(ctx context.Context) (bool, error) { // CurrentAccount is the resolver for the currentAccount field. func (r *queryResolver) CurrentAccount(ctx context.Context) (*model.Account, error) { authCtx := authentication.GetAuthContext(ctx) - account, err := r.finder.QueryAccount(ctx, domain.AccountQuery{ + account, err := r.finder.QueryAccount(ctx, query.AccountQuery{ AccountID: authCtx.AccountID, Opts: helper.AccountQueryOptsFromSelection(ctx), }) diff --git a/backend/api/graph/generated/generated.go b/backend/api/graph/generated/generated.go index 5b64e12..741d561 100644 --- a/backend/api/graph/generated/generated.go +++ b/backend/api/graph/generated/generated.go @@ -18,7 +18,7 @@ import ( gqlparser "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" "myvendor.mytld/myproject/backend/api/graph/model" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) // region ************************** generated!.gotpl ************************** @@ -87,13 +87,13 @@ type ComplexityRoot struct { } Mutation struct { - CreateAccount func(childComplexity int, role domain.Role, emailAddress string, password string, organisationID *uuid.UUID) int + CreateAccount func(childComplexity int, role types.Role, emailAddress string, password string, organisationID *uuid.UUID) int CreateOrganisation func(childComplexity int, name string) int DeleteAccount func(childComplexity int, id uuid.UUID) int DeleteOrganisation func(childComplexity int, id uuid.UUID) int Login func(childComplexity int, credentials model.LoginCredentials) int Logout func(childComplexity int) int - UpdateAccount func(childComplexity int, id uuid.UUID, role domain.Role, emailAddress string, password *string, organisationID *uuid.UUID) int + UpdateAccount func(childComplexity int, id uuid.UUID, role types.Role, emailAddress string, password *string, organisationID *uuid.UUID) int UpdateOrganisation func(childComplexity int, id uuid.UUID, name string) int } @@ -122,8 +122,8 @@ type ComplexityRoot struct { } type MutationResolver interface { - CreateAccount(ctx context.Context, role domain.Role, emailAddress string, password string, organisationID *uuid.UUID) (*model.Account, error) - UpdateAccount(ctx context.Context, id uuid.UUID, role domain.Role, emailAddress string, password *string, organisationID *uuid.UUID) (*model.Account, error) + CreateAccount(ctx context.Context, role types.Role, emailAddress string, password string, organisationID *uuid.UUID) (*model.Account, error) + UpdateAccount(ctx context.Context, id uuid.UUID, role types.Role, emailAddress string, password *string, organisationID *uuid.UUID) (*model.Account, error) DeleteAccount(ctx context.Context, id uuid.UUID) (*model.Account, error) CreateOrganisation(ctx context.Context, name string) (*model.Organisation, error) UpdateOrganisation(ctx context.Context, id uuid.UUID, name string) (*model.Organisation, error) @@ -298,7 +298,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.CreateAccount(childComplexity, args["role"].(domain.Role), args["emailAddress"].(string), args["password"].(string), args["organisationId"].(*uuid.UUID)), true + return e.complexity.Mutation.CreateAccount(childComplexity, args["role"].(types.Role), args["emailAddress"].(string), args["password"].(string), args["organisationId"].(*uuid.UUID)), true case "Mutation.createOrganisation": if e.complexity.Mutation.CreateOrganisation == nil { @@ -365,7 +365,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.UpdateAccount(childComplexity, args["id"].(uuid.UUID), args["role"].(domain.Role), args["emailAddress"].(string), args["password"].(*string), args["organisationId"].(*uuid.UUID)), true + return e.complexity.Mutation.UpdateAccount(childComplexity, args["id"].(uuid.UUID), args["role"].(types.Role), args["emailAddress"].(string), args["password"].(*string), args["organisationId"].(*uuid.UUID)), true case "Mutation.updateOrganisation": if e.complexity.Mutation.UpdateOrganisation == nil { @@ -808,8 +808,9 @@ scalar UUID scalar Date scalar DateTime scalar ByteSize +scalar Upload -## TODO Add domain types for your general schema here +## boilderplate: Add domain types for your general schema here # # Queries @@ -874,10 +875,10 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) func (ec *executionContext) field_Mutation_createAccount_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - var arg0 domain.Role + var arg0 types.Role if tmp, ok := rawArgs["role"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role")) - arg0, err = ec.unmarshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚐRole(ctx, tmp) + arg0, err = ec.unmarshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚋtypesᚐRole(ctx, tmp) if err != nil { return nil, err } @@ -985,10 +986,10 @@ func (ec *executionContext) field_Mutation_updateAccount_args(ctx context.Contex } } args["id"] = arg0 - var arg1 domain.Role + var arg1 types.Role if tmp, ok := rawArgs["role"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("role")) - arg1, err = ec.unmarshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚐRole(ctx, tmp) + arg1, err = ec.unmarshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚋtypesᚐRole(ctx, tmp) if err != nil { return nil, err } @@ -1464,9 +1465,9 @@ func (ec *executionContext) _Account_role(ctx context.Context, field graphql.Col } return graphql.Null } - res := resTmp.(domain.Role) + res := resTmp.(types.Role) fc.Result = res - return ec.marshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚐRole(ctx, field.Selections, res) + return ec.marshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚋtypesᚐRole(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Account_role(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -2174,7 +2175,7 @@ func (ec *executionContext) _Mutation_createAccount(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateAccount(rctx, fc.Args["role"].(domain.Role), fc.Args["emailAddress"].(string), fc.Args["password"].(string), fc.Args["organisationId"].(*uuid.UUID)) + return ec.resolvers.Mutation().CreateAccount(rctx, fc.Args["role"].(types.Role), fc.Args["emailAddress"].(string), fc.Args["password"].(string), fc.Args["organisationId"].(*uuid.UUID)) }) if err != nil { ec.Error(ctx, err) @@ -2242,7 +2243,7 @@ func (ec *executionContext) _Mutation_updateAccount(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateAccount(rctx, fc.Args["id"].(uuid.UUID), fc.Args["role"].(domain.Role), fc.Args["emailAddress"].(string), fc.Args["password"].(*string), fc.Args["organisationId"].(*uuid.UUID)) + return ec.resolvers.Mutation().UpdateAccount(rctx, fc.Args["id"].(uuid.UUID), fc.Args["role"].(types.Role), fc.Args["emailAddress"].(string), fc.Args["password"].(*string), fc.Args["organisationId"].(*uuid.UUID)) }) if err != nil { ec.Error(ctx, err) @@ -6747,13 +6748,13 @@ func (ec *executionContext) marshalNOrganisation2ᚖmyvendorᚗmytldᚋmyproject return ec._Organisation(ctx, sel, v) } -func (ec *executionContext) unmarshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚐRole(ctx context.Context, v interface{}) (domain.Role, error) { - var res domain.Role +func (ec *executionContext) unmarshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚋtypesᚐRole(ctx context.Context, v interface{}) (types.Role, error) { + var res types.Role err := res.UnmarshalGQL(v) return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) marshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚐRole(ctx context.Context, sel ast.SelectionSet, v domain.Role) graphql.Marshaler { +func (ec *executionContext) marshalNRole2myvendorᚗmytldᚋmyprojectᚋbackendᚋdomainᚋtypesᚐRole(ctx context.Context, sel ast.SelectionSet, v types.Role) graphql.Marshaler { return v } diff --git a/backend/api/graph/helper/account.go b/backend/api/graph/helper/account.go index 4db053b..04cf3f1 100644 --- a/backend/api/graph/helper/account.go +++ b/backend/api/graph/helper/account.go @@ -4,10 +4,11 @@ import ( "context" "myvendor.mytld/myproject/backend/api/graph/model" - "myvendor.mytld/myproject/backend/domain" + model2 "myvendor.mytld/myproject/backend/domain/model" + "myvendor.mytld/myproject/backend/domain/query" ) -func MapToAccount(record domain.Account) *model.Account { +func MapToAccount(record model2.Account) *model.Account { return &model.Account{ ID: record.ID, EmailAddress: record.EmailAddress, @@ -19,7 +20,7 @@ func MapToAccount(record domain.Account) *model.Account { } } -func MapToAccounts(records []domain.Account) []*model.Account { +func MapToAccounts(records []model2.Account) []*model.Account { result := make([]*model.Account, len(records)) for i, record := range records { result[i] = MapToAccount(record) @@ -27,20 +28,20 @@ func MapToAccounts(records []domain.Account) []*model.Account { return result } -func MapFromAccountFilter(filter *model.AccountFilter) domain.AccountsQuery { +func MapFromAccountFilter(filter *model.AccountFilter) query.AccountsQuery { if filter == nil { - return domain.AccountsQuery{} + return query.AccountsQuery{} } - return domain.AccountsQuery{ + return query.AccountsQuery{ IDs: filter.Ids, SearchTerm: ToVal(filter.Q), OrganisationID: filter.OrganisationID, } } -func AccountQueryOptsFromSelection(ctx context.Context, accountSelectPath ...string) domain.AccountQueryOpts { +func AccountQueryOptsFromSelection(ctx context.Context, accountSelectPath ...string) *query.AccountQueryOpts { selectedFields := SelectedFields(ctx) - return domain.AccountQueryOpts{ + return &query.AccountQueryOpts{ IncludeOrganisation: selectedFields.PathSelected(append(accountSelectPath, "organisation")...), OrganisationQueryOpts: OrganisationQueryOptsFromSelection(ctx, append(accountSelectPath, "organisation")...), } diff --git a/backend/api/graph/helper/authentication.go b/backend/api/graph/helper/authentication.go index e3f6bb2..de06312 100644 --- a/backend/api/graph/helper/authentication.go +++ b/backend/api/graph/helper/authentication.go @@ -6,11 +6,11 @@ import ( fog_errors "github.com/friendsofgo/errors" "myvendor.mytld/myproject/backend/api" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/security/authentication" ) -func SetAuthTokenCookieForAccount(ctx context.Context, account authentication.AuthTokenDataProvider, timeSource domain.TimeSource, extendedExpiry bool) (authToken string, csrfToken string, err error) { +func SetAuthTokenCookieForAccount(ctx context.Context, account authentication.AuthTokenDataProvider, timeSource types.TimeSource, extendedExpiry bool) (authToken string, csrfToken string, err error) { tokenOpts := authentication.TokenOptsForAccount(account, extendedExpiry) authToken, err = authentication.GenerateAuthToken(account, timeSource, tokenOpts) if err != nil { diff --git a/backend/api/graph/helper/common.go b/backend/api/graph/helper/common.go index 1085c86..c08f235 100644 --- a/backend/api/graph/helper/common.go +++ b/backend/api/graph/helper/common.go @@ -5,7 +5,7 @@ import ( "github.com/gofrs/uuid" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/finder" ) @@ -62,9 +62,9 @@ func ToNullUUID(id *uuid.UUID) uuid.NullUUID { return uuid.NullUUID{UUID: *id, Valid: true} } -func ToNullDate(date *domain.Date) domain.NullDate { +func ToNullDate(date *types.Date) types.NullDate { if date == nil { - return domain.NullDate{Valid: false} + return types.NullDate{Valid: false} } - return domain.NullDate{Date: *date, Valid: true} + return types.NullDate{Date: *date, Valid: true} } diff --git a/backend/api/graph/helper/organisation.go b/backend/api/graph/helper/organisation.go index 32efac4..cfc937b 100644 --- a/backend/api/graph/helper/organisation.go +++ b/backend/api/graph/helper/organisation.go @@ -4,10 +4,11 @@ import ( "context" "myvendor.mytld/myproject/backend/api/graph/model" - "myvendor.mytld/myproject/backend/domain" + model2 "myvendor.mytld/myproject/backend/domain/model" + "myvendor.mytld/myproject/backend/domain/query" ) -func MapToOrganisation(record domain.Organisation) *model.Organisation { +func MapToOrganisation(record model2.Organisation) *model.Organisation { return &model.Organisation{ ID: record.ID, Name: record.Name, @@ -16,7 +17,7 @@ func MapToOrganisation(record domain.Organisation) *model.Organisation { } } -func MapToOrganisations(records []domain.Organisation) []*model.Organisation { +func MapToOrganisations(records []model2.Organisation) []*model.Organisation { result := make([]*model.Organisation, len(records)) for i, record := range records { result[i] = MapToOrganisation(record) @@ -24,16 +25,16 @@ func MapToOrganisations(records []domain.Organisation) []*model.Organisation { return result } -func MapToOrganisationsQuery(filter *model.OrganisationFilter) domain.OrganisationsQuery { +func MapToOrganisationsQuery(filter *model.OrganisationFilter) query.OrganisationsQuery { if filter == nil { - return domain.OrganisationsQuery{} + return query.OrganisationsQuery{} } - return domain.OrganisationsQuery{ + return query.OrganisationsQuery{ IDs: filter.Ids, SearchTerm: ToVal(filter.Q), } } -func OrganisationQueryOptsFromSelection(ctx context.Context, organisationSelectPath ...string) domain.OrganisationQueryOpts { - return domain.OrganisationQueryOpts{} +func OrganisationQueryOptsFromSelection(ctx context.Context, organisationSelectPath ...string) *query.OrganisationQueryOpts { + return &query.OrganisationQueryOpts{} } diff --git a/backend/api/graph/middleware/sentry.go b/backend/api/graph/middleware/sentry.go index a1de748..2b087e5 100644 --- a/backend/api/graph/middleware/sentry.go +++ b/backend/api/graph/middleware/sentry.go @@ -9,7 +9,7 @@ import ( "github.com/getsentry/sentry-go" apexlogutils_middleware "github.com/networkteam/apexlogutils/middleware" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) func SentryGraphqlMiddleware(ctx context.Context, next graphql.Resolver) (res any, err error) { @@ -18,7 +18,7 @@ func SentryGraphqlMiddleware(ctx context.Context, next graphql.Resolver) (res an res, err = next(ctx) if err != nil { // Skip field resolvable errors, since these are expected to occur - var fieldErr domain.FieldResolvableError + var fieldErr types.FieldResolvableError if errors.As(err, &fieldErr) { return nil, err } diff --git a/backend/api/graph/model/model_date.go b/backend/api/graph/model/model_date.go index 27f761a..e86b1ae 100644 --- a/backend/api/graph/model/model_date.go +++ b/backend/api/graph/model/model_date.go @@ -7,22 +7,22 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) -func MarshalDateScalar(value domain.Date) graphql.Marshaler { +func MarshalDateScalar(value types.Date) graphql.Marshaler { return graphql.WriterFunc(func(w io.Writer) { _, _ = w.Write([]byte(strconv.Quote(value.String()))) //nolint:errcheck }) } -func UnmarshalDateScalar(v any) (domain.Date, error) { +func UnmarshalDateScalar(v any) (types.Date, error) { dateString, ok := v.(string) if !ok { - return domain.Date{}, errors.Errorf("%T is not a string", v) + return types.Date{}, errors.Errorf("%T is not a string", v) } - d, err := domain.ParseDate(dateString) + d, err := types.ParseDate(dateString) if err != nil { return d, err } diff --git a/backend/api/graph/model/model_date_test.go b/backend/api/graph/model/model_date_test.go index 4049a4a..fdffee1 100644 --- a/backend/api/graph/model/model_date_test.go +++ b/backend/api/graph/model/model_date_test.go @@ -8,14 +8,14 @@ import ( "github.com/stretchr/testify/require" "myvendor.mytld/myproject/backend/api/graph/model" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) func TestUnmarshalDateScalar(t *testing.T) { tests := []struct { name string v any - want domain.Date + want types.Date wantErr bool }{ { @@ -26,7 +26,7 @@ func TestUnmarshalDateScalar(t *testing.T) { { name: "ISO date", v: "2020-10-09", - want: domain.Date{Year: 2020, Month: time.October, Day: 9}, + want: types.Date{Year: 2020, Month: time.October, Day: 9}, }, { name: "RFC3339 date with time", diff --git a/backend/api/graph/model/models_gen.go b/backend/api/graph/model/models_gen.go index 68cbbab..f68d92b 100644 --- a/backend/api/graph/model/models_gen.go +++ b/backend/api/graph/model/models_gen.go @@ -6,17 +6,17 @@ import ( "time" "github.com/gofrs/uuid" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) type Account struct { - ID uuid.UUID `json:"id"` - EmailAddress string `json:"emailAddress"` - Role domain.Role `json:"role"` - LastLogin *time.Time `json:"lastLogin,omitempty"` - OrganisationID *uuid.UUID `json:"organisationId,omitempty"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + ID uuid.UUID `json:"id"` + EmailAddress string `json:"emailAddress"` + Role types.Role `json:"role"` + LastLogin *time.Time `json:"lastLogin,omitempty"` + OrganisationID *uuid.UUID `json:"organisationId,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` } type AccountFilter struct { diff --git a/backend/api/graph/tests/admin/mutation_create_account_test.go b/backend/api/graph/tests/admin/mutation_create_account_test.go index 51fcade..352f361 100644 --- a/backend/api/graph/tests/admin/mutation_create_account_test.go +++ b/backend/api/graph/tests/admin/mutation_create_account_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "myvendor.mytld/myproject/backend/api" - "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/test" test_auth "myvendor.mytld/myproject/backend/test/auth" @@ -61,7 +60,7 @@ func TestMutationResolver_CreateAccount(t *testing.T) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) require.NotNil(t, res.Data.Result) - account, err := repository.FindAccountByID(context.Background(), db, res.Data.Result.ID, domain.AccountQueryOpts{}) + account, err := repository.FindAccountByID(context.Background(), db, res.Data.Result.ID, nil) require.NoError(t, err) assert.Equal(t, "test@acme.com", account.EmailAddress) @@ -98,7 +97,7 @@ func TestMutationResolver_CreateAccount(t *testing.T) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) require.NotNil(t, res.Data.Result) - account, err := repository.FindAccountByID(context.Background(), db, res.Data.Result.ID, domain.AccountQueryOpts{}) + account, err := repository.FindAccountByID(context.Background(), db, res.Data.Result.ID, nil) require.NoError(t, err) assert.Equal(t, "test@acme.com", account.EmailAddress) diff --git a/backend/api/graph/tests/admin/mutation_create_organisation_test.go b/backend/api/graph/tests/admin/mutation_create_organisation_test.go index 216dfc5..8f0620a 100644 --- a/backend/api/graph/tests/admin/mutation_create_organisation_test.go +++ b/backend/api/graph/tests/admin/mutation_create_organisation_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "myvendor.mytld/myproject/backend/api" - "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/test" test_auth "myvendor.mytld/myproject/backend/test/auth" @@ -56,7 +55,7 @@ func TestMutationResolver_CreateOrganisation(t *testing.T) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) require.NotNil(t, res.Data.Result) - organisation, err := repository.FindOrganisationByID(context.Background(), db, res.Data.Result.ID, domain.OrganisationQueryOpts{}) + organisation, err := repository.FindOrganisationByID(context.Background(), db, res.Data.Result.ID, nil) require.NoError(t, err) assert.Equal(t, "Next big thing", organisation.Name) diff --git a/backend/api/graph/tests/admin/mutation_delete_account_test.go b/backend/api/graph/tests/admin/mutation_delete_account_test.go index 724b8aa..b409640 100644 --- a/backend/api/graph/tests/admin/mutation_delete_account_test.go +++ b/backend/api/graph/tests/admin/mutation_delete_account_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" "myvendor.mytld/myproject/backend/api" - "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/test" test_auth "myvendor.mytld/myproject/backend/test/auth" @@ -65,7 +64,7 @@ func TestMutationResolver_DeleteAccount(t *testing.T) { expects: func(t *testing.T, db *sql.DB, auth test_auth.FixedAuthTokenData, res result) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) - _, err := repository.FindAccountByID(context.Background(), db, uuid.Must(uuid.FromString("3ad082c7-cbda-49e1-a707-c53e1962be65")), domain.AccountQueryOpts{}) + _, err := repository.FindAccountByID(context.Background(), db, uuid.Must(uuid.FromString("3ad082c7-cbda-49e1-a707-c53e1962be65")), nil) require.ErrorIs(t, err, repository.ErrNotFound) }, }, @@ -90,7 +89,7 @@ func TestMutationResolver_DeleteAccount(t *testing.T) { expects: func(t *testing.T, db *sql.DB, auth test_auth.FixedAuthTokenData, res result) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) - _, err := repository.FindAccountByID(context.Background(), db, uuid.Must(uuid.FromString("f045e5d1-cdad-4964-a7e2-139c8a87346c")), domain.AccountQueryOpts{}) + _, err := repository.FindAccountByID(context.Background(), db, uuid.Must(uuid.FromString("f045e5d1-cdad-4964-a7e2-139c8a87346c")), nil) require.ErrorIs(t, err, repository.ErrNotFound) }, }, diff --git a/backend/api/graph/tests/admin/mutation_delete_organisation_test.go b/backend/api/graph/tests/admin/mutation_delete_organisation_test.go index a73cc4d..20db3a7 100644 --- a/backend/api/graph/tests/admin/mutation_delete_organisation_test.go +++ b/backend/api/graph/tests/admin/mutation_delete_organisation_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/require" "myvendor.mytld/myproject/backend/api" - "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/test" test_auth "myvendor.mytld/myproject/backend/test/auth" @@ -55,7 +54,7 @@ func TestMutationResolver_DeleteOrganisation(t *testing.T) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) require.NotNil(t, res.Data.Result) - _, err := repository.FindOrganisationByID(context.Background(), db, res.Data.Result.ID, domain.OrganisationQueryOpts{}) + _, err := repository.FindOrganisationByID(context.Background(), db, res.Data.Result.ID, nil) require.ErrorIs(t, err, repository.ErrNotFound) }, }, diff --git a/backend/api/graph/tests/admin/mutation_update_account_test.go b/backend/api/graph/tests/admin/mutation_update_account_test.go index d2144d6..c62ebb1 100644 --- a/backend/api/graph/tests/admin/mutation_update_account_test.go +++ b/backend/api/graph/tests/admin/mutation_update_account_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "myvendor.mytld/myproject/backend/api" - "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/test" test_auth "myvendor.mytld/myproject/backend/test/auth" @@ -62,7 +61,7 @@ func TestMutationResolver_UpdateAccount(t *testing.T) { expects: func(t *testing.T, db *sql.DB, auth test_auth.FixedAuthTokenData, res result) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) - account, err := repository.FindAccountByID(context.Background(), db, uuid.Must(uuid.FromString("d7037ad0-d4bb-4dcc-8759-d82fbb3354e8")), domain.AccountQueryOpts{}) + account, err := repository.FindAccountByID(context.Background(), db, uuid.Must(uuid.FromString("d7037ad0-d4bb-4dcc-8759-d82fbb3354e8")), nil) require.NoError(t, err) assert.Equal(t, "test@acme.com", account.EmailAddress) @@ -82,7 +81,7 @@ func TestMutationResolver_UpdateAccount(t *testing.T) { expects: func(t *testing.T, db *sql.DB, auth test_auth.FixedAuthTokenData, res result) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) - account, err := repository.FindAccountByID(context.Background(), db, uuid.Must(uuid.FromString("3ad082c7-cbda-49e1-a707-c53e1962be65")), domain.AccountQueryOpts{}) + account, err := repository.FindAccountByID(context.Background(), db, uuid.Must(uuid.FromString("3ad082c7-cbda-49e1-a707-c53e1962be65")), nil) require.NoError(t, err) assert.Equal(t, "test@acme.com", account.EmailAddress) diff --git a/backend/api/graph/tests/admin/mutation_update_organisation_test.go b/backend/api/graph/tests/admin/mutation_update_organisation_test.go index 2b71bfe..009a1fd 100644 --- a/backend/api/graph/tests/admin/mutation_update_organisation_test.go +++ b/backend/api/graph/tests/admin/mutation_update_organisation_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "myvendor.mytld/myproject/backend/api" - "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/test" test_auth "myvendor.mytld/myproject/backend/test/auth" @@ -58,7 +57,7 @@ func TestMutationResolver_UpdateOrganisation(t *testing.T) { test_graphql.RequireNoErrors(t, res.GraphqlErrors) require.NotNil(t, res.Data.Result) - organisation, err := repository.FindOrganisationByID(context.Background(), db, res.Data.Result.ID, domain.OrganisationQueryOpts{}) + organisation, err := repository.FindOrganisationByID(context.Background(), db, res.Data.Result.ID, nil) require.NoError(t, err) assert.Equal(t, "Acme Ltd.", organisation.Name) diff --git a/backend/api/handler/error_presenter.go b/backend/api/handler/error_presenter.go index f83e78d..d5a7138 100644 --- a/backend/api/handler/error_presenter.go +++ b/backend/api/handler/error_presenter.go @@ -7,7 +7,7 @@ import ( "github.com/99designs/gqlgen/graphql" "github.com/vektah/gqlparser/v2/gqlerror" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) type extendedError interface { @@ -18,7 +18,7 @@ func ErrorPresenter(ctx context.Context, err error) *gqlerror.Error { graphqlErr := graphql.DefaultErrorPresenter(ctx, err) // A field resolvable error should be unwrapped before presenting - var fieldErr domain.FieldResolvableError + var fieldErr types.FieldResolvableError if errors.As(err, &fieldErr) { graphqlErr.Message = fieldErr.Error() } diff --git a/backend/api/http/middleware/auth_context.go b/backend/api/http/middleware/auth_context.go index 8dd1e7f..d096c52 100644 --- a/backend/api/http/middleware/auth_context.go +++ b/backend/api/http/middleware/auth_context.go @@ -12,14 +12,14 @@ import ( "github.com/gofrs/uuid" "myvendor.mytld/myproject/backend/api" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" ) // AuthContextMiddleware sets an auth context from a HTTP request // considering auth token and CSRF token -func AuthContextMiddleware(db *sql.DB, timeSource domain.TimeSource, next http.Handler) http.Handler { +func AuthContextMiddleware(db *sql.DB, timeSource types.TimeSource, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -50,7 +50,7 @@ func AuthContextMiddleware(db *sql.DB, timeSource domain.TimeSource, next http.H }) } -func authCtxFromToken(ctx context.Context, db *sql.DB, authTokenValue string, timeSource domain.TimeSource) (authCtx authentication.AuthContext) { +func authCtxFromToken(ctx context.Context, db *sql.DB, authTokenValue string, timeSource types.TimeSource) (authCtx authentication.AuthContext) { log := logger.FromContext(ctx) authToken, err := jwt.ParseSigned(authTokenValue, []jose.SignatureAlgorithm{jose.HS256}) @@ -76,7 +76,7 @@ func authCtxFromToken(ctx context.Context, db *sql.DB, authTokenValue string, ti return authentication.AuthContextWithError(api.ErrAuthTokenInvalid) } - account, err := repository.FindAccountByID(ctx, db, accountID, domain.AccountQueryOpts{}) + account, err := repository.FindAccountByID(ctx, db, accountID, nil) if err != nil { log. WithError(errors.WithStack(err)). @@ -129,7 +129,7 @@ func authCtxFromToken(ctx context.Context, db *sql.DB, authTokenValue string, ti return authCtx } -func checkCsrfToken(ctx context.Context, authCtx authentication.AuthContext, csrfTokenValue string, timeSource domain.TimeSource) error { +func checkCsrfToken(ctx context.Context, authCtx authentication.AuthContext, csrfTokenValue string, timeSource types.TimeSource) error { log := logger.FromContext(ctx) if csrfTokenValue == "" { diff --git a/backend/api/http/middleware/refresh_tokens.go b/backend/api/http/middleware/refresh_tokens.go index f8305e8..a943e0d 100644 --- a/backend/api/http/middleware/refresh_tokens.go +++ b/backend/api/http/middleware/refresh_tokens.go @@ -8,7 +8,7 @@ import ( logger "github.com/apex/log" "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" ) @@ -17,7 +17,7 @@ const ( AuthTokenRefreshThreshold = 15 * time.Minute ) -func RefreshTokensMiddleware(db *sql.DB, timeSource domain.TimeSource, next http.Handler) http.Handler { +func RefreshTokensMiddleware(db *sql.DB, timeSource types.TimeSource, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := logger.FromContext(ctx) @@ -40,8 +40,8 @@ func RefreshTokensMiddleware(db *sql.DB, timeSource domain.TimeSource, next http }) } -func refreshTokens(w http.ResponseWriter, r *http.Request, authCtx authentication.AuthContext, db *sql.DB, timeSource domain.TimeSource) error { - account, err := repository.FindAccountByID(r.Context(), db, authCtx.AccountID, domain.AccountQueryOpts{}) +func refreshTokens(w http.ResponseWriter, r *http.Request, authCtx authentication.AuthContext, db *sql.DB, timeSource types.TimeSource) error { + account, err := repository.FindAccountByID(r.Context(), db, authCtx.AccountID, nil) if err != nil { return errors.Wrap(err, "could not find account") } diff --git a/backend/api/resolver_dependencies.go b/backend/api/resolver_dependencies.go index 510b161..66839ef 100644 --- a/backend/api/resolver_dependencies.go +++ b/backend/api/resolver_dependencies.go @@ -6,6 +6,7 @@ import ( "go.opentelemetry.io/otel/metric" "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/mail" ) @@ -13,7 +14,7 @@ import ( type ResolverDependencies struct { Config domain.Config DB *sql.DB - TimeSource domain.TimeSource + TimeSource types.TimeSource MeterProvider metric.MeterProvider Mailer *mail.Mailer } diff --git a/backend/cli/ctl/cmd_account.go b/backend/cli/ctl/cmd_account.go index 7d42279..cfee8c5 100644 --- a/backend/cli/ctl/cmd_account.go +++ b/backend/cli/ctl/cmd_account.go @@ -10,7 +10,8 @@ import ( "github.com/urfave/cli/v2" "golang.org/x/term" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/handler" ) @@ -25,7 +26,7 @@ func newAccountCmd() *cli.Command { Flags: []cli.Flag{ &cli.StringFlag{ Name: "role", - Value: string(domain.RoleSystemAdministrator), + Value: string(types.RoleSystemAdministrator), }, &cli.StringFlag{ Name: "email", @@ -44,7 +45,7 @@ func newAccountCmd() *cli.Command { fmt.Println() //nolint:forbidigo password := strings.TrimSpace(string(line)) - cmd, err := domain.NewAccountCreateCmd(c.String("email"), domain.Role(c.String("role")), password) + cmd, err := command.NewAccountCreateCmd(c.String("email"), types.Role(c.String("role")), password) if err != nil { return err } diff --git a/backend/cli/ctl/cmd_account_list.go b/backend/cli/ctl/cmd_account_list.go index d67e185..e7c822c 100644 --- a/backend/cli/ctl/cmd_account_list.go +++ b/backend/cli/ctl/cmd_account_list.go @@ -7,7 +7,7 @@ import ( "github.com/gofrs/uuid" "github.com/urfave/cli/v2" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" ) @@ -37,9 +37,9 @@ func newAccountListCmd() *cli.Command { roleIdentifiers := c.StringSlice("role") - roles := make([]domain.Role, len(roleIdentifiers)) + roles := make([]types.Role, len(roleIdentifiers)) for i, roleIdentifier := range roleIdentifiers { - role := domain.Role(roleIdentifier) + role := types.Role(roleIdentifier) if !role.IsValid() { return errors.Errorf("invalid role %q", roleIdentifier) } diff --git a/backend/cli/ctl/main.go b/backend/cli/ctl/main.go index ae3c434..717c409 100644 --- a/backend/cli/ctl/main.go +++ b/backend/cli/ctl/main.go @@ -18,6 +18,7 @@ import ( "github.com/urfave/cli/v2" "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/mail" "myvendor.mytld/myproject/backend/mail/smtp" "myvendor.mytld/myproject/backend/security/authentication" @@ -103,7 +104,7 @@ func main() { // Pretend the CLI has a SystemAdministrator role (without setting an account) c.Context = authentication.WithAuthContext(c.Context, authentication.AuthContext{ Authenticated: true, - Role: domain.RoleSystemAdministrator, + Role: types.RoleSystemAdministrator, }) return nil diff --git a/backend/cli/ctl/time_source.go b/backend/cli/ctl/time_source.go index c6d26ba..8dcd1c3 100644 --- a/backend/cli/ctl/time_source.go +++ b/backend/cli/ctl/time_source.go @@ -5,7 +5,7 @@ import ( "github.com/urfave/cli/v2" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) type currentTimeSource struct{} @@ -14,7 +14,7 @@ func (cts currentTimeSource) Now() time.Time { return time.Now() } -func newCurrentTimeSource(_ *cli.Context) (domain.TimeSource, error) { +func newCurrentTimeSource(_ *cli.Context) (types.TimeSource, error) { // TODO Get location from CLI context and store in time source return currentTimeSource{}, nil diff --git a/backend/domain/cmd_account_create.go b/backend/domain/command/account_create_cmd.go similarity index 54% rename from backend/domain/cmd_account_create.go rename to backend/domain/command/account_create_cmd.go index fb756e3..384f6df 100644 --- a/backend/domain/cmd_account_create.go +++ b/backend/domain/command/account_create_cmd.go @@ -1,4 +1,4 @@ -package domain +package command import ( "strings" @@ -6,18 +6,21 @@ import ( "github.com/friendsofgo/errors" "github.com/gofrs/uuid" + "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/model" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/security/helper" ) type AccountCreateCmd struct { AccountID uuid.UUID EmailAddress string - Role Role + Role types.Role OrganisationID uuid.NullUUID password string } -func NewAccountCreateCmd(emailAddress string, role Role, password string) (cmd AccountCreateCmd, err error) { +func NewAccountCreateCmd(emailAddress string, role types.Role, password string) (cmd AccountCreateCmd, err error) { accountID, err := uuid.NewV4() if err != nil { return cmd, errors.Wrap(err, "generate account id") @@ -31,51 +34,51 @@ func NewAccountCreateCmd(emailAddress string, role Role, password string) (cmd A }, nil } -func (c AccountCreateCmd) Validate(_ Config) error { - if IsBlank(c.EmailAddress) { - return FieldError{ +func (c AccountCreateCmd) Validate(_ domain.Config) error { + if isBlank(c.EmailAddress) { + return types.FieldError{ Field: "emailAddress", - Code: ErrorCodeRequired, + Code: types.ErrorCodeRequired, } } - if IsBlank(c.password) { - return FieldError{ + if isBlank(c.password) { + return types.FieldError{ Field: "password", - Code: ErrorCodeRequired, + Code: types.ErrorCodeRequired, } } if err := helper.ValidatePassword(c.password); err != nil { - return FieldError{ + return types.FieldError{ Field: "password", Code: err.Error(), } } if !c.Role.IsValid() { - return FieldError{ + return types.FieldError{ Field: "role", - Code: ErrorCodeInvalid, + Code: types.ErrorCodeInvalid, } } // organisationID must be set iff role is not SystemAdministrator - if !((c.Role != RoleSystemAdministrator) == c.OrganisationID.Valid) { - return FieldError{ + if !((c.Role != types.RoleSystemAdministrator) == c.OrganisationID.Valid) { + return types.FieldError{ Field: "organisationId", - Code: ErrorCodeRequired, + Code: types.ErrorCodeRequired, } } return nil } -func (c AccountCreateCmd) NewAccount(config Config) (Account, error) { - accountSecret, err := newAccountSecret() +func (c AccountCreateCmd) NewAccount(config domain.Config) (model.Account, error) { + accountSecret, err := model.NewAccountSecret() if err != nil { - return Account{}, errors.Wrap(err, "generate account secret") + return model.Account{}, errors.Wrap(err, "generate account secret") } passwordHash, err := helper.GenerateHashFromPassword([]byte(c.password), config.HashCost) if err != nil { - return Account{}, errors.Wrap(err, "hashing password") + return model.Account{}, errors.Wrap(err, "hashing password") } - account := Account{ + account := model.Account{ ID: c.AccountID, EmailAddress: c.EmailAddress, Secret: accountSecret, diff --git a/backend/domain/cmd_account_delete.go b/backend/domain/command/account_delete_cmd.go similarity index 95% rename from backend/domain/cmd_account_delete.go rename to backend/domain/command/account_delete_cmd.go index 556cfa8..9b0ec9c 100644 --- a/backend/domain/cmd_account_delete.go +++ b/backend/domain/command/account_delete_cmd.go @@ -1,4 +1,4 @@ -package domain +package command import ( "github.com/gofrs/uuid" diff --git a/backend/domain/cmd_account_update.go b/backend/domain/command/account_update_cmd.go similarity index 59% rename from backend/domain/cmd_account_update.go rename to backend/domain/command/account_update_cmd.go index 35e30e9..9a291b7 100644 --- a/backend/domain/cmd_account_update.go +++ b/backend/domain/command/account_update_cmd.go @@ -1,17 +1,20 @@ -package domain +package command import ( "strings" "github.com/gofrs/uuid" + "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/model" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/security/helper" ) type AccountUpdateCmd struct { AccountID uuid.UUID EmailAddress string - Role Role + Role types.Role CurrentOrganisationID uuid.NullUUID NewOrganisationID uuid.NullUUID // Will be nil if not changed @@ -20,7 +23,7 @@ type AccountUpdateCmd struct { password string } -func NewAccountUpdateCmd(config Config, currentOrganisationID uuid.NullUUID, accountID uuid.UUID, emailAddress string, role Role, password string) (cmd AccountUpdateCmd, err error) { +func NewAccountUpdateCmd(config domain.Config, currentOrganisationID uuid.NullUUID, accountID uuid.UUID, emailAddress string, role types.Role, password string) (cmd AccountUpdateCmd, err error) { cmd = AccountUpdateCmd{ CurrentOrganisationID: currentOrganisationID, AccountID: accountID, @@ -33,7 +36,7 @@ func NewAccountUpdateCmd(config Config, currentOrganisationID uuid.NullUUID, acc if err != nil { return cmd, err } - cmd.Secret, err = newAccountSecret() + cmd.Secret, err = model.NewAccountSecret() if err != nil { return cmd, err } @@ -41,32 +44,32 @@ func NewAccountUpdateCmd(config Config, currentOrganisationID uuid.NullUUID, acc return cmd, nil } -func (c AccountUpdateCmd) Validate(_ Config) error { - if IsBlank(c.EmailAddress) { - return FieldError{ +func (c AccountUpdateCmd) Validate(_ domain.Config) error { + if isBlank(c.EmailAddress) { + return types.FieldError{ Field: "emailAddress", - Code: ErrorCodeRequired, + Code: types.ErrorCodeRequired, } } if c.password != "" { if err := helper.ValidatePassword(c.password); err != nil { - return FieldError{ + return types.FieldError{ Field: "password", Code: err.Error(), } } } if !c.Role.IsValid() { - return FieldError{ + return types.FieldError{ Field: "role", - Code: ErrorCodeInvalid, + Code: types.ErrorCodeInvalid, } } // organisationID must be set iff role is not SystemAdministrator - if !((c.Role != RoleSystemAdministrator) == c.NewOrganisationID.Valid) { - return FieldError{ + if !((c.Role != types.RoleSystemAdministrator) == c.NewOrganisationID.Valid) { + return types.FieldError{ Field: "organisationId", - Code: ErrorCodeRequired, + Code: types.ErrorCodeRequired, } } return nil diff --git a/backend/domain/cmd_login.go b/backend/domain/command/login_cmd.go similarity index 95% rename from backend/domain/cmd_login.go rename to backend/domain/command/login_cmd.go index 655fe0a..5ac4f4b 100644 --- a/backend/domain/cmd_login.go +++ b/backend/domain/command/login_cmd.go @@ -1,4 +1,4 @@ -package domain +package command import ( "github.com/gofrs/uuid" diff --git a/backend/domain/cmd_organisation_create.go b/backend/domain/command/organisation_create_cmd.go similarity index 77% rename from backend/domain/cmd_organisation_create.go rename to backend/domain/command/organisation_create_cmd.go index 180152d..088fcfb 100644 --- a/backend/domain/cmd_organisation_create.go +++ b/backend/domain/command/organisation_create_cmd.go @@ -1,8 +1,10 @@ -package domain +package command import ( "github.com/friendsofgo/errors" "github.com/gofrs/uuid" + + "myvendor.mytld/myproject/backend/domain/types" ) type OrganisationCreateCmd struct { @@ -22,10 +24,10 @@ func NewOrganisationCreateCmd() (cmd OrganisationCreateCmd, err error) { } func (c OrganisationCreateCmd) Validate() error { - if IsBlank(c.Name) { - return FieldError{ + if isBlank(c.Name) { + return types.FieldError{ Field: "name", - Code: ErrorCodeRequired, + Code: types.ErrorCodeRequired, } } diff --git a/backend/domain/cmd_organisation_delete.go b/backend/domain/command/organisation_delete_cmd.go similarity index 94% rename from backend/domain/cmd_organisation_delete.go rename to backend/domain/command/organisation_delete_cmd.go index d8d3e67..04c68a5 100644 --- a/backend/domain/cmd_organisation_delete.go +++ b/backend/domain/command/organisation_delete_cmd.go @@ -1,4 +1,4 @@ -package domain +package command import ( "github.com/gofrs/uuid" diff --git a/backend/domain/cmd_organisation_update.go b/backend/domain/command/organisation_update_cmd.go similarity index 59% rename from backend/domain/cmd_organisation_update.go rename to backend/domain/command/organisation_update_cmd.go index 2b750fb..03e78d4 100644 --- a/backend/domain/cmd_organisation_update.go +++ b/backend/domain/command/organisation_update_cmd.go @@ -1,7 +1,9 @@ -package domain +package command import ( "github.com/gofrs/uuid" + + "myvendor.mytld/myproject/backend/domain/types" ) type OrganisationUpdateCmd struct { @@ -10,10 +12,10 @@ type OrganisationUpdateCmd struct { } func (c OrganisationUpdateCmd) Validate() error { - if IsBlank(c.Name) { - return FieldError{ + if isBlank(c.Name) { + return types.FieldError{ Field: "name", - Code: ErrorCodeRequired, + Code: types.ErrorCodeRequired, } } diff --git a/backend/domain/command/validate.go b/backend/domain/command/validate.go new file mode 100644 index 0000000..35c10d1 --- /dev/null +++ b/backend/domain/command/validate.go @@ -0,0 +1,7 @@ +package command + +import "strings" + +func isBlank(s string) bool { + return strings.Trim(s, " ") == "" +} diff --git a/backend/domain/enums.go b/backend/domain/enums.go deleted file mode 100644 index 8e51f4d..0000000 --- a/backend/domain/enums.go +++ /dev/null @@ -1,5 +0,0 @@ -package domain - -import "errors" - -var ErrEnumsMustBeStrings = errors.New("enums must be strings") diff --git a/backend/domain/errors.go b/backend/domain/errors.go deleted file mode 100644 index def4b0b..0000000 --- a/backend/domain/errors.go +++ /dev/null @@ -1,43 +0,0 @@ -package domain - -import "fmt" - -type FieldResolvableError interface { - error - FieldPath() []string - ErrorArguments() []string - ErrorCode() string -} - -type FieldError struct { - Field string - Code string - Arguments []string -} - -func (n FieldError) FieldPath() []string { - return []string{n.Field} -} - -func (n FieldError) ErrorArguments() []string { - return n.Arguments -} - -func (n FieldError) Error() string { - return fmt.Sprintf("for field: %s: %s", n.Field, n.Code) -} - -func (n FieldError) ErrorCode() string { - return n.Code -} - -func (n FieldError) Extensions() map[string]interface{} { - return map[string]interface{}{ - "type": "validationFailed", - "code": n.Code, - "field": n.Field, - "arguments": n.Arguments, - } -} - -var _ FieldResolvableError = new(FieldError) //nolint:errcheck diff --git a/backend/domain/helper.go b/backend/domain/helper.go deleted file mode 100644 index bb928d2..0000000 --- a/backend/domain/helper.go +++ /dev/null @@ -1,30 +0,0 @@ -package domain - -import ( - "strings" - - "github.com/gofrs/uuid" -) - -func IsBlank(s string) bool { - return strings.Trim(s, " ") == "" -} - -type UUIDSet map[uuid.UUID]struct{} - -func NewUUIDSet(size ...int) UUIDSet { - n := 0 - if len(size) > 0 { - n = size[0] - } - return make(UUIDSet, n) -} - -func (s UUIDSet) Add(id uuid.UUID) { - s[id] = struct{}{} -} - -func (s UUIDSet) Has(id uuid.UUID) bool { - _, ok := s[id] - return ok -} diff --git a/backend/domain/account.go b/backend/domain/model/account.go similarity index 91% rename from backend/domain/account.go rename to backend/domain/model/account.go index 843a14c..2782aa6 100644 --- a/backend/domain/account.go +++ b/backend/domain/model/account.go @@ -1,4 +1,4 @@ -package domain +package model import ( "time" @@ -6,6 +6,7 @@ import ( "github.com/gofrs/uuid" "github.com/networkteam/construct/v2" + "myvendor.mytld/myproject/backend/domain/types" security_helper "myvendor.mytld/myproject/backend/security/helper" ) @@ -18,7 +19,7 @@ type Account struct { EmailAddress string `read_col:"accounts.email_address,sortable" write_col:"email_address"` Secret []byte `read_col:"accounts.secret" write_col:"secret"` PasswordHash []byte `read_col:"accounts.password_hash" write_col:"password_hash"` - Role Role `read_col:"accounts.role_identifier,sortable" write_col:"role_identifier"` + Role types.Role `read_col:"accounts.role_identifier,sortable" write_col:"role_identifier"` LastLogin *time.Time `read_col:"accounts.last_login,sortable" write_col:"last_login"` OrganisationID uuid.NullUUID `read_col:"accounts.organisation_id" write_col:"organisation_id"` @@ -56,6 +57,6 @@ func (a Account) GetPasswordHash() []byte { return a.PasswordHash } -func newAccountSecret() ([]byte, error) { +func NewAccountSecret() ([]byte, error) { return security_helper.GenerateRandomBytes(accountSecretLength) } diff --git a/backend/domain/organisation.go b/backend/domain/model/organisation.go similarity index 63% rename from backend/domain/organisation.go rename to backend/domain/model/organisation.go index 1b6c13a..e8b47e0 100644 --- a/backend/domain/organisation.go +++ b/backend/domain/model/organisation.go @@ -1,4 +1,4 @@ -package domain +package model import ( "time" @@ -16,16 +16,3 @@ type Organisation struct { CreatedAt time.Time `read_col:"organisations.created_at,sortable"` UpdatedAt time.Time `read_col:"organisations.updated_at,sortable"` } - -type OrganisationsQuery struct { - IDs []uuid.UUID - SearchTerm string -} - -func (f *OrganisationsQuery) SetOrganisationID(organisationID *uuid.UUID) { - if organisationID != nil { - f.IDs = []uuid.UUID{*organisationID} - } else { - f.IDs = nil - } -} diff --git a/backend/domain/query_account.go b/backend/domain/query/account_query.go similarity index 75% rename from backend/domain/query_account.go rename to backend/domain/query/account_query.go index 5382efc..04d54b2 100644 --- a/backend/domain/query_account.go +++ b/backend/domain/query/account_query.go @@ -1,22 +1,23 @@ -package domain +package query import ( "github.com/gofrs/uuid" ) type AccountQuery struct { - Opts AccountQueryOpts + Opts *AccountQueryOpts AccountID uuid.UUID } type AccountQueryNotAuthorized struct { - Opts AccountQueryOpts + Opts *AccountQueryOpts AccountID *uuid.UUID EmailAddress *string ConfirmationToken *string } type AccountsQuery struct { + Opts *AccountQueryOpts IDs []uuid.UUID SearchTerm string OrganisationID *uuid.UUID @@ -28,5 +29,5 @@ func (f *AccountsQuery) SetOrganisationID(organisationID *uuid.UUID) { type AccountQueryOpts struct { IncludeOrganisation bool - OrganisationQueryOpts OrganisationQueryOpts + OrganisationQueryOpts *OrganisationQueryOpts } diff --git a/backend/domain/query/organisation_query.go b/backend/domain/query/organisation_query.go new file mode 100644 index 0000000..5ca0fd8 --- /dev/null +++ b/backend/domain/query/organisation_query.go @@ -0,0 +1,25 @@ +package query + +import "github.com/gofrs/uuid" + +type OrganisationQuery struct { + Opts *OrganisationQueryOpts + OrganisationID uuid.UUID +} + +type OrganisationQueryOpts struct { +} + +type OrganisationsQuery struct { + Opts *OrganisationQueryOpts + IDs []uuid.UUID + SearchTerm string +} + +func (f *OrganisationsQuery) SetOrganisationID(organisationID *uuid.UUID) { + if organisationID != nil { + f.IDs = []uuid.UUID{*organisationID} + } else { + f.IDs = nil + } +} diff --git a/backend/domain/query_organisation.go b/backend/domain/query_organisation.go deleted file mode 100644 index a39af63..0000000 --- a/backend/domain/query_organisation.go +++ /dev/null @@ -1,10 +0,0 @@ -package domain - -import "github.com/gofrs/uuid" - -type OrganisationQuery struct { - OrganisationID uuid.UUID -} - -type OrganisationQueryOpts struct { -} diff --git a/backend/domain/date.go b/backend/domain/types/date.go similarity index 99% rename from backend/domain/date.go rename to backend/domain/types/date.go index 9cb493c..f946e9e 100644 --- a/backend/domain/date.go +++ b/backend/domain/types/date.go @@ -1,4 +1,4 @@ -package domain +package types // Copied from cloud.google.com/go/civil diff --git a/backend/domain/date_test.go b/backend/domain/types/date_test.go similarity index 61% rename from backend/domain/date_test.go rename to backend/domain/types/date_test.go index 9425a35..f07c04e 100644 --- a/backend/domain/date_test.go +++ b/backend/domain/types/date_test.go @@ -1,4 +1,4 @@ -package domain_test +package types_test import ( "encoding/json" @@ -7,30 +7,30 @@ import ( "github.com/stretchr/testify/assert" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) func TestDates(t *testing.T) { for _, test := range []struct { - date domain.Date + date types.Date loc *time.Location wantStr string wantTime time.Time }{ { - date: domain.Date{2014, 7, 29}, + date: types.Date{2014, 7, 29}, loc: time.Local, wantStr: "2014-07-29", wantTime: time.Date(2014, time.July, 29, 0, 0, 0, 0, time.Local), }, { - date: domain.DateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)), + date: types.DateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)), loc: time.UTC, wantStr: "2014-08-20", wantTime: time.Date(2014, 8, 20, 0, 0, 0, 0, time.UTC), }, { - date: domain.DateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)), + date: types.DateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)), loc: time.UTC, wantStr: "0999-01-26", wantTime: time.Date(999, 1, 26, 0, 0, 0, 0, time.UTC), @@ -47,21 +47,21 @@ func TestDates(t *testing.T) { func TestDateIsValid(t *testing.T) { for _, test := range []struct { - date domain.Date + date types.Date want bool }{ - {domain.Date{2014, 7, 29}, true}, - {domain.Date{2000, 2, 29}, true}, - {domain.Date{10000, 12, 31}, true}, - {domain.Date{1, 1, 1}, true}, - {domain.Date{0, 1, 1}, true}, // year zero is OK - {domain.Date{-1, 1, 1}, true}, // negative year is OK - {domain.Date{1, 0, 1}, false}, - {domain.Date{1, 1, 0}, false}, - {domain.Date{2016, 1, 32}, false}, - {domain.Date{2016, 13, 1}, false}, - {domain.Date{1, -1, 1}, false}, - {domain.Date{1, 1, -1}, false}, + {types.Date{2014, 7, 29}, true}, + {types.Date{2000, 2, 29}, true}, + {types.Date{10000, 12, 31}, true}, + {types.Date{1, 1, 1}, true}, + {types.Date{0, 1, 1}, true}, // year zero is OK + {types.Date{-1, 1, 1}, true}, // negative year is OK + {types.Date{1, 0, 1}, false}, + {types.Date{1, 1, 0}, false}, + {types.Date{2016, 1, 32}, false}, + {types.Date{2016, 13, 1}, false}, + {types.Date{1, -1, 1}, false}, + {types.Date{1, 1, -1}, false}, } { got := test.date.IsValid() if got != test.want { @@ -73,20 +73,20 @@ func TestDateIsValid(t *testing.T) { func TestParseDate(t *testing.T) { for _, test := range []struct { str string - want domain.Date // if empty, expect an error + want types.Date // if empty, expect an error }{ - {"2016-01-02", domain.Date{2016, 1, 2}}, - {"2016-12-31", domain.Date{2016, 12, 31}}, - {"0003-02-04", domain.Date{3, 2, 4}}, - {"999-01-26", domain.Date{}}, - {"", domain.Date{}}, - {"2016-01-02x", domain.Date{}}, + {"2016-01-02", types.Date{2016, 1, 2}}, + {"2016-12-31", types.Date{2016, 12, 31}}, + {"0003-02-04", types.Date{3, 2, 4}}, + {"999-01-26", types.Date{}}, + {"", types.Date{}}, + {"2016-01-02x", types.Date{}}, } { - got, err := domain.ParseDate(test.str) + got, err := types.ParseDate(test.str) if got != test.want { t.Errorf("ParseDate(%q) = %+v, want %+v", test.str, got, test.want) } - if err != nil && test.want != (domain.Date{}) { + if err != nil && test.want != (types.Date{}) { t.Errorf("Unexpected error %v from ParseDate(%q)", err, test.str) } } @@ -95,50 +95,50 @@ func TestParseDate(t *testing.T) { func TestDateArithmetic(t *testing.T) { for _, test := range []struct { desc string - start domain.Date - end domain.Date + start types.Date + end types.Date days int }{ { desc: "zero days noop", - start: domain.Date{2014, 5, 9}, - end: domain.Date{2014, 5, 9}, + start: types.Date{2014, 5, 9}, + end: types.Date{2014, 5, 9}, days: 0, }, { desc: "crossing a year boundary", - start: domain.Date{2014, 12, 31}, - end: domain.Date{2015, 1, 1}, + start: types.Date{2014, 12, 31}, + end: types.Date{2015, 1, 1}, days: 1, }, { desc: "negative number of days", - start: domain.Date{2015, 1, 1}, - end: domain.Date{2014, 12, 31}, + start: types.Date{2015, 1, 1}, + end: types.Date{2014, 12, 31}, days: -1, }, { desc: "full leap year", - start: domain.Date{2004, 1, 1}, - end: domain.Date{2005, 1, 1}, + start: types.Date{2004, 1, 1}, + end: types.Date{2005, 1, 1}, days: 366, }, { desc: "full non-leap year", - start: domain.Date{2001, 1, 1}, - end: domain.Date{2002, 1, 1}, + start: types.Date{2001, 1, 1}, + end: types.Date{2002, 1, 1}, days: 365, }, { desc: "crossing a leap second", - start: domain.Date{1972, 6, 30}, - end: domain.Date{1972, 7, 1}, + start: types.Date{1972, 6, 30}, + end: types.Date{1972, 7, 1}, days: 1, }, { desc: "dates before the unix epoch", - start: domain.Date{101, 1, 1}, - end: domain.Date{102, 1, 1}, + start: types.Date{101, 1, 1}, + end: types.Date{102, 1, 1}, days: 365, }, } { @@ -154,38 +154,38 @@ func TestDateArithmetic(t *testing.T) { func TestAddMonths(t *testing.T) { for _, test := range []struct { desc string - start domain.Date - end domain.Date + start types.Date + end types.Date months int }{ { desc: "zero months noop", - start: domain.Date{2014, 5, 9}, - end: domain.Date{2014, 5, 9}, + start: types.Date{2014, 5, 9}, + end: types.Date{2014, 5, 9}, months: 0, }, { desc: "crossing a year boundary", - start: domain.Date{2014, 12, 31}, - end: domain.Date{2015, 1, 31}, + start: types.Date{2014, 12, 31}, + end: types.Date{2015, 1, 31}, months: 1, }, { desc: "keeps amount of days", - start: domain.Date{2015, 1, 31}, - end: domain.Date{2015, 3, 3}, + start: types.Date{2015, 1, 31}, + end: types.Date{2015, 3, 3}, months: 1, }, { desc: "negative number of months", - start: domain.Date{2015, 1, 1}, - end: domain.Date{2014, 12, 1}, + start: types.Date{2015, 1, 1}, + end: types.Date{2014, 12, 1}, months: -1, }, { desc: "full year", - start: domain.Date{2004, 1, 1}, - end: domain.Date{2005, 1, 1}, + start: types.Date{2004, 1, 1}, + end: types.Date{2005, 1, 1}, months: 12, }, } { @@ -197,12 +197,12 @@ func TestAddMonths(t *testing.T) { func TestDateBefore(t *testing.T) { for _, test := range []struct { - d1, d2 domain.Date + d1, d2 types.Date want bool }{ - {domain.Date{2016, 12, 31}, domain.Date{2017, 1, 1}, true}, - {domain.Date{2016, 1, 1}, domain.Date{2016, 1, 1}, false}, - {domain.Date{2016, 12, 30}, domain.Date{2016, 12, 31}, true}, + {types.Date{2016, 12, 31}, types.Date{2017, 1, 1}, true}, + {types.Date{2016, 1, 1}, types.Date{2016, 1, 1}, false}, + {types.Date{2016, 12, 30}, types.Date{2016, 12, 31}, true}, } { if got := test.d1.Before(test.d2); got != test.want { t.Errorf("%v.Before(%v): got %t, want %t", test.d1, test.d2, got, test.want) @@ -212,12 +212,12 @@ func TestDateBefore(t *testing.T) { func TestDateAfter(t *testing.T) { for _, test := range []struct { - d1, d2 domain.Date + d1, d2 types.Date want bool }{ - {domain.Date{2016, 12, 31}, domain.Date{2017, 1, 1}, false}, - {domain.Date{2016, 1, 1}, domain.Date{2016, 1, 1}, false}, - {domain.Date{2016, 12, 30}, domain.Date{2016, 12, 31}, false}, + {types.Date{2016, 12, 31}, types.Date{2017, 1, 1}, false}, + {types.Date{2016, 1, 1}, types.Date{2016, 1, 1}, false}, + {types.Date{2016, 12, 30}, types.Date{2016, 12, 31}, false}, } { if got := test.d1.After(test.d2); got != test.want { t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want) @@ -230,7 +230,7 @@ func TestMarshalJSON(t *testing.T) { value interface{} want string }{ - {domain.Date{1987, 4, 15}, `"1987-04-15"`}, + {types.Date{1987, 4, 15}, `"1987-04-15"`}, } { bgot, err := json.Marshal(test.value) if err != nil { @@ -243,14 +243,14 @@ func TestMarshalJSON(t *testing.T) { } func TestUnmarshalJSON(t *testing.T) { - var d domain.Date + var d types.Date for _, test := range []struct { data string ptr interface{} want interface{} }{ - {`"1987-04-15"`, &d, &domain.Date{1987, 4, 15}}, - {`"1987-04-\u0031\u0035"`, &d, &domain.Date{1987, 4, 15}}, + {`"1987-04-15"`, &d, &types.Date{1987, 4, 15}}, + {`"1987-04-\u0031\u0035"`, &d, &types.Date{1987, 4, 15}}, } { if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil { t.Fatalf("%s: %v", test.data, err) @@ -270,7 +270,7 @@ func TestUnmarshalJSON(t *testing.T) { } func TestDateAt(t *testing.T) { - d := domain.Date{ + d := types.Date{ Year: 2022, Month: 1, Day: 28, diff --git a/backend/domain/error_codes.go b/backend/domain/types/error_codes.go similarity index 98% rename from backend/domain/error_codes.go rename to backend/domain/types/error_codes.go index d8bccc6..f966c61 100644 --- a/backend/domain/error_codes.go +++ b/backend/domain/types/error_codes.go @@ -1,4 +1,4 @@ -package domain +package types const ErrorCodeRequired = "required" const ErrorCodeAlreadyExists = "alreadyExists" diff --git a/backend/domain/types/errors.go b/backend/domain/types/errors.go new file mode 100644 index 0000000..e33fdea --- /dev/null +++ b/backend/domain/types/errors.go @@ -0,0 +1,127 @@ +package types + +import ( + "errors" + "fmt" +) + +var ErrEnumsMustBeStrings = errors.New("enums must be strings") + +type FieldResolvableError interface { + error + FieldPath() []string + ErrorArguments() []string + ErrorCode() string + Is(error) bool +} + +type FieldError struct { + // Field name to identify the field for the error. + // Can be omitted for anonymous errors that can be wrapped later with a specific field name. + Field string + Code string + Arguments []string +} + +func (n FieldError) FieldPath() []string { + if n.Field == "" { + return nil + } + return []string{n.Field} +} + +func (n FieldError) ErrorArguments() []string { + return n.Arguments +} + +func (n FieldError) Error() string { + if n.Field == "" { + return n.Code + } + return fmt.Sprintf("field %s: %s", n.Field, n.Code) +} + +func (n FieldError) ErrorCode() string { + return n.Code +} + +func (n FieldError) Is(err error) bool { + return fieldResolvableErrorIs(n, err) +} + +func fieldResolvableErrorIs(fieldErr FieldResolvableError, err error) bool { + newErr, ok := err.(FieldResolvableError) //nolint:errorlint + if !ok { + return false + } + if !stringsEqual(fieldErr.FieldPath(), newErr.FieldPath()) { + return false + } + if fieldErr.ErrorCode() != newErr.ErrorCode() { + return false + } + if !stringsEqual(fieldErr.ErrorArguments(), newErr.ErrorArguments()) { + return false + } + return true +} + +func (n FieldError) Extensions() map[string]any { + return map[string]any{ + "type": "validationFailed", + "code": n.Code, + "field": n.Field, + "arguments": n.Arguments, + } +} + +var _ FieldResolvableError = &FieldError{} + +type nestedFieldError struct { + field string + err FieldResolvableError +} + +func (n nestedFieldError) Error() string { + if n.field == "" { + return n.err.Error() + } + return fmt.Sprintf("field %s: %s", n.field, n.err.Error()) +} + +func (n nestedFieldError) FieldPath() []string { + return append([]string{n.field}, n.err.FieldPath()...) +} + +func (n nestedFieldError) ErrorArguments() []string { + return n.err.ErrorArguments() +} + +func (n nestedFieldError) ErrorCode() string { + return n.err.ErrorCode() +} + +func (n nestedFieldError) Is(err error) bool { + return fieldResolvableErrorIs(n, err) +} + +var _ FieldResolvableError = &nestedFieldError{} + +func WrapFieldError(err FieldResolvableError, fieldName string) FieldResolvableError { + return &nestedFieldError{ + field: fieldName, + err: err, + } +} + +func stringsEqual(s1 []string, s2 []string) bool { + if len(s1) != len(s2) { + return false + } + for i, s := range s1 { + if s != s2[i] { + return false + } + } + return true +} diff --git a/backend/domain/role.go b/backend/domain/types/role.go similarity index 98% rename from backend/domain/role.go rename to backend/domain/types/role.go index 61a0424..35268a1 100644 --- a/backend/domain/role.go +++ b/backend/domain/types/role.go @@ -1,4 +1,4 @@ -package domain +package types import ( "errors" diff --git a/backend/domain/time_source.go b/backend/domain/types/time_source.go similarity index 80% rename from backend/domain/time_source.go rename to backend/domain/types/time_source.go index bc25627..3192cd5 100644 --- a/backend/domain/time_source.go +++ b/backend/domain/types/time_source.go @@ -1,4 +1,4 @@ -package domain +package types import "time" diff --git a/backend/finder/account_finder.go b/backend/finder/account_finder.go index 458752a..91e878e 100644 --- a/backend/finder/account_finder.go +++ b/backend/finder/account_finder.go @@ -5,13 +5,14 @@ import ( "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/model" + domain_query "myvendor.mytld/myproject/backend/domain/query" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) -func (f *Finder) QueryAccount(ctx context.Context, query domain.AccountQuery) (domain.Account, error) { +func (f *Finder) QueryAccount(ctx context.Context, query domain_query.AccountQuery) (model.Account, error) { record, err := repository.FindAccountByID(ctx, f.executor, query.AccountID, query.Opts) if err != nil { return record, err @@ -23,7 +24,7 @@ func (f *Finder) QueryAccount(ctx context.Context, query domain.AccountQuery) (d return record, nil } -func (f *Finder) QueryAccountNotAuthorized(ctx context.Context, query domain.AccountQueryNotAuthorized) (domain.Account, error) { +func (f *Finder) QueryAccountNotAuthorized(ctx context.Context, query domain_query.AccountQueryNotAuthorized) (model.Account, error) { if query.AccountID != nil { return repository.FindAccountByID(ctx, f.executor, *query.AccountID, query.Opts) } @@ -32,10 +33,10 @@ func (f *Finder) QueryAccountNotAuthorized(ctx context.Context, query domain.Acc return repository.FindAccountByEmailAddress(ctx, f.executor, *query.EmailAddress, query.Opts) } - return domain.Account{}, errors.Wrap(ErrInvalidQuery, "AccountID or EmailAddress must be set") + return model.Account{}, errors.Wrap(ErrInvalidQuery, "AccountID or EmailAddress must be set") } -func (f *Finder) QueryAccounts(ctx context.Context, query domain.AccountsQuery, paging Paging) ([]domain.Account, error) { +func (f *Finder) QueryAccounts(ctx context.Context, query domain_query.AccountsQuery, paging Paging) ([]model.Account, error) { authorizer := authorization.NewAuthorizer(authentication.GetAuthContext(ctx)) err := authorizer.AllowsAndFilterAllAccountsQuery(&query) if err != nil { @@ -43,14 +44,14 @@ func (f *Finder) QueryAccounts(ctx context.Context, query domain.AccountsQuery, } return repository.FindAllAccounts(ctx, f.executor, repository.AccountsFilter{ - Opts: domain.AccountQueryOpts{}, + Opts: query.Opts, OrganisationID: query.OrganisationID, IDs: query.IDs, SearchTerm: query.SearchTerm, }, paging.options()...) } -func (f *Finder) CountAccounts(ctx context.Context, query domain.AccountsQuery) (int, error) { +func (f *Finder) CountAccounts(ctx context.Context, query domain_query.AccountsQuery) (int, error) { authorizer := authorization.NewAuthorizer(authentication.GetAuthContext(ctx)) err := authorizer.AllowsAndFilterAllAccountsQuery(&query) if err != nil { diff --git a/backend/finder/finder.go b/backend/finder/finder.go index 6cff981..badb987 100644 --- a/backend/finder/finder.go +++ b/backend/finder/finder.go @@ -8,18 +8,18 @@ import ( "github.com/friendsofgo/errors" "github.com/networkteam/qrb/qrbsql" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" ) // Finder is a higher level executor for queries that includes authorization. type Finder struct { executor qrbsql.Executor - timeSource domain.TimeSource + timeSource types.TimeSource } // NewFinder creates a new Finder. -func NewFinder(db *sql.DB, timeSource domain.TimeSource) *Finder { +func NewFinder(db *sql.DB, timeSource types.TimeSource) *Finder { return &Finder{ executor: db, timeSource: timeSource, diff --git a/backend/finder/organisation_finder.go b/backend/finder/organisation_finder.go index 57ad26b..1d5356e 100644 --- a/backend/finder/organisation_finder.go +++ b/backend/finder/organisation_finder.go @@ -3,40 +3,41 @@ package finder import ( "context" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/model" + domain_query "myvendor.mytld/myproject/backend/domain/query" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) -func (f *Finder) QueryOrganisation(ctx context.Context, query domain.OrganisationQuery) (domain.Organisation, error) { +func (f *Finder) QueryOrganisation(ctx context.Context, query domain_query.OrganisationQuery) (model.Organisation, error) { authorizer := authorization.NewAuthorizer(authentication.GetAuthContext(ctx)) err := authorizer.AllowsOrganisationQuery(query) if err != nil { - return domain.Organisation{}, err + return model.Organisation{}, err } - record, err := repository.FindOrganisationByID(ctx, f.executor, query.OrganisationID, domain.OrganisationQueryOpts{}) + record, err := repository.FindOrganisationByID(ctx, f.executor, query.OrganisationID, query.Opts) if err != nil { return record, err } return record, nil } -func (f *Finder) QueryOrganisations(ctx context.Context, query domain.OrganisationsQuery, paging Paging) ([]domain.Organisation, error) { +func (f *Finder) QueryOrganisations(ctx context.Context, query domain_query.OrganisationsQuery, paging Paging) ([]model.Organisation, error) { authorizer := authorization.NewAuthorizer(authentication.GetAuthContext(ctx)) err := authorizer.AllowsAndFilterAllOrganisationsQuery(&query) if err != nil { return nil, err } return repository.FindAllOrganisations(ctx, f.executor, repository.OrganisationsFilter{ - Opts: domain.OrganisationQueryOpts{}, + Opts: query.Opts, IDs: query.IDs, SearchTerm: query.SearchTerm, }, paging.options()...) } -func (f *Finder) CountOrganisations(ctx context.Context, query domain.OrganisationsQuery) (int, error) { +func (f *Finder) CountOrganisations(ctx context.Context, query domain_query.OrganisationsQuery) (int, error) { authorizer := authorization.NewAuthorizer(authentication.GetAuthContext(ctx)) err := authorizer.AllowsAndFilterAllOrganisationsQuery(&query) if err != nil { diff --git a/backend/go.mod b/backend/go.mod index 39c7171..4850c69 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,7 +5,6 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/apex/log v1.9.0 github.com/boumenot/gocover-cobertura v1.2.0 - github.com/davecgh/go-spew v1.1.1 github.com/friendsofgo/errors v0.9.2 github.com/getsentry/sentry-go v0.28.1 github.com/go-jose/go-jose/v4 v4.0.3 @@ -57,6 +56,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/dave/jennifer v1.7.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dnephin/pflag v1.0.7 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fatih/structtag v1.2.0 // indirect diff --git a/backend/gqlgen.yml b/backend/gqlgen.yml index dc8c928..6ab72e2 100644 --- a/backend/gqlgen.yml +++ b/backend/gqlgen.yml @@ -55,4 +55,4 @@ models: - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 Role: - model: myvendor.mytld/myproject/backend/domain.Role + model: myvendor.mytld/myproject/backend/domain/types.Role diff --git a/backend/handler/account_create_handler.go b/backend/handler/account_create_handler.go index 52fcf58..e0d2f36 100644 --- a/backend/handler/account_create_handler.go +++ b/backend/handler/account_create_handler.go @@ -7,13 +7,13 @@ import ( logger "github.com/apex/log" "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) -func (h *Handler) AccountCreate(ctx context.Context, cmd domain.AccountCreateCmd) error { +func (h *Handler) AccountCreate(ctx context.Context, cmd command.AccountCreateCmd) error { log := logger.FromContext(ctx). WithField("component", "handler"). WithField("handler", "AccountCreate") diff --git a/backend/handler/account_delete_handler.go b/backend/handler/account_delete_handler.go index d676013..9b3a128 100644 --- a/backend/handler/account_delete_handler.go +++ b/backend/handler/account_delete_handler.go @@ -7,13 +7,14 @@ import ( logger "github.com/apex/log" "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) -func (h *Handler) AccountDelete(ctx context.Context, cmd domain.AccountDeleteCmd) error { +func (h *Handler) AccountDelete(ctx context.Context, cmd command.AccountDeleteCmd) error { log := logger.FromContext(ctx). WithField("component", "handler"). WithField("handler", "AccountDelete") @@ -30,14 +31,14 @@ func (h *Handler) AccountDelete(ctx context.Context, cmd domain.AccountDeleteCmd var ( organisationID string emailAddress string - role domain.Role + role types.Role ) err := repository.Transactional(ctx, h.db, func(tx *sql.Tx) error { - record, err := repository.FindAccountByID(ctx, tx, cmd.AccountID, domain.AccountQueryOpts{}) + record, err := repository.FindAccountByID(ctx, tx, cmd.AccountID, nil) if errors.Is(err, repository.ErrNotFound) { - return domain.FieldError{ + return types.FieldError{ Field: "accountId", - Code: domain.ErrorCodeNotExists, + Code: types.ErrorCodeNotExists, } } else if err != nil { return errors.Wrap(err, "fetching account") diff --git a/backend/handler/account_update_handler.go b/backend/handler/account_update_handler.go index 0d9b56c..8bf5254 100644 --- a/backend/handler/account_update_handler.go +++ b/backend/handler/account_update_handler.go @@ -7,13 +7,14 @@ import ( logger "github.com/apex/log" "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) -func (h *Handler) AccountUpdate(ctx context.Context, cmd domain.AccountUpdateCmd) error { +func (h *Handler) AccountUpdate(ctx context.Context, cmd command.AccountUpdateCmd) error { log := logger.FromContext(ctx). WithField("component", "handler"). WithField("handler", "AccountUpdate") @@ -37,11 +38,11 @@ func (h *Handler) AccountUpdate(ctx context.Context, cmd domain.AccountUpdateCmd prevRole string ) err := repository.Transactional(ctx, h.db, func(tx *sql.Tx) error { - prevRecord, err := repository.FindAccountByID(ctx, tx, cmd.AccountID, domain.AccountQueryOpts{}) + prevRecord, err := repository.FindAccountByID(ctx, tx, cmd.AccountID, nil) if errors.Is(err, repository.ErrNotFound) { - return domain.FieldError{ + return types.FieldError{ Field: "accountId", - Code: domain.ErrorCodeNotExists, + Code: types.ErrorCodeNotExists, } } else if err != nil { return errors.Wrap(err, "finding account") diff --git a/backend/handler/handler.go b/backend/handler/handler.go index 0676e7e..6ec9ce1 100644 --- a/backend/handler/handler.go +++ b/backend/handler/handler.go @@ -6,12 +6,13 @@ import ( "go.opentelemetry.io/otel/metric" "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/mail" ) type Handler struct { db *sql.DB - timeSource domain.TimeSource + timeSource types.TimeSource mailer *mail.Mailer config domain.Config @@ -19,7 +20,7 @@ type Handler struct { } type Deps struct { - TimeSource domain.TimeSource + TimeSource types.TimeSource Mailer *mail.Mailer MeterProvider metric.MeterProvider } diff --git a/backend/handler/login_handler.go b/backend/handler/login_handler.go index 9d5e511..90db4d2 100644 --- a/backend/handler/login_handler.go +++ b/backend/handler/login_handler.go @@ -7,14 +7,16 @@ import ( logger "github.com/apex/log" fog_errors "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/model" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" security_helper "myvendor.mytld/myproject/backend/security/helper" ) var ErrLoginInvalidCredentials = std_errors.New("invalid credentials") -func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { +func (h *Handler) Login(ctx context.Context, cmd command.LoginCmd) (err error) { log := logger. FromContext(ctx). WithField("handler", "login") @@ -26,7 +28,7 @@ func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { account := cmd.Account if cmd.Account == nil { // Use an empty user to have constant password compare times - account = domain.Account{ + account = model.Account{ PasswordHash: security_helper.DefaultHashForComparison(h.config.HashCost), } } @@ -37,7 +39,7 @@ func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { if cmd.Account == nil { log. WithField("emailAddress", cmd.EmailAddress). - WithField("errorCode", domain.ErrorCodeNotExists). + WithField("errorCode", types.ErrorCodeNotExists). Warn("Login failed, account not found") } else { log. diff --git a/backend/handler/organisation_create_handler.go b/backend/handler/organisation_create_handler.go index c47e058..68c7035 100644 --- a/backend/handler/organisation_create_handler.go +++ b/backend/handler/organisation_create_handler.go @@ -7,13 +7,13 @@ import ( logger "github.com/apex/log" "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) -func (h *Handler) OrganisationCreate(ctx context.Context, cmd domain.OrganisationCreateCmd) error { +func (h *Handler) OrganisationCreate(ctx context.Context, cmd command.OrganisationCreateCmd) error { log := logger.FromContext(ctx). WithField("component", "handler"). WithField("handler", "OrganisationCreate") diff --git a/backend/handler/organisation_delete_handler.go b/backend/handler/organisation_delete_handler.go index 3d71dd9..f57db31 100644 --- a/backend/handler/organisation_delete_handler.go +++ b/backend/handler/organisation_delete_handler.go @@ -7,13 +7,14 @@ import ( logger "github.com/apex/log" "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) -func (h *Handler) OrganisationDelete(ctx context.Context, cmd domain.OrganisationDeleteCmd) error { +func (h *Handler) OrganisationDelete(ctx context.Context, cmd command.OrganisationDeleteCmd) error { log := logger.FromContext(ctx). WithField("component", "handler"). WithField("handler", "OrganisationDelete") @@ -29,11 +30,11 @@ func (h *Handler) OrganisationDelete(ctx context.Context, cmd domain.Organisatio var organisationName string err := repository.Transactional(ctx, h.db, func(tx *sql.Tx) error { - prevRecord, err := repository.FindOrganisationByID(ctx, tx, cmd.OrganisationID, domain.OrganisationQueryOpts{}) + prevRecord, err := repository.FindOrganisationByID(ctx, tx, cmd.OrganisationID, nil) if errors.Is(err, repository.ErrNotFound) { - return domain.FieldError{ + return types.FieldError{ Field: "organisationId", - Code: domain.ErrorCodeNotExists, + Code: types.ErrorCodeNotExists, } } else if err != nil { return errors.Wrap(err, "finding organisation") diff --git a/backend/handler/organisation_update_handler.go b/backend/handler/organisation_update_handler.go index fead398..64a9738 100644 --- a/backend/handler/organisation_update_handler.go +++ b/backend/handler/organisation_update_handler.go @@ -7,13 +7,14 @@ import ( logger "github.com/apex/log" "github.com/friendsofgo/errors" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/persistence/repository" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) -func (h *Handler) OrganisationUpdate(ctx context.Context, cmd domain.OrganisationUpdateCmd) error { +func (h *Handler) OrganisationUpdate(ctx context.Context, cmd command.OrganisationUpdateCmd) error { log := logger.FromContext(ctx). WithField("component", "handler"). WithField("handler", "OrganisationUpdate") @@ -33,11 +34,11 @@ func (h *Handler) OrganisationUpdate(ctx context.Context, cmd domain.Organisatio var prevOrganisationName string err := repository.Transactional(ctx, h.db, func(tx *sql.Tx) error { - prevRecord, err := repository.FindOrganisationByID(ctx, tx, cmd.OrganisationID, domain.OrganisationQueryOpts{}) + prevRecord, err := repository.FindOrganisationByID(ctx, tx, cmd.OrganisationID, nil) if errors.Is(err, repository.ErrNotFound) { - return domain.FieldError{ + return types.FieldError{ Field: "organisationId", - Code: domain.ErrorCodeNotExists, + Code: types.ErrorCodeNotExists, } } else if err != nil { return errors.Wrap(err, "finding organisation") diff --git a/backend/persistence/repository/account_repository.go b/backend/persistence/repository/account_repository.go index beb23a4..1d51dfd 100644 --- a/backend/persistence/repository/account_repository.go +++ b/backend/persistence/repository/account_repository.go @@ -13,20 +13,26 @@ import ( "github.com/networkteam/qrb/fn" "github.com/networkteam/qrb/qrbsql" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/model" + domain_query "myvendor.mytld/myproject/backend/domain/query" + "myvendor.mytld/myproject/backend/domain/types" ) type AccountsFilter struct { - Opts domain.AccountQueryOpts + Opts *domain_query.AccountQueryOpts OrganisationID *uuid.UUID IDs []uuid.UUID // SearchTerm filters accounts by text fields (email address or organisation name) SearchTerm string // Roles filters account to have one of the given roles - Roles []domain.Role + Roles []types.Role } -func accountBuildFindQuery(opts domain.AccountQueryOpts) builder.SelectBuilder { +func accountBuildFindQuery(opts *domain_query.AccountQueryOpts) builder.SelectBuilder { + if opts == nil { + opts = &domain_query.AccountQueryOpts{} + } + return Select(buildAccountJSON(opts)). From(account). ApplyIf(opts.IncludeOrganisation, func(q builder.SelectBuilder) builder.SelectBuilder { @@ -34,20 +40,20 @@ func accountBuildFindQuery(opts domain.AccountQueryOpts) builder.SelectBuilder { }) } -func FindAccountByID(ctx context.Context, executor qrbsql.Executor, id uuid.UUID, opts domain.AccountQueryOpts) (domain.Account, error) { +func FindAccountByID(ctx context.Context, executor qrbsql.Executor, id uuid.UUID, opts *domain_query.AccountQueryOpts) (model.Account, error) { query := accountBuildFindQuery(opts). Where(account.ID.Eq(Arg(id))) - return constructsql.ScanRow[domain.Account]( + return constructsql.ScanRow[model.Account]( qrbsql.Build(query).WithExecutor(executor).QueryRow(ctx), ) } -func FindAccountByEmailAddress(ctx context.Context, executor qrbsql.Executor, emailAddress string, opts domain.AccountQueryOpts) (domain.Account, error) { +func FindAccountByEmailAddress(ctx context.Context, executor qrbsql.Executor, emailAddress string, opts *domain_query.AccountQueryOpts) (model.Account, error) { query := accountBuildFindQuery(opts). Where(fn.Lower(account.EmailAddress).Eq(Arg(strings.ToLower(emailAddress)))) - return constructsql.ScanRow[domain.Account]( + return constructsql.ScanRow[model.Account]( qrbsql.Build(query).WithExecutor(executor).QueryRow(ctx), ) } @@ -60,7 +66,7 @@ func applyAccountFilter(filter AccountsFilter) func(q builder.SelectBuilder) bui }). ApplyIf(filter.SearchTerm != "", func(q builder.SelectBuilder) builder.SelectBuilder { var incOrg builder.Exp - if filter.Opts.IncludeOrganisation { + if filter.Opts != nil && filter.Opts.IncludeOrganisation { incOrg = organisation.Name.ILike(Arg("%" + filter.SearchTerm + "%")) } @@ -78,7 +84,7 @@ func applyAccountFilter(filter AccountsFilter) func(q builder.SelectBuilder) bui } } -func FindAllAccounts(ctx context.Context, executor qrbsql.Executor, filter AccountsFilter, pagingOpts ...PagingOption) ([]domain.Account, error) { +func FindAllAccounts(ctx context.Context, executor qrbsql.Executor, filter AccountsFilter, pagingOpts ...PagingOption) ([]model.Account, error) { query := accountBuildFindQuery(filter.Opts). ApplyIf(true, applyAccountFilter(filter)) @@ -87,7 +93,7 @@ func FindAllAccounts(ctx context.Context, executor qrbsql.Executor, filter Accou return nil, err } - return constructsql.CollectRows[domain.Account]( + return constructsql.CollectRows[model.Account]( qrbsql.Build(query).WithExecutor(executor).Query(ctx), ) } @@ -133,16 +139,20 @@ func AccountConstraintErr(err error) error { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { if pgErr.Code == pgErrCode_unique_violation && pgErr.ConstraintName == "accounts_email_address_idx" { - return domain.FieldError{ + return types.FieldError{ Field: "emailAddress", - Code: domain.ErrorCodeAlreadyExists, + Code: types.ErrorCodeAlreadyExists, } } } return nil } -func buildAccountJSON(opts domain.AccountQueryOpts) builder.JsonBuildObjectBuilder { +func buildAccountJSON(opts *domain_query.AccountQueryOpts) builder.JsonBuildObjectBuilder { + if opts == nil { + opts = &domain_query.AccountQueryOpts{} + } + return accountDefaultJson. PropIf( opts.IncludeOrganisation, diff --git a/backend/persistence/repository/mappings_account_gen.go b/backend/persistence/repository/mappings_account_gen.go index 27158f6..bb5a907 100644 --- a/backend/persistence/repository/mappings_account_gen.go +++ b/backend/persistence/repository/mappings_account_gen.go @@ -6,7 +6,10 @@ import ( qrb "github.com/networkteam/qrb" builder "github.com/networkteam/qrb/builder" fn "github.com/networkteam/qrb/fn" - domain "myvendor.mytld/myproject/backend/domain" + + "myvendor.mytld/myproject/backend/domain/model" + domain "myvendor.mytld/myproject/backend/domain/types" + "time" ) @@ -78,7 +81,7 @@ func (c AccountChangeSet) toMap() map[string]interface{} { return m } -func AccountToChangeSet(r domain.Account) (c AccountChangeSet) { +func AccountToChangeSet(r model.Account) (c AccountChangeSet) { if r.ID != uuid.Nil { c.ID = &r.ID } diff --git a/backend/persistence/repository/mappings_organisation_gen.go b/backend/persistence/repository/mappings_organisation_gen.go index df14dfb..e76f9ee 100644 --- a/backend/persistence/repository/mappings_organisation_gen.go +++ b/backend/persistence/repository/mappings_organisation_gen.go @@ -6,7 +6,8 @@ import ( qrb "github.com/networkteam/qrb" builder "github.com/networkteam/qrb/builder" fn "github.com/networkteam/qrb/fn" - domain "myvendor.mytld/myproject/backend/domain" + + domain "myvendor.mytld/myproject/backend/domain/model" ) var organisation = struct { diff --git a/backend/persistence/repository/organisation_repository.go b/backend/persistence/repository/organisation_repository.go index 886f665..f51c3e6 100644 --- a/backend/persistence/repository/organisation_repository.go +++ b/backend/persistence/repository/organisation_repository.go @@ -10,26 +10,27 @@ import ( "github.com/networkteam/qrb/fn" "github.com/networkteam/qrb/qrbsql" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/model" + domain_query "myvendor.mytld/myproject/backend/domain/query" ) type OrganisationsFilter struct { - Opts domain.OrganisationQueryOpts + Opts *domain_query.OrganisationQueryOpts IDs []uuid.UUID SearchTerm string } -func organisationBuildFindQuery(opts domain.OrganisationQueryOpts) builder.SelectBuilder { +func organisationBuildFindQuery(opts *domain_query.OrganisationQueryOpts) builder.SelectBuilder { return Select(buildOrganisationJSON(opts)). From(organisation). SelectBuilder } -func FindOrganisationByID(ctx context.Context, executor qrbsql.Executor, id uuid.UUID, opts domain.OrganisationQueryOpts) (domain.Organisation, error) { +func FindOrganisationByID(ctx context.Context, executor qrbsql.Executor, id uuid.UUID, opts *domain_query.OrganisationQueryOpts) (model.Organisation, error) { query := organisationBuildFindQuery(opts). Where(organisation.ID.Eq(Arg(id))) - return constructsql.ScanRow[domain.Organisation]( + return constructsql.ScanRow[model.Organisation]( qrbsql.Build(query).WithExecutor(executor).QueryRow(ctx), ) } @@ -46,7 +47,7 @@ func applyOrganisationFilter(filter OrganisationsFilter) func(q builder.SelectBu } } -func FindAllOrganisations(ctx context.Context, executor qrbsql.Executor, filter OrganisationsFilter, pagingOpts ...PagingOption) ([]domain.Organisation, error) { +func FindAllOrganisations(ctx context.Context, executor qrbsql.Executor, filter OrganisationsFilter, pagingOpts ...PagingOption) ([]model.Organisation, error) { query := organisationBuildFindQuery(filter.Opts). ApplyIf(true, applyOrganisationFilter(filter)) @@ -55,7 +56,7 @@ func FindAllOrganisations(ctx context.Context, executor qrbsql.Executor, filter return nil, err } - return constructsql.CollectRows[domain.Organisation]( + return constructsql.CollectRows[model.Organisation]( qrbsql.Build(query).WithExecutor(executor).Query(ctx), ) } @@ -101,6 +102,6 @@ func OrganisationConstraintErr(error) error { return nil } -func buildOrganisationJSON(domain.OrganisationQueryOpts) builder.JsonBuildObjectBuilder { +func buildOrganisationJSON(*domain_query.OrganisationQueryOpts) builder.JsonBuildObjectBuilder { return organisationDefaultJson } diff --git a/backend/security/authentication/auth_context.go b/backend/security/authentication/auth_context.go index 66db2ce..a5cf02e 100644 --- a/backend/security/authentication/auth_context.go +++ b/backend/security/authentication/auth_context.go @@ -7,7 +7,7 @@ import ( "github.com/apex/log" "github.com/gofrs/uuid" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) type ctxKey string @@ -37,7 +37,7 @@ type AuthContext struct { Error error AccountID uuid.UUID OrganisationID *uuid.UUID - Role domain.Role + Role types.Role Secret []byte IssuedAt time.Time Expiry time.Time diff --git a/backend/security/authentication/generate_auth_token.go b/backend/security/authentication/generate_auth_token.go index 6a6af26..3bb0db5 100644 --- a/backend/security/authentication/generate_auth_token.go +++ b/backend/security/authentication/generate_auth_token.go @@ -7,7 +7,7 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) const ( @@ -32,7 +32,7 @@ func TokenOptsForAccount(_ RoleIdentifierProvider, extendedExpiry bool) TokenOpt } } -func GenerateAuthToken(account AuthTokenDataProvider, timeSource domain.TimeSource, opts TokenOpts) (string, error) { +func GenerateAuthToken(account AuthTokenDataProvider, timeSource types.TimeSource, opts TokenOpts) (string, error) { key := account.GetTokenSecret() sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT")) if err != nil { diff --git a/backend/security/authentication/generate_csrf_token.go b/backend/security/authentication/generate_csrf_token.go index a8c6441..7e16924 100644 --- a/backend/security/authentication/generate_csrf_token.go +++ b/backend/security/authentication/generate_csrf_token.go @@ -5,10 +5,10 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" ) -func GenerateCsrfToken(account TokenSecretProvider, timeSource domain.TimeSource, opts TokenOpts) (string, error) { +func GenerateCsrfToken(account TokenSecretProvider, timeSource types.TimeSource, opts TokenOpts) (string, error) { key := account.GetTokenSecret() sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT")) if err != nil { diff --git a/backend/security/authorization/checks.go b/backend/security/authorization/checks.go index 8fec226..a4bc493 100644 --- a/backend/security/authorization/checks.go +++ b/backend/security/authorization/checks.go @@ -6,13 +6,13 @@ import ( "github.com/gofrs/uuid" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/security/authentication" ) type authorizationCheck func(authCtx authentication.AuthContext) error -func requireRole(roles ...domain.Role) authorizationCheck { +func requireRole(roles ...types.Role) authorizationCheck { return func(authCtx authentication.AuthContext) error { currentRole := authCtx.Role for _, role := range roles { @@ -87,14 +87,14 @@ func satisfyAny(checks ...authorizationCheck) authorizationCheck { func requireSameOrganisationAdministrator(organisationID *uuid.UUID) authorizationCheck { return requireAll( - requireRole(domain.RoleOrganisationAdministrator), + requireRole(types.RoleOrganisationAdministrator), requireOrganisationID(organisationID), ) } func requireSameOrganisation(organisationID *uuid.UUID) authorizationCheck { return requireAll( - requireRole(domain.OrganisationRoles...), + requireRole(types.OrganisationRoles...), requireOrganisationID(organisationID), ) } @@ -108,7 +108,7 @@ func requireNotAuthenticated() authorizationCheck { } } -func setOrganisationID(query domain.OrganisationIDSetter) authorizationCheck { +func setOrganisationID(query OrganisationIDSetter) authorizationCheck { return func(authCtx authentication.AuthContext) error { if authCtx.OrganisationID == nil { return authorizationError{"organisation ID is required"} diff --git a/backend/security/authorization/checks_test.go b/backend/security/authorization/checks_test.go index 610b790..8d8b001 100644 --- a/backend/security/authorization/checks_test.go +++ b/backend/security/authorization/checks_test.go @@ -6,39 +6,39 @@ import ( "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/security/authentication" ) func TestRequireRole(t *testing.T) { tests := []struct { name string - authRole domain.Role - required []domain.Role + authRole types.Role + required []types.Role expectErr bool }{ { name: "Role matches", - authRole: domain.RoleOrganisationAdministrator, - required: []domain.Role{domain.RoleOrganisationAdministrator}, + authRole: types.RoleOrganisationAdministrator, + required: []types.Role{types.RoleOrganisationAdministrator}, expectErr: false, }, { name: "Role does not match", - authRole: domain.RoleSystemAdministrator, - required: []domain.Role{domain.RoleOrganisationAdministrator}, + authRole: types.RoleSystemAdministrator, + required: []types.Role{types.RoleOrganisationAdministrator}, expectErr: true, }, { name: "Role matches one of multiple", - authRole: domain.RoleOrganisationAdministrator, - required: []domain.Role{domain.RoleSystemAdministrator, domain.RoleOrganisationAdministrator}, + authRole: types.RoleOrganisationAdministrator, + required: []types.Role{types.RoleSystemAdministrator, types.RoleOrganisationAdministrator}, expectErr: false, }, { name: "No roles required", - authRole: domain.RoleSystemAdministrator, - required: []domain.Role{}, + authRole: types.RoleSystemAdministrator, + required: []types.Role{}, expectErr: true, }, } @@ -351,35 +351,35 @@ func TestRequireSameOrganisationAdministrator(t *testing.T) { tests := []struct { name string - authRole domain.Role + authRole types.Role authOrgID *uuid.UUID inputOrgID *uuid.UUID expectedError bool }{ { name: "Role and organisation match", - authRole: domain.RoleOrganisationAdministrator, + authRole: types.RoleOrganisationAdministrator, authOrgID: &organisationID, inputOrgID: &organisationID, expectedError: false, }, { name: "Role matches, organisation does not", - authRole: domain.RoleOrganisationAdministrator, + authRole: types.RoleOrganisationAdministrator, authOrgID: &organisationID, inputOrgID: &differentOrganisationID, expectedError: true, }, { name: "Role does not match", - authRole: domain.RoleSystemAdministrator, + authRole: types.RoleSystemAdministrator, authOrgID: &organisationID, inputOrgID: &organisationID, expectedError: true, }, { name: "Nil organisation ID", - authRole: domain.RoleOrganisationAdministrator, + authRole: types.RoleOrganisationAdministrator, authOrgID: nil, inputOrgID: &organisationID, expectedError: true, @@ -410,35 +410,35 @@ func TestRequireSameOrganisation(t *testing.T) { tests := []struct { name string - authRole domain.Role + authRole types.Role authOrgID *uuid.UUID inputOrgID *uuid.UUID expectedError bool }{ { name: "Organisation Administrator with matching ID", - authRole: domain.RoleOrganisationAdministrator, + authRole: types.RoleOrganisationAdministrator, authOrgID: &organisationID, inputOrgID: &organisationID, expectedError: false, }, { name: "Role does not match", - authRole: domain.RoleSystemAdministrator, + authRole: types.RoleSystemAdministrator, authOrgID: &organisationID, inputOrgID: &organisationID, expectedError: true, }, { name: "Organisation IDs do not match", - authRole: domain.RoleOrganisationAdministrator, + authRole: types.RoleOrganisationAdministrator, authOrgID: &organisationID, inputOrgID: &differentOrganisationID, expectedError: true, }, { name: "Nil organisation ID", - authRole: domain.RoleOrganisationAdministrator, + authRole: types.RoleOrganisationAdministrator, authOrgID: nil, inputOrgID: &organisationID, expectedError: true, diff --git a/backend/security/authorization/commands.go b/backend/security/authorization/commands.go index 38991f5..610f8d6 100644 --- a/backend/security/authorization/commands.go +++ b/backend/security/authorization/commands.go @@ -1,18 +1,19 @@ package authorization import ( - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/command" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/security/authentication" ) -func (a *Authorizer) AllowsAccountCreateCmd(cmd domain.AccountCreateCmd) error { +func (a *Authorizer) AllowsAccountCreateCmd(cmd command.AccountCreateCmd) error { return a.check( satisfyAny( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), requireAll( requireSameOrganisationAdministrator(uuidOrNil(cmd.OrganisationID)), func(_ authentication.AuthContext) error { - if cmd.Role != domain.RoleOrganisationAdministrator { + if cmd.Role != types.RoleOrganisationAdministrator { return authorizationError{cause: "role not allowed"} } return nil @@ -22,17 +23,17 @@ func (a *Authorizer) AllowsAccountCreateCmd(cmd domain.AccountCreateCmd) error { ) } -func (a *Authorizer) AllowsAccountUpdateCmd(cmd domain.AccountUpdateCmd) error { +func (a *Authorizer) AllowsAccountUpdateCmd(cmd command.AccountUpdateCmd) error { return a.check( satisfyAny( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), requireAll( requireSameOrganisationAdministrator(uuidOrNil(cmd.CurrentOrganisationID)), func(_ authentication.AuthContext) error { if cmd.CurrentOrganisationID != cmd.NewOrganisationID { return authorizationError{cause: "organisation may not be changed"} } - if cmd.Role != domain.RoleOrganisationAdministrator { + if cmd.Role != types.RoleOrganisationAdministrator { return authorizationError{cause: "role not allowed"} } return nil @@ -42,32 +43,32 @@ func (a *Authorizer) AllowsAccountUpdateCmd(cmd domain.AccountUpdateCmd) error { ) } -func (a *Authorizer) AllowsAccountDeleteCmd(cmd domain.AccountDeleteCmd) error { +func (a *Authorizer) AllowsAccountDeleteCmd(cmd command.AccountDeleteCmd) error { return a.check( requireAll( requireNotSameAccount(&cmd.AccountID), satisfyAny( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), requireSameOrganisationAdministrator(uuidOrNil(cmd.OrganisationID)), ), ), ) } -func (a *Authorizer) AllowsOrganisationCreateCmd(domain.OrganisationCreateCmd) error { +func (a *Authorizer) AllowsOrganisationCreateCmd(command.OrganisationCreateCmd) error { return a.check( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), ) } -func (a *Authorizer) AllowsOrganisationUpdateCmd(domain.OrganisationUpdateCmd) error { +func (a *Authorizer) AllowsOrganisationUpdateCmd(command.OrganisationUpdateCmd) error { return a.check( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), ) } -func (a *Authorizer) AllowsOrganisationDeleteCmd(domain.OrganisationDeleteCmd) error { +func (a *Authorizer) AllowsOrganisationDeleteCmd(command.OrganisationDeleteCmd) error { return a.check( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), ) } diff --git a/backend/security/authorization/queries.go b/backend/security/authorization/queries.go index a4bfb84..e7260d2 100644 --- a/backend/security/authorization/queries.go +++ b/backend/security/authorization/queries.go @@ -1,45 +1,47 @@ package authorization import ( - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/model" + "myvendor.mytld/myproject/backend/domain/query" + "myvendor.mytld/myproject/backend/domain/types" ) -func (a *Authorizer) AllowsOrganisationQuery(query domain.OrganisationQuery) error { +func (a *Authorizer) AllowsOrganisationQuery(query query.OrganisationQuery) error { return a.check( satisfyAny( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), requireSameOrganisation(&query.OrganisationID), ), ) } -func (a *Authorizer) AllowsAndFilterAllOrganisationsQuery(query *domain.OrganisationsQuery) error { +func (a *Authorizer) AllowsAndFilterAllOrganisationsQuery(query *query.OrganisationsQuery) error { return a.check( satisfyAny( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), requireAll( - requireRole(domain.RoleOrganisationAdministrator), + requireRole(types.RoleOrganisationAdministrator), setOrganisationID(query), ), ), ) } -func (a *Authorizer) AllowsAccountView(record domain.Account) error { +func (a *Authorizer) AllowsAccountView(record model.Account) error { return a.check( satisfyAny( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), requireSameOrganisation(uuidOrNil(record.OrganisationID)), ), ) } -func (a *Authorizer) AllowsAndFilterAllAccountsQuery(query *domain.AccountsQuery) error { +func (a *Authorizer) AllowsAndFilterAllAccountsQuery(query *query.AccountsQuery) error { return a.check( satisfyAny( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), requireAll( - requireRole(domain.RoleOrganisationAdministrator), + requireRole(types.RoleOrganisationAdministrator), setOrganisationID(query), ), ), @@ -48,6 +50,6 @@ func (a *Authorizer) AllowsAndFilterAllAccountsQuery(query *domain.AccountsQuery func (a *Authorizer) AllowsAllAccountsQuery() error { return a.check( - requireRole(domain.RoleSystemAdministrator), + requireRole(types.RoleSystemAdministrator), ) } diff --git a/backend/security/authorization/queries_test.go b/backend/security/authorization/queries_test.go index 2348504..a3b5eeb 100644 --- a/backend/security/authorization/queries_test.go +++ b/backend/security/authorization/queries_test.go @@ -6,7 +6,8 @@ import ( "github.com/gofrs/uuid" "github.com/stretchr/testify/require" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/model" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/security/authentication" "myvendor.mytld/myproject/backend/security/authorization" ) @@ -18,16 +19,16 @@ func TestAuthorizer_AllowsAccountView(t *testing.T) { tests := []struct { name string authCtx authentication.AuthContext - record domain.Account + record model.Account wantErr bool }{ { name: "unauthenticated", authCtx: authentication.AuthContext{}, - record: domain.Account{ + record: model.Account{ ID: fixtureAccountID, OrganisationID: uuid.NullUUID{UUID: fixtureOrganisationID, Valid: true}, - Role: domain.RoleOrganisationAdministrator, + Role: types.RoleOrganisationAdministrator, }, wantErr: true, }, @@ -37,12 +38,12 @@ func TestAuthorizer_AllowsAccountView(t *testing.T) { Authenticated: true, AccountID: fixtureAccountID, OrganisationID: &fixtureOrganisationID, - Role: domain.RoleOrganisationAdministrator, + Role: types.RoleOrganisationAdministrator, }, - record: domain.Account{ + record: model.Account{ ID: uuid.Must(uuid.FromString("f49c01b7-15a6-48ad-8989-f2fd4e5fa5c1")), OrganisationID: uuid.NullUUID{UUID: fixtureOrganisationID, Valid: true}, - Role: domain.RoleOrganisationAdministrator, + Role: types.RoleOrganisationAdministrator, }, wantErr: false, }, @@ -52,12 +53,12 @@ func TestAuthorizer_AllowsAccountView(t *testing.T) { Authenticated: true, AccountID: fixtureAccountID, OrganisationID: &fixtureOrganisationID, - Role: domain.RoleOrganisationAdministrator, + Role: types.RoleOrganisationAdministrator, }, - record: domain.Account{ + record: model.Account{ ID: uuid.Must(uuid.FromString("f49c01b7-15a6-48ad-8989-f2fd4e5fa5c1")), OrganisationID: uuid.NullUUID{UUID: uuid.Must(uuid.FromString("f9e84475-45f9-47d1-a58c-e416f1c7f39d")), Valid: true}, - Role: domain.RoleOrganisationAdministrator, + Role: types.RoleOrganisationAdministrator, }, wantErr: true, }, @@ -66,12 +67,12 @@ func TestAuthorizer_AllowsAccountView(t *testing.T) { authCtx: authentication.AuthContext{ Authenticated: true, AccountID: fixtureAccountID, - Role: domain.RoleSystemAdministrator, + Role: types.RoleSystemAdministrator, }, - record: domain.Account{ + record: model.Account{ ID: uuid.Must(uuid.FromString("f49c01b7-15a6-48ad-8989-f2fd4e5fa5c1")), OrganisationID: uuid.NullUUID{UUID: uuid.Must(uuid.FromString("f9e84475-45f9-47d1-a58c-e416f1c7f39d")), Valid: true}, - Role: domain.RoleOrganisationAdministrator, + Role: types.RoleOrganisationAdministrator, }, wantErr: false, }, diff --git a/backend/domain/filter.go b/backend/security/authorization/types.go similarity index 84% rename from backend/domain/filter.go rename to backend/security/authorization/types.go index 4efd4c7..dd1ad59 100644 --- a/backend/domain/filter.go +++ b/backend/security/authorization/types.go @@ -1,4 +1,4 @@ -package domain +package authorization import "github.com/gofrs/uuid" diff --git a/backend/test/auth/fixed_login_data.go b/backend/test/auth/fixed_login_data.go index 5ef5dbd..749ac50 100644 --- a/backend/test/auth/fixed_login_data.go +++ b/backend/test/auth/fixed_login_data.go @@ -8,7 +8,7 @@ import ( "github.com/gofrs/uuid" - "myvendor.mytld/myproject/backend/domain" + "myvendor.mytld/myproject/backend/domain/types" "myvendor.mytld/myproject/backend/security/authentication" ) @@ -22,16 +22,16 @@ var ( fixedTokenSecret = "f71ab8929ad747915e135b8e9a5e01403329cc6b202c8e540e74920a78394e36" //nolint:gosec ) -type ApplyAuthValuesFunc func(t *testing.T, timeSource domain.TimeSource, req *http.Request) FixedAuthTokenData +type ApplyAuthValuesFunc func(t *testing.T, timeSource types.TimeSource, req *http.Request) FixedAuthTokenData -func ApplyFixedAuthValuesOrganisationAdministrator(t *testing.T, timeSource domain.TimeSource, req *http.Request) FixedAuthTokenData { +func ApplyFixedAuthValuesOrganisationAdministrator(t *testing.T, timeSource types.TimeSource, req *http.Request) FixedAuthTokenData { t.Helper() authTokenData := FixedAuthTokenData{ TokenSecret: mustHexDecode(fixedTokenSecret), AccountID: fixedOrganisationAdminAccountID, OrganisationID: uuid.NullUUID{Valid: true, UUID: fixedOrganisationID}, - RoleIdentifier: string(domain.RoleOrganisationAdministrator), + RoleIdentifier: string(types.RoleOrganisationAdministrator), } addTokenToRequest(t, timeSource, req, authTokenData) @@ -41,13 +41,13 @@ func ApplyFixedAuthValuesOrganisationAdministrator(t *testing.T, timeSource doma var _ ApplyAuthValuesFunc = ApplyFixedAuthValuesOrganisationAdministrator -func ApplyFixedAuthValuesSystemAdministrator(t *testing.T, timeSource domain.TimeSource, req *http.Request) FixedAuthTokenData { +func ApplyFixedAuthValuesSystemAdministrator(t *testing.T, timeSource types.TimeSource, req *http.Request) FixedAuthTokenData { t.Helper() authTokenData := FixedAuthTokenData{ TokenSecret: mustHexDecode(fixedTokenSecret), AccountID: fixedSystemAdminAccountID, - RoleIdentifier: string(domain.RoleSystemAdministrator), + RoleIdentifier: string(types.RoleSystemAdministrator), } addTokenToRequest(t, timeSource, req, authTokenData) @@ -57,7 +57,7 @@ func ApplyFixedAuthValuesSystemAdministrator(t *testing.T, timeSource domain.Tim var _ ApplyAuthValuesFunc = ApplyFixedAuthValuesSystemAdministrator -func addTokenToRequest(t *testing.T, timeSource domain.TimeSource, req *http.Request, authTokenData FixedAuthTokenData) { +func addTokenToRequest(t *testing.T, timeSource types.TimeSource, req *http.Request, authTokenData FixedAuthTokenData) { t.Helper() tokenOpts := authentication.TokenOptsForAccount(authTokenData, false) @@ -92,7 +92,7 @@ func GetContextWithSystemAdministrator() context.Context { return authentication.WithAuthContext(ctx, authentication.AuthContext{ Authenticated: true, - Role: domain.RoleSystemAdministrator, + Role: types.RoleSystemAdministrator, }) } @@ -103,7 +103,7 @@ func GetContextWithOrganisationAdministrator() context.Context { return authentication.WithAuthContext(ctx, authentication.AuthContext{ Authenticated: true, OrganisationID: &organisationID, - Role: domain.RoleOrganisationAdministrator, + Role: types.RoleOrganisationAdministrator, }) }