Skip to content

Commit

Permalink
feat: return complete user in admin API
Browse files Browse the repository at this point in the history
add username, identities and password credential to the return object when calling /users/{user_id} from the admin API
  • Loading branch information
FreddyDevelop committed Aug 19, 2024
1 parent 951c061 commit d3fb41b
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 19 deletions.
27 changes: 27 additions & 0 deletions backend/dto/admin/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package admin

import (
"github.com/gofrs/uuid"
"github.com/teamhanko/hanko/backend/persistence/models"
"time"
)

type Identity struct {
ID uuid.UUID `json:"id"`
ProviderID string `json:"provider_id"`
ProviderName string `json:"provider_name"`
EmailID uuid.UUID `json:"email_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func FromIdentityModel(model models.Identity) Identity {
return Identity{
ID: model.ID,
ProviderID: model.ProviderID,
ProviderName: model.ProviderName,
EmailID: model.EmailID,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
12 changes: 12 additions & 0 deletions backend/dto/admin/password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package admin

import (
"github.com/gofrs/uuid"
"time"
)

type PasswordCredential struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
27 changes: 26 additions & 1 deletion backend/dto/admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ type User struct {
ID uuid.UUID `json:"id"`
WebauthnCredentials []dto.WebauthnCredentialResponse `json:"webauthn_credentials,omitempty"`
Emails []Email `json:"emails,omitempty"`
Username *Username `json:"username,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Password *PasswordCredential `json:"password,omitempty"`
Identities []Identity `json:"identities,omitempty"`
}

// FromUserModel Converts the DB model to a DTO object
Expand All @@ -22,20 +25,42 @@ func FromUserModel(model models.User) User {
credentials[i] = *dto.FromWebauthnCredentialModel(&model.WebauthnCredentials[i])
}
emails := make([]Email, len(model.Emails))
var identities = make([]Identity, 0)
for i := range model.Emails {
emails[i] = *FromEmailModel(&model.Emails[i])
for j := range model.Emails[i].Identities {
identities = append(identities, FromIdentityModel(model.Emails[i].Identities[j]))
}
}
var username *Username = nil
if model.Username != nil {
username = FromUsernameModel(model.Username)
}

var passwordCredential *PasswordCredential = nil
if model.PasswordCredential != nil {
passwordCredential = &PasswordCredential{
ID: model.PasswordCredential.ID,
CreatedAt: model.PasswordCredential.CreatedAt,
UpdatedAt: model.PasswordCredential.UpdatedAt,
}
}

return User{
ID: model.ID,
WebauthnCredentials: credentials,
Emails: emails,
Username: username,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
Password: passwordCredential,
Identities: identities,
}
}

type CreateUser struct {
ID uuid.UUID `json:"id"`
Emails []CreateEmail `json:"emails" validate:"required,gte=1,unique=Address,dive"`
Emails []CreateEmail `json:"emails" validate:"unique=Address,dive"`
Username *string `json:"username"`
CreatedAt time.Time `json:"created_at"`
}
24 changes: 24 additions & 0 deletions backend/dto/admin/username.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package admin

import (
"github.com/gofrs/uuid"
"github.com/teamhanko/hanko/backend/persistence/models"
"time"
)

type Username struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// FromEmailModel Converts the DB model to a DTO object
func FromUsernameModel(model *models.Username) *Username {
return &Username{
ID: model.ID,
Username: model.Username,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
33 changes: 28 additions & 5 deletions backend/handler/user_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type UserListRequest struct {
Page int `query:"page"`
Email string `query:"email"`
UserId string `query:"user_id"`
Username string `query:"username"`
SortDirection string `query:"sort_direction"`
}

Expand Down Expand Up @@ -101,13 +102,14 @@ func (h *UserHandlerAdmin) List(c echo.Context) error {
}

email := strings.ToLower(request.Email)
username := strings.ToLower(request.Username)

users, err := h.persister.GetUserPersister().List(request.Page, request.PerPage, userId, email, request.SortDirection)
users, err := h.persister.GetUserPersister().List(request.Page, request.PerPage, userId, email, username, request.SortDirection)
if err != nil {
return fmt.Errorf("failed to get list of users: %w", err)
}

userCount, err := h.persister.GetUserPersister().Count(userId, email)
userCount, err := h.persister.GetUserPersister().Count(userId, email, username)
if err != nil {
return fmt.Errorf("failed to get total count of users: %w", err)
}
Expand Down Expand Up @@ -154,6 +156,10 @@ func (h *UserHandlerAdmin) Create(c echo.Context) error {
return dto.ToHttpError(err)
}

if len(body.Emails) == 0 && (body.Username == nil || *body.Username == "") {
return echo.NewHTTPError(http.StatusBadRequest, "at least one of [Emails, Username] must be set")
}

// if no userID is provided, create a new one
if body.ID.IsNil() {
userId, err := uuid.NewV4()
Expand All @@ -171,9 +177,7 @@ func (h *UserHandlerAdmin) Create(c echo.Context) error {
}
}

if primaryEmails == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "at least one primary email must be provided")
} else if primaryEmails > 1 {
if primaryEmails > 1 {
return echo.NewHTTPError(http.StatusBadRequest, "only one primary email is allowed")
}

Expand Down Expand Up @@ -238,6 +242,25 @@ func (h *UserHandlerAdmin) Create(c echo.Context) error {
}
}
}

if body.Username != nil {
username := models.NewUsername(u.ID, *body.Username)
err = tx.Create(username)
if err != nil {
var pgErr *pgconn.PgError
var mysqlErr *mysql.MySQLError
if errors.As(err, &pgErr) {
if pgErr.Code == "23505" {
return echo.NewHTTPError(http.StatusConflict, fmt.Errorf("failed to create username '%s' for user '%v': %w", username.Username, u.ID, fmt.Errorf("username already exists")))
}
} else if errors.As(err, &mysqlErr) {
if mysqlErr.Number == 1062 {
return echo.NewHTTPError(http.StatusConflict, fmt.Errorf("failed to create username '%s' for user '%v': %w", username.Username, u.ID, fmt.Errorf("username already exists")))
}
}
return fmt.Errorf("failed to create email '%s' for user '%v': %w", username.Username, u.ID, err)
}
}
return nil
})

Expand Down
32 changes: 19 additions & 13 deletions backend/persistence/user_persister.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ type UserPersister interface {
Create(models.User) error
Update(models.User) error
Delete(models.User) error
List(page int, perPage int, userId uuid.UUID, email string, sortDirection string) ([]models.User, error)
List(page int, perPage int, userId uuid.UUID, email string, username string, sortDirection string) ([]models.User, error)
All() ([]models.User, error)
Count(userId uuid.UUID, email string) (int, error)
Count(userId uuid.UUID, email string, username string) (int, error)
GetByUsername(username string) (*models.User, error)
}

Expand Down Expand Up @@ -129,17 +129,17 @@ func (p *userPersister) Delete(user models.User) error {
return nil
}

func (p *userPersister) List(page int, perPage int, userId uuid.UUID, email string, sortDirection string) ([]models.User, error) {
func (p *userPersister) List(page int, perPage int, userId uuid.UUID, email string, username string, sortDirection string) ([]models.User, error) {
users := []models.User{}

query := p.db.
Q().
EagerPreload("Emails", "Emails.PrimaryEmail", "WebauthnCredentials").
EagerPreload("Emails", "Emails.PrimaryEmail", "WebauthnCredentials", "Username").
LeftJoin("emails", "emails.user_id = users.id").
LeftJoin("usernames", "usernames.user_id = users.id")
query = p.addQueryParamsToSqlQuery(query, userId, email)
query = p.addQueryParamsToSqlQuery(query, userId, email, username)
err := query.GroupBy("users.id").
Having("count(emails.id) > 0").
Having("count(emails.id) > 0 OR count(usernames.id) > 0").
Order(fmt.Sprintf("users.created_at %s", sortDirection)).
Paginate(page, perPage).
All(&users)
Expand All @@ -156,7 +156,7 @@ func (p *userPersister) List(page int, perPage int, userId uuid.UUID, email stri

func (p *userPersister) All() ([]models.User, error) {
users := []models.User{}
err := p.db.EagerPreload("Emails", "Emails.PrimaryEmail", "Emails.Identities", "WebauthnCredentials", "Usernames").All(&users)
err := p.db.EagerPreload("Emails", "Emails.PrimaryEmail", "Emails.Identities", "WebauthnCredentials", "Username").All(&users)
if err != nil && errors.Is(err, sql.ErrNoRows) {
return users, nil
}
Expand All @@ -167,13 +167,14 @@ func (p *userPersister) All() ([]models.User, error) {
return users, nil
}

func (p *userPersister) Count(userId uuid.UUID, email string) (int, error) {
func (p *userPersister) Count(userId uuid.UUID, email string, username string) (int, error) {
query := p.db.
Q().
LeftJoin("emails", "emails.user_id = users.id")
query = p.addQueryParamsToSqlQuery(query, userId, email)
LeftJoin("emails", "emails.user_id = users.id").
LeftJoin("usernames", "usernames.user_id = users.id")
query = p.addQueryParamsToSqlQuery(query, userId, email, username)
count, err := query.GroupBy("users.id").
Having("count(emails.id) > 0").
Having("count(emails.id) > 0 OR count(usernames.id) > 0").
Count(&models.User{})
if err != nil {
return 0, fmt.Errorf("failed to get user count: %w", err)
Expand All @@ -182,10 +183,15 @@ func (p *userPersister) Count(userId uuid.UUID, email string) (int, error) {
return count, nil
}

func (p *userPersister) addQueryParamsToSqlQuery(query *pop.Query, userId uuid.UUID, email string) *pop.Query {
if email != "" {
func (p *userPersister) addQueryParamsToSqlQuery(query *pop.Query, userId uuid.UUID, email string, username string) *pop.Query {
if email != "" && username != "" {
query = query.Where("emails.address LIKE ? OR usernames.username LIKE ?", "%"+email+"%", "%"+username+"%")
} else if email != "" {
query = query.Where("emails.address LIKE ?", "%"+email+"%")
} else if username != "" {
query = query.Where("usernames.username LIKE ?", "%"+username+"%")
}

if !userId.IsNil() {
query = query.Where("users.id = ?", userId)
}
Expand Down

0 comments on commit d3fb41b

Please sign in to comment.