diff --git a/api/api.yaml b/api/api.yaml index f6cda2c13..aaee64168 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -194,6 +194,8 @@ paths: $ref: '#/components/responses/401' '403': $ref: '#/components/responses/403' + '409': + $ref: '#/components/responses/409' '500': $ref: '#/components/responses/500' get: diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 2e6ab8aa6..c672ebda2 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -3199,6 +3199,15 @@ func (response CreateIdentity403JSONResponse) VisitCreateIdentityResponse(w http return json.NewEncoder(w).Encode(response) } +type CreateIdentity409JSONResponse struct{ N409JSONResponse } + +func (response CreateIdentity409JSONResponse) VisitCreateIdentityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + type CreateIdentity500JSONResponse struct{ N500JSONResponse } func (response CreateIdentity500JSONResponse) VisitCreateIdentityResponse(w http.ResponseWriter) error { diff --git a/internal/api/identity.go b/internal/api/identity.go index 523b6d2c1..b143c92c8 100644 --- a/internal/api/identity.go +++ b/internal/api/identity.go @@ -78,7 +78,6 @@ func (s *Server) CreateIdentity(ctx context.Context, request CreateIdentityReque }, }, nil } - if errors.Is(err, kms.ErrPermissionDenied) { var message string if s.cfg.KeyStore.VaultUserPassAuthEnabled { @@ -94,6 +93,13 @@ func (s *Server) CreateIdentity(ctx context.Context, request CreateIdentityReque }, }, nil } + if errors.Is(err, services.ErrIdentityDisplayNameDuplicated) { + return CreateIdentity409JSONResponse{ + N409JSONResponse{ + Message: fmt.Sprintf("display name field already exists: <%s>", *request.Body.DisplayName), + }, + }, nil + } var customErr *services.PublishingStateError if errors.As(err, &customErr) { diff --git a/internal/api/identity_test.go b/internal/api/identity_test.go index 0b791b76b..3b8274534 100644 --- a/internal/api/identity_test.go +++ b/internal/api/identity_test.go @@ -95,7 +95,7 @@ func TestServer_CreateIdentity(t *testing.T) { Method string `json:"method"` Network string `json:"network"` Type CreateIdentityRequestDidMetadataType `json:"type"` - }{Blockchain: blockchain, Method: method, Network: network, Type: BJJ}, DisplayName: common.ToPointer("my display name"), + }{Blockchain: blockchain, Method: method, Network: network, Type: BJJ}, DisplayName: common.ToPointer("blockchain display name"), CredentialStatusType: &authBJJCredentialStatus, }, expected: expected{ @@ -236,6 +236,40 @@ func TestServer_CreateIdentity(t *testing.T) { } }) } + t.Run("Duplicated display name", func(t *testing.T) { + bodyRequest := CreateIdentityRequest{ + DidMetadata: struct { + Blockchain string `json:"blockchain"` + Method string `json:"method"` + Network string `json:"network"` + Type CreateIdentityRequestDidMetadataType `json:"type"` + }{ + Blockchain: blockchain, + Method: method, + Network: network, + Type: BJJ, + }, + DisplayName: common.ToPointer("Very common display name"), + } + // First request + req, err := http.NewRequest("POST", "/v2/identities", tests.JSONBody(t, bodyRequest)) + req.SetBasicAuth(authOk()) + require.NoError(t, err) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + require.Equal(t, http.StatusCreated, rr.Code) // First time we expect 201 + + // Second request + req, err = http.NewRequest("POST", "/v2/identities", tests.JSONBody(t, bodyRequest)) + req.SetBasicAuth(authOk()) + require.NoError(t, err) + rr = httptest.NewRecorder() + handler.ServeHTTP(rr, req) + require.Equal(t, http.StatusConflict, rr.Code) // Second time we expect a conflict 409 + var response CreateIdentity409JSONResponse + assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response)) + assert.Equal(t, "display name field already exists: ", response.Message) + }) } func TestServer_GetIdentities(t *testing.T) { diff --git a/internal/core/services/identity.go b/internal/core/services/identity.go index 52765003e..11f23d6d1 100644 --- a/internal/core/services/identity.go +++ b/internal/core/services/identity.go @@ -34,6 +34,7 @@ import ( "github.com/polygonid/sh-id-platform/internal/kms" "github.com/polygonid/sh-id-platform/internal/log" "github.com/polygonid/sh-id-platform/internal/qrlink" + "github.com/polygonid/sh-id-platform/internal/repositories" "github.com/polygonid/sh-id-platform/internal/urn" "github.com/polygonid/sh-id-platform/pkg/credentials/revocation_status" "github.com/polygonid/sh-id-platform/pkg/credentials/signature/circuit/signer" @@ -53,12 +54,17 @@ const ( // ErrWrongDIDMetada - represents an error in the identity metadata var ( - // ErrWrongDIDMetada - represents an error in the identity metadata - ErrWrongDIDMetada = errors.New("wrong DID Metadata") // ErrAssigningMTPProof - represents an error in the identity metadata ErrAssigningMTPProof = errors.New("error assigning the MTP Proof from Auth Claim. If this identity has keyType=ETH you must to publish the state first") + + // ErrIdentityDisplayNameDuplicated - returned when trying to create an identity with a duplicated display name + ErrIdentityDisplayNameDuplicated = errors.New("duplicated identity display name") + // ErrNoClaimsFoundToProcess - means that there are no claims to process ErrNoClaimsFoundToProcess = errors.New("no MTP or revoked claims found to process") + + // ErrWrongDIDMetada - represents an error in the identity metadata + ErrWrongDIDMetada = errors.New("wrong DID Metadata") ) type identity struct { @@ -676,6 +682,9 @@ func (i *identity) createEthIdentity(ctx context.Context, tx db.Querier, hostURL identity.DisplayName = didOptions.DisplayName if err = i.identityRepository.Save(ctx, tx, identity); err != nil { + if errors.Is(err, repositories.ErrDisplayNameDuplicated) { + return nil, nil, ErrIdentityDisplayNameDuplicated + } log.Error(ctx, "saving identity", "err", err) return nil, nil, errors.Join(err, errors.New("can't save identity")) } @@ -784,6 +793,9 @@ func (i *identity) createIdentity(ctx context.Context, tx db.Querier, hostURL st } if err = i.identityRepository.Save(ctx, tx, identity); err != nil { + if errors.Is(err, repositories.ErrDisplayNameDuplicated) { + return nil, nil, ErrIdentityDisplayNameDuplicated + } return nil, nil, fmt.Errorf("can't save identity: %w", err) } diff --git a/internal/db/schema/migrations/202410021258180_identities_display_name_set_to_unique.sql b/internal/db/schema/migrations/202410021258180_identities_display_name_set_to_unique.sql new file mode 100644 index 000000000..14985e857 --- /dev/null +++ b/internal/db/schema/migrations/202410021258180_identities_display_name_set_to_unique.sql @@ -0,0 +1,22 @@ +-- +goose Up +-- +goose StatementBegin +WITH cte AS (SELECT identifier, + display_name, + ROW_NUMBER() OVER (PARTITION BY display_name ORDER BY identifier) AS rn + FROM identities + WHERE display_name IS NOT NULL) +UPDATE identities +SET display_name = identities.display_name || '_' || rn +FROM cte +WHERE identities.identifier = cte.identifier + AND cte.rn > 1; + +ALTER TABLE identities + ADD CONSTRAINT identities_display_name_unique UNIQUE (display_name); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE identities + DROP CONSTRAINT identities_display_name_unique; +-- +goose StatementEnd diff --git a/internal/repositories/identity.go b/internal/repositories/identity.go index 399ea6201..e139add6b 100644 --- a/internal/repositories/identity.go +++ b/internal/repositories/identity.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/iden3/go-iden3-core/v2/w3c" + "github.com/jackc/pgconn" "github.com/polygonid/sh-id-platform/internal/core/domain" "github.com/polygonid/sh-id-platform/internal/core/ports" @@ -16,6 +17,9 @@ import ( // ErrIdentityNotFound - identity not found error var ErrIdentityNotFound = errors.New("identity not found") +// ErrDisplayNameDuplicated - display name already exists error +var ErrDisplayNameDuplicated = errors.New("display name already exists") + type identity struct{} // NewIdentity TODO @@ -26,6 +30,13 @@ func NewIdentity() ports.IndentityRepository { // Save - Create new identity func (i *identity) Save(ctx context.Context, conn db.Querier, identity *domain.Identity) error { _, err := conn.Exec(ctx, `INSERT INTO identities (identifier, address, keyType, display_name) VALUES ($1, $2, $3, $4)`, identity.Identifier, identity.Address, identity.KeyType, identity.DisplayName) + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == duplicateViolationErrorCode { + return ErrDisplayNameDuplicated + } + return err + } return err } diff --git a/internal/repositories/schema.go b/internal/repositories/schema.go index ba7e719fd..264e38807 100644 --- a/internal/repositories/schema.go +++ b/internal/repositories/schema.go @@ -17,11 +17,9 @@ import ( "github.com/polygonid/sh-id-platform/internal/db" ) -const duplicatedEntryPGCode = "23505" - var ( ErrSchemaDoesNotExist = errors.New("schema does not exist") // ErrSchemaDoesNotExist schema does not exist - ErrSchemaDuplicated = errors.New("schema already imported") // ErrSchemaDuplicated schema duplicated + ErrDuplicated = errors.New("schema already imported") // ErrDuplicated schema duplicated ) type dbSchema struct { @@ -68,8 +66,8 @@ func (r *schema) Save(ctx context.Context, s *domain.Schema) error { s.Description) if err != nil { var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == duplicatedEntryPGCode { - return ErrSchemaDuplicated + if errors.As(err, &pgErr) && pgErr.Code == duplicateViolationErrorCode { + return ErrDuplicated } return err diff --git a/internal/repositories/schema_test.go b/internal/repositories/schema_test.go index 4e1cbf735..742db2728 100644 --- a/internal/repositories/schema_test.go +++ b/internal/repositories/schema_test.go @@ -86,7 +86,7 @@ func TestCreateSchema(t *testing.T) { } require.NoError(t, store.Save(ctx, schema1)) - assert.ErrorIs(t, ErrSchemaDuplicated, store.Save(ctx, schema1), "cannot have duplicated schemas with the same version for the same issuer and type") + assert.ErrorIs(t, ErrDuplicated, store.Save(ctx, schema1), "cannot have duplicated schemas with the same version for the same issuer and type") schema2 := schema1 schema2.Version = uuid.NewString()