diff --git a/backend/cmd/user/format.go b/backend/cmd/user/format.go index a47222125..07e788146 100644 --- a/backend/cmd/user/format.go +++ b/backend/cmd/user/format.go @@ -3,12 +3,13 @@ package user import ( "errors" "fmt" - "github.com/gofrs/uuid" "time" + + "github.com/gofrs/uuid" ) -// ImportEmail The import format for a user's email -type ImportEmail struct { +// ImportOrExportEmail The import/export format for a user's email +type ImportOrExportEmail struct { // Address Valid email address Address string `json:"address" yaml:"address"` // IsPrimary indicates if this is the primary email of the users. In the Emails array there has to be exactly one primary email. @@ -18,10 +19,10 @@ type ImportEmail struct { } // Emails Array of email addresses -type Emails []ImportEmail +type Emails []ImportOrExportEmail -// ImportEntry represents a user to be imported to the Hanko database -type ImportEntry struct { +// ImportOrExportEntry represents a user to be imported/export to the Hanko database +type ImportOrExportEntry struct { // UserID optional uuid.v4. If not provided a new one will be generated for the user UserID string `json:"user_id" yaml:"user_id"` // Emails List of emails @@ -32,10 +33,10 @@ type ImportEntry struct { UpdatedAt *time.Time `json:"updated_at" yaml:"updated_at"` } -// ImportList a list of ImportEntries -type ImportList []ImportEntry +// ImportOrExportList a list of ImportEntries +type ImportOrExportList []ImportOrExportEntry -func (entry *ImportEntry) validate() error { +func (entry *ImportOrExportEntry) validate() error { if len(entry.Emails) == 0 { return errors.New(fmt.Sprintf("Entry with id: %v has got no Emails.", entry.UserID)) } diff --git a/backend/cmd/user/format_test.go b/backend/cmd/user/format_test.go index cecdf4902..4d0a4f5ee 100644 --- a/backend/cmd/user/format_test.go +++ b/backend/cmd/user/format_test.go @@ -2,9 +2,10 @@ package user import ( "fmt" - "github.com/stretchr/testify/assert" "testing" "time" + + "github.com/stretchr/testify/assert" ) const validUUID = "62418053-a2cd-47a8-9b61-4426380d263a" @@ -27,7 +28,7 @@ func TestImportEntry_validate(t *testing.T) { fields: fields{ UserID: "", Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary@hanko.io", IsPrimary: true, IsVerified: false, @@ -43,7 +44,7 @@ func TestImportEntry_validate(t *testing.T) { fields: fields{ UserID: validUUID, Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary@hanko.io", IsPrimary: true, IsVerified: false, @@ -59,7 +60,7 @@ func TestImportEntry_validate(t *testing.T) { fields: fields{ UserID: invalidUUID, Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary@hanko.io", IsPrimary: true, IsVerified: false, @@ -85,7 +86,7 @@ func TestImportEntry_validate(t *testing.T) { fields: fields{ UserID: "", Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary@hanko.io", IsPrimary: false, IsVerified: false, @@ -101,12 +102,12 @@ func TestImportEntry_validate(t *testing.T) { fields: fields{ UserID: "", Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary@hanko.io", IsPrimary: true, IsVerified: false, }, - ImportEmail{ + ImportOrExportEmail{ Address: "primary2@hanko.io", IsPrimary: true, IsVerified: false, @@ -120,7 +121,7 @@ func TestImportEntry_validate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - entry := &ImportEntry{ + entry := &ImportOrExportEntry{ UserID: tt.fields.UserID, Emails: tt.fields.Emails, CreatedAt: tt.fields.CreatedAt, diff --git a/backend/cmd/user/generate.go b/backend/cmd/user/generate.go index cd64050fd..3f991d516 100644 --- a/backend/cmd/user/generate.go +++ b/backend/cmd/user/generate.go @@ -2,12 +2,13 @@ package user import ( "encoding/json" - "github.com/brianvoe/gofakeit/v6" - "github.com/gofrs/uuid" - "github.com/spf13/cobra" "log" "os" "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/gofrs/uuid" + "github.com/spf13/cobra" ) var outputFile string @@ -36,18 +37,18 @@ func NewGenerateCommand() *cobra.Command { } func generate() error { - var entries []ImportEntry + var entries []ImportOrExportEntry for i := 0; i < count; i++ { now := time.Now().UTC() id, _ := uuid.NewV4() - emails := []ImportEmail{ + emails := []ImportOrExportEmail{ { Address: gofakeit.Email(), IsPrimary: true, IsVerified: true, }, } - entry := ImportEntry{ + entry := ImportOrExportEntry{ UserID: id.String(), Emails: emails, CreatedAt: &now, diff --git a/backend/cmd/user/import.go b/backend/cmd/user/import.go index 260de53ee..b403e2dd9 100644 --- a/backend/cmd/user/import.go +++ b/backend/cmd/user/import.go @@ -104,7 +104,7 @@ func NewImportCommand() *cobra.Command { // loadAndValidate reads json from an io.Reader so we read every entry separate and validate it. We go through the whole // array to print out every validation error in the input data. -func loadAndValidate(input io.Reader) ([]ImportEntry, error) { +func loadAndValidate(input io.Reader) ([]ImportOrExportEntry, error) { dec := json.NewDecoder(input) // read the open bracket @@ -113,14 +113,14 @@ func loadAndValidate(input io.Reader) ([]ImportEntry, error) { return nil, err } - users := []ImportEntry{} + users := []ImportOrExportEntry{} numErrors := 0 index := 0 // while the array contains values for dec.More() { index = index + 1 - var userEntry ImportEntry + var userEntry ImportOrExportEntry // decode one ImportEntry err := dec.Decode(&userEntry) if err != nil { @@ -153,7 +153,7 @@ func loadAndValidate(input io.Reader) ([]ImportEntry, error) { } // commits the list of ImportEntries to the database. Wrapped in a transaction so if something fails no new users are added. -func addToDatabase(entries []ImportEntry, persister persistence.Persister) error { +func addToDatabase(entries []ImportOrExportEntry, persister persistence.Persister) error { tx := persister.GetConnection() err := tx.Transaction(func(tx *pop.Connection) error { for i, v := range entries { diff --git a/backend/cmd/user/import_test.go b/backend/cmd/user/import_test.go index b577e17ed..cf30d4f83 100644 --- a/backend/cmd/user/import_test.go +++ b/backend/cmd/user/import_test.go @@ -2,16 +2,17 @@ package user import ( "fmt" - "github.com/gofrs/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/teamhanko/hanko/backend/persistence" - "github.com/teamhanko/hanko/backend/test" "io" "log" "strings" "testing" "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "github.com/teamhanko/hanko/backend/persistence" + "github.com/teamhanko/hanko/backend/test" ) const validUUID2 = "799e95f0-4cc7-4bd7-9f01-5fdc4fa26ea3" @@ -33,7 +34,7 @@ func (s *importSuite) Test_loadAndValidate() { tests := []struct { name string args args - want []ImportEntry + want []ImportOrExportEntry wantErr assert.ErrorAssertionFunc }{ { @@ -42,7 +43,7 @@ func (s *importSuite) Test_loadAndValidate() { input: strings.NewReader("[]"), }, wantErr: assert.NoError, - want: []ImportEntry{}, + want: []ImportOrExportEntry{}, }, { name: "empty file -> nil result", @@ -58,11 +59,11 @@ func (s *importSuite) Test_loadAndValidate() { input: strings.NewReader("[{\"user_id\":\"799e95f0-4cc7-4bd7-9f01-5fdc4fa26ea3\",\"emails\":[{\"address\":\"koreyrath@wolff.name\",\"is_primary\":true,\"is_verified\":true}],\"created_at\":\"2023-06-07T13:42:49.369489Z\",\"updated_at\":\"2023-06-07T13:42:49.369489Z\"}]\n"), }, wantErr: assert.NoError, - want: []ImportEntry{ + want: []ImportOrExportEntry{ { UserID: validUUID2, Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "koreyrath@wolff.name", IsPrimary: true, IsVerified: true, @@ -107,7 +108,7 @@ func (s *importSuite) Test_addToDatabase() { } type args struct { - entries []ImportEntry + entries []ImportOrExportEntry persister persistence.Persister } tests := []struct { @@ -119,11 +120,11 @@ func (s *importSuite) Test_addToDatabase() { { name: "Positive", args: args{ - entries: []ImportEntry{ + entries: []ImportOrExportEntry{ { UserID: "", Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary@hanko.io", IsPrimary: true, IsVerified: false, @@ -141,11 +142,11 @@ func (s *importSuite) Test_addToDatabase() { { name: "Double uuid", args: args{ - entries: []ImportEntry{ + entries: []ImportOrExportEntry{ { UserID: validUUID, Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary1@hanko.io", IsPrimary: true, IsVerified: false, @@ -157,7 +158,7 @@ func (s *importSuite) Test_addToDatabase() { { UserID: validUUID, Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary2@hanko.io", IsPrimary: true, IsVerified: false, @@ -175,11 +176,11 @@ func (s *importSuite) Test_addToDatabase() { { name: "Double primary email", args: args{ - entries: []ImportEntry{ + entries: []ImportOrExportEntry{ { UserID: validUUID, Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary@hanko.io", IsPrimary: true, IsVerified: false, @@ -191,7 +192,7 @@ func (s *importSuite) Test_addToDatabase() { { UserID: validUUID, Emails: Emails{ - ImportEmail{ + ImportOrExportEmail{ Address: "primary@hanko.io", IsPrimary: true, IsVerified: false, diff --git a/backend/json_schema/schema_generator.go b/backend/json_schema/schema_generator.go index 61a162cbd..ffd55c191 100644 --- a/backend/json_schema/schema_generator.go +++ b/backend/json_schema/schema_generator.go @@ -3,17 +3,18 @@ package main import ( "encoding/json" "fmt" + "os" + "github.com/invopop/jsonschema" "github.com/teamhanko/hanko/backend/cmd/user" "github.com/teamhanko/hanko/backend/config" - "os" ) func main() { if err := generateSchema("./config", "./json_schema/hanko.config.json", &config.Config{}); err != nil { panic(err) } - if err := generateSchema("./cmd/user", "./json_schema/hanko.user_import.json", &user.ImportList{}); err != nil { + if err := generateSchema("./cmd/user", "./json_schema/hanko.user_import.json", &user.ImportOrExportList{}); err != nil { panic(err) } diff --git a/backend/persistence/user_persister.go b/backend/persistence/user_persister.go index e73338ce6..74d57ff7f 100644 --- a/backend/persistence/user_persister.go +++ b/backend/persistence/user_persister.go @@ -4,6 +4,7 @@ import ( "database/sql" "errors" "fmt" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/teamhanko/hanko/backend/persistence/models" @@ -15,6 +16,7 @@ type UserPersister interface { Update(models.User) error Delete(models.User) error List(page int, perPage int, userId uuid.UUID, email string, sortDirection string) ([]models.User, error) + All() ([]models.User, error) Count(userId uuid.UUID, email string) (int, error) } @@ -26,6 +28,19 @@ func NewUserPersister(db *pop.Connection) UserPersister { return &userPersister{db: db} } +func (p *userPersister) All() ([]models.User, error) { + users := []models.User{} + err := p.db.EagerPreload("Emails", "Emails.PrimaryEmail", "Emails.Identity", "WebauthnCredentials").All(&users) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return users, nil + } + if err != nil { + return nil, fmt.Errorf("failed to fetch users: %w", err) + } + + return users, nil +} + func (p *userPersister) Get(id uuid.UUID) (*models.User, error) { user := models.User{} err := p.db.EagerPreload("Emails", "Emails.PrimaryEmail", "Emails.Identity", "WebauthnCredentials").Find(&user, id)