package handler import ( "errors" "fmt" "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/labstack/echo/v4" "github.com/lestrrat-go/jwx/v2/jwt" auditlog "github.com/teamhanko/hanko/backend/v2/audit_log" "github.com/teamhanko/hanko/backend/v2/config" "github.com/teamhanko/hanko/backend/v2/dto" "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" "strings" ) type EmailHandler struct { persister persistence.Persister cfg *config.Config auditLogger auditlog.Logger } func NewEmailHandler(cfg *config.Config, persister persistence.Persister, auditLogger auditlog.Logger) *EmailHandler { return &EmailHandler{ persister: persister, cfg: cfg, auditLogger: auditLogger, } } func (h *EmailHandler) List(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { return errors.New("failed to cast session object") } userId, err := uuid.FromString(sessionToken.Subject()) if err != nil { return fmt.Errorf("failed to parse subject as uuid: %w", err) } emails, err := h.persister.GetEmailPersister().FindByUserId(userId) if err != nil { return fmt.Errorf("failed to fetch emails from db: %w", err) } response := make([]*dto.EmailResponse, len(emails)) for i := range emails { response[i] = dto.FromEmailModel(&emails[i], h.cfg) } return c.JSON(http.StatusOK, response) } func (h *EmailHandler) Create(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { return errors.New("failed to cast session object") } userId, err := uuid.FromString(sessionToken.Subject()) if err != nil { return fmt.Errorf("failed to parse subject as uuid: %w", err) } var body dto.EmailCreateRequest err = (&echo.DefaultBinder{}).BindBody(c, &body) if err != nil { return dto.ToHttpError(err) } emailCount, err := h.persister.GetEmailPersister().CountByUserId(userId) if err != nil { return fmt.Errorf("failed to count user emails: %w", err) } if emailCount >= h.cfg.Email.Limit { return echo.NewHTTPError(http.StatusConflict).SetInternal(errors.New("max number of email addresses reached")) } newEmailAddress := strings.ToLower(body.Address) email, err := h.persister.GetEmailPersister().FindByAddress(newEmailAddress) if err != nil { return fmt.Errorf("failed to fetch email from db: %w", err) } return h.persister.Transaction(func(tx *pop.Connection) error { user, err := h.persister.GetUserPersister().Get(userId) if err != nil { return fmt.Errorf("failed to fetch user from db: %w", err) } if email != nil { // The email address already exists. if email.UserID != nil { // The email address exists and is assigned to a user already, therefore it can't be created. return echo.NewHTTPError(http.StatusBadRequest).SetInternal(errors.New("email address already exists")) } if !h.cfg.Email.RequireVerification { // Email verification is currently not required and there is no user assigned to the existing email // address. This can happen, when email verification was turned on before, because then the email // address will be assigned to the user only after passcode verification. The email was left unassigned // and has not been verified, so we assign the email to the current user. email.UserID = &user.ID err = h.persister.GetEmailPersisterWithConnection(tx).Update(*email) if err != nil { return fmt.Errorf("failed to update the existing email: %w", err) } } } else { // The email address has not been registered so far. if h.cfg.Email.RequireVerification { // The email address will be assigned to the user only after passcode verification. email = models.NewEmail(nil, newEmailAddress) } else { // No verification required - assign the email to the given user. email = models.NewEmail(&user.ID, newEmailAddress) } err = h.persister.GetEmailPersisterWithConnection(tx).Create(*email) if err != nil { return fmt.Errorf("failed to store email to db: %w", err) } } err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogEmailCreated, user, nil) if err != nil { return fmt.Errorf("failed to create audit log: %w", err) } if !h.cfg.Email.RequireVerification { var evt events.Event if len(user.Emails) >= 1 { evt = events.UserEmailCreate } else { evt = events.UserCreate } utils.NotifyUserChange(c, tx, h.persister, evt, userId) } return c.JSON(http.StatusOK, email) }) } func (h *EmailHandler) SetPrimaryEmail(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { return errors.New("failed to cast session object") } userId, err := uuid.FromString(sessionToken.Subject()) if err != nil { return fmt.Errorf("failed to parse subject as uuid: %w", err) } emailId, err := uuid.FromString(c.Param("id")) if err != nil { return echo.NewHTTPError(http.StatusBadRequest).SetInternal(err) } user, err := h.persister.GetUserPersister().Get(userId) if err != nil { return fmt.Errorf("failed to fetch user from db: %w", err) } email := user.GetEmailById(emailId) if email == nil { return echo.NewHTTPError(http.StatusNotFound).SetInternal(errors.New("the email address is not assigned to the current user")) } if email.IsPrimary() { return c.NoContent(http.StatusNoContent) } return h.persister.Transaction(func(tx *pop.Connection) error { var primaryEmail *models.PrimaryEmail if e := user.Emails.GetPrimary(); e != nil { primaryEmail = e.PrimaryEmail } if primaryEmail == nil { primaryEmail = models.NewPrimaryEmail(email.ID, user.ID) err = h.persister.GetPrimaryEmailPersisterWithConnection(tx).Create(*primaryEmail) if err != nil { return fmt.Errorf("failed to store new primary email: %w", err) } } else { primaryEmail.EmailID = email.ID err = h.persister.GetPrimaryEmailPersisterWithConnection(tx).Update(*primaryEmail) if err != nil { return fmt.Errorf("failed to change primary email: %w", err) } } err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogPrimaryEmailChanged, user, nil) if err != nil { return fmt.Errorf("failed to create audit log: %w", err) } utils.NotifyUserChange(c, tx, h.persister, events.UserEmailPrimary, userId) return c.NoContent(http.StatusNoContent) }) } func (h *EmailHandler) Delete(c echo.Context) error { sessionToken, ok := c.Get("session").(jwt.Token) if !ok { return errors.New("failed to cast session object") } userId, err := uuid.FromString(sessionToken.Subject()) if err != nil { return fmt.Errorf("failed to parse subject as uuid: %w", err) } emailId, err := uuid.FromString(c.Param("id")) user, err := h.persister.GetUserPersister().Get(userId) if err != nil { return fmt.Errorf("failed to fetch user from db: %w", err) } emailToBeDeleted := user.GetEmailById(emailId) if emailToBeDeleted == nil { return errors.New("email with given emailId not available") } if emailToBeDeleted.IsPrimary() { return echo.NewHTTPError(http.StatusConflict).SetInternal(errors.New("primary email can't be deleted")) } return h.persister.Transaction(func(tx *pop.Connection) error { err = h.persister.GetEmailPersisterWithConnection(tx).Delete(*emailToBeDeleted) if err != nil { return fmt.Errorf("failed to delete email from db: %w", err) } err = h.auditLogger.CreateWithConnection(tx, c, models.AuditLogEmailDeleted, user, nil) if err != nil { return fmt.Errorf("failed to create audit log: %w", err) } utils.NotifyUserChange(c, tx, h.persister, events.UserEmailDelete, userId) return c.NoContent(http.StatusNoContent) }) }