Files
hanko/backend/handler/user_admin.go
2025-09-25 19:15:20 +02:00

304 lines
8.6 KiB
Go

package handler
import (
"fmt"
"github.com/go-sql-driver/mysql"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/jackc/pgconn"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/teamhanko/hanko/backend/v2/dto"
"github.com/teamhanko/hanko/backend/v2/dto/admin"
"github.com/teamhanko/hanko/backend/v2/pagination"
"github.com/teamhanko/hanko/backend/v2/persistence"
"github.com/teamhanko/hanko/backend/v2/persistence/models"
"github.com/teamhanko/hanko/backend/v2/webhooks/events"
"github.com/teamhanko/hanko/backend/v2/webhooks/utils"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type UserHandlerAdmin struct {
persister persistence.Persister
}
func NewUserHandlerAdmin(persister persistence.Persister) *UserHandlerAdmin {
return &UserHandlerAdmin{persister: persister}
}
func (h *UserHandlerAdmin) Delete(c echo.Context) error {
userId, err := uuid.FromString(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "failed to parse userId as uuid").SetInternal(err)
}
err = h.persister.Transaction(func(tx *pop.Connection) error {
p := h.persister.GetUserPersisterWithConnection(tx)
user, err := p.Get(userId)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
err = p.Delete(*user)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
err = utils.TriggerWebhooks(c, tx, events.UserDelete, admin.FromUserModel(*user))
if err != nil {
c.Logger().Warn(err)
}
return nil
})
if err != nil {
return err
}
return c.NoContent(http.StatusNoContent)
}
type UserListRequest struct {
PerPage int `query:"per_page"`
Page int `query:"page"`
Email string `query:"email"`
UserID string `query:"user_id"`
Username string `query:"username"`
SortDirection string `query:"sort_direction"`
}
func (h *UserHandlerAdmin) List(c echo.Context) error {
var request UserListRequest
err := (&echo.DefaultBinder{}).BindQueryParams(c, &request)
if err != nil {
return dto.ToHttpError(err)
}
if request.Page == 0 {
request.Page = 1
}
if request.PerPage == 0 {
request.PerPage = 20
}
var userIDs []uuid.UUID
if request.UserID != "" {
for _, userIDString := range strings.Split(request.UserID, ",") {
userID, err := uuid.FromString(userIDString)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "failed to parse user_id as uuid").SetInternal(err)
}
userIDs = append(userIDs, userID)
}
}
if request.SortDirection == "" {
request.SortDirection = "desc"
}
switch request.SortDirection {
case "desc", "asc":
default:
return echo.NewHTTPError(http.StatusBadRequest, "sort_direction must be desc or asc")
}
email := strings.ToLower(request.Email)
username := strings.ToLower(request.Username)
users, err := h.persister.GetUserPersister().List(request.Page, request.PerPage, userIDs, email, username, request.SortDirection)
if err != nil {
return fmt.Errorf("failed to get list of users: %w", err)
}
userCount, err := h.persister.GetUserPersister().Count(userIDs, email, username)
if err != nil {
return fmt.Errorf("failed to get total count of users: %w", err)
}
u, _ := url.Parse(fmt.Sprintf("%s://%s%s", c.Scheme(), c.Request().Host, c.Request().RequestURI))
c.Response().Header().Set("Link", pagination.CreateHeader(u, userCount, request.Page, request.PerPage))
c.Response().Header().Set("X-Total-Count", strconv.FormatInt(int64(userCount), 10))
l := make([]admin.User, len(users))
for i := range users {
l[i] = admin.FromUserModel(users[i])
}
return c.JSON(http.StatusOK, l)
}
func (h *UserHandlerAdmin) Get(c echo.Context) error {
userId, err := uuid.FromString(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "failed to parse userId as uuid").SetInternal(err)
}
p := h.persister.GetUserPersister()
user, err := p.Get(userId)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
return c.JSON(http.StatusOK, admin.FromUserModel(*user))
}
func (h *UserHandlerAdmin) Create(c echo.Context) error {
var body admin.CreateUser
if err := (&echo.DefaultBinder{}).BindBody(c, &body); err != nil {
return dto.ToHttpError(err)
}
if err := c.Validate(body); err != nil {
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()
if err != nil {
return fmt.Errorf("failed to create new userId: %w", err)
}
body.ID = userId
}
// check that only one email is marked as primary
primaryEmails := 0
for _, email := range body.Emails {
if email.IsPrimary {
primaryEmails++
}
}
if primaryEmails == 0 && len(body.Emails) > 0 {
return echo.NewHTTPError(http.StatusBadRequest, "at least one primary email must be provided")
} else if primaryEmails > 1 {
return echo.NewHTTPError(http.StatusBadRequest, "only one primary email is allowed")
}
err := h.persister.GetConnection().Transaction(func(tx *pop.Connection) error {
u := models.User{
ID: body.ID,
CreatedAt: body.CreatedAt,
}
err := tx.Create(&u)
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 user with id '%v': %w", u.ID, fmt.Errorf("user already exists")))
}
} else if errors.As(err, &mysqlErr) {
if mysqlErr.Number == 1062 {
return echo.NewHTTPError(http.StatusConflict, fmt.Errorf("failed to create user with id '%v': %w", u.ID, fmt.Errorf("user already exists")))
}
}
return fmt.Errorf("failed to create user with id '%v': %w", u.ID, err)
}
now := time.Now()
for _, email := range body.Emails {
emailId, _ := uuid.NewV4()
mail := models.Email{
ID: emailId,
UserID: &u.ID,
Address: strings.ToLower(email.Address),
Verified: email.IsVerified,
CreatedAt: now,
UpdatedAt: now,
}
err := tx.Create(&mail)
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 email '%s' for user '%v': %w", mail.Address, u.ID, fmt.Errorf("email already exists")))
}
} else if errors.As(err, &mysqlErr) {
if mysqlErr.Number == 1062 {
return echo.NewHTTPError(http.StatusConflict, fmt.Errorf("failed to create email '%s' for user '%v': %w", mail.Address, u.ID, fmt.Errorf("email already exists")))
}
}
return fmt.Errorf("failed to create email '%s' for user '%v': %w", mail.Address, u.ID, err)
}
if email.IsPrimary {
primary := models.PrimaryEmail{
UserID: u.ID,
EmailID: mail.ID,
}
err := tx.Create(&primary)
if err != nil {
return fmt.Errorf("failed to set email '%s' as primary for user '%v': %w", mail.Address, u.ID, err)
}
}
}
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
})
if httpError, ok := err.(*echo.HTTPError); ok {
return httpError
} else if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}
p := h.persister.GetUserPersister()
user, err := p.Get(body.ID)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "user not found")
}
userDto := admin.FromUserModel(*user)
err = utils.TriggerWebhooks(c, h.persister.GetConnection(), events.UserCreate, userDto)
if err != nil {
c.Logger().Warn(err)
}
return c.JSON(http.StatusOK, userDto)
}