Refactor auth_user usecase: split in multiple files
This commit is contained in:
parent
cffbc104ae
commit
0495ab4693
16 changed files with 813 additions and 391 deletions
|
|
@ -128,7 +128,8 @@ func (ac *AuthUserController) EmailValidationLink(c *gin.Context) {
|
|||
func (ac *AuthUserController) RecoverUserAcct(c *gin.Context) {
|
||||
user := c.MustGet("authuser").(*happydns.UserAuth)
|
||||
|
||||
happydns.ApiResponse(c, ac.auService.GenerateRecoveryLink(user), nil)
|
||||
link, err := ac.auService.GenerateRecoveryLink(user)
|
||||
happydns.ApiResponse(c, link, err)
|
||||
}
|
||||
|
||||
type resetPassword struct {
|
||||
|
|
|
|||
|
|
@ -138,14 +138,6 @@ func (rc *UserRecoveryController) ValidateUserAddress(c *gin.Context) {
|
|||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type UploadedAccountRecovery struct {
|
||||
// Key is the secret sent by email to the user.
|
||||
Key string
|
||||
|
||||
// Password is the new password to use with this account.
|
||||
Password string
|
||||
}
|
||||
|
||||
// recoverUserAccount performs account recovery by reseting the password of the account.
|
||||
//
|
||||
// @Summary Reset password with link in email.
|
||||
|
|
@ -155,7 +147,7 @@ type UploadedAccountRecovery struct {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param userId path string true "User identifier"
|
||||
// @Param body body UploadedAccountRecovery true "Recovery form"
|
||||
// @Param body body happydns.AccountRecoveryForm true "Recovery form"
|
||||
// @Success 204 {null} null "Recovery completed, you can now login with your new credentials"
|
||||
// @Failure 400 {object} happydns.ErrorResponse "Invalid input"
|
||||
// @Failure 500 {object} happydns.ErrorResponse
|
||||
|
|
@ -163,7 +155,7 @@ type UploadedAccountRecovery struct {
|
|||
func (rc *UserRecoveryController) RecoverUserAccount(c *gin.Context) {
|
||||
user := c.MustGet("authuser").(*happydns.UserAuth)
|
||||
|
||||
var uar UploadedAccountRecovery
|
||||
var uar happydns.AccountRecoveryForm
|
||||
err := c.ShouldBindJSON(&uar)
|
||||
if err != nil {
|
||||
log.Printf("%s sends invalid AccountRecovey JSON: %s", c.ClientIP(), err.Error())
|
||||
|
|
@ -171,17 +163,7 @@ func (rc *UserRecoveryController) RecoverUserAccount(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := user.CanRecoverAccount(uar.Key); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, happydns.ErrorResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if len(uar.Password) == 0 {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, happydns.ErrorResponse{Message: "Password can't be empty!"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := rc.auService.ChangePassword(user, uar.Password); err != nil {
|
||||
if err := rc.auService.ResetPassword(user, uar); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, happydns.ErrorResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"git.happydns.org/happyDomain/internal/session"
|
||||
"git.happydns.org/happyDomain/internal/storage"
|
||||
"git.happydns.org/happyDomain/internal/usecase"
|
||||
authuserUC "git.happydns.org/happyDomain/internal/usecase/authuser"
|
||||
sessionUC "git.happydns.org/happyDomain/internal/usecase/session"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/web"
|
||||
|
|
@ -212,6 +213,7 @@ func (app *App) initInsights() {
|
|||
}
|
||||
|
||||
func (app *App) initUsecases() {
|
||||
sessionService := sessionUC.NewSessionUsecases(app.store)
|
||||
app.usecases.providerSpecs = usecase.NewProviderSpecsUsecase()
|
||||
app.usecases.provider = usecase.NewProviderUsecase(app.cfg, app.store)
|
||||
app.usecases.providerSettings = usecase.NewProviderSettingsUsecase(app.cfg, app.usecases.provider, app.store)
|
||||
|
|
@ -223,9 +225,9 @@ func (app *App) initUsecases() {
|
|||
|
||||
app.usecases.user = usecase.NewUserUsecase(app.store, app.newsletter)
|
||||
app.usecases.authentication = usecase.NewAuthenticationUsecase(app.cfg, app.store, app.usecases.user)
|
||||
app.usecases.authUser = usecase.NewAuthUserUsecase(app.cfg, app.mailer, app.store)
|
||||
app.usecases.authUser = authuserUC.NewAuthUserUsecases(app.cfg, app.mailer, app.store, sessionService.CloseUserSessionsUC)
|
||||
app.usecases.resolver = usecase.NewResolverUsecase(app.cfg)
|
||||
app.usecases.session = sessionUC.NewSessionUsecases(app.store)
|
||||
app.usecases.session = sessionService
|
||||
}
|
||||
|
||||
func (app *App) setupRouter() {
|
||||
|
|
|
|||
|
|
@ -36,11 +36,6 @@ type AuthenticationStorage interface {
|
|||
user.UserStorage
|
||||
}
|
||||
|
||||
type AuthUserAndSessionStorage interface {
|
||||
authuser.AuthUserStorage
|
||||
session.SessionStorage
|
||||
}
|
||||
|
||||
type ProviderAndDomainStorage interface {
|
||||
provider.ProviderStorage
|
||||
domain.DomainStorage
|
||||
|
|
|
|||
|
|
@ -1,247 +0,0 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
"git.happydns.org/happyDomain/internal/mailer"
|
||||
"git.happydns.org/happyDomain/internal/storage"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
type authUserUsecase struct {
|
||||
config *happydns.Options
|
||||
mailer *mailer.Mailer
|
||||
store storage.AuthUserAndSessionStorage
|
||||
}
|
||||
|
||||
func NewAuthUserUsecase(cfg *happydns.Options, m *mailer.Mailer, store storage.AuthUserAndSessionStorage) happydns.AuthUserUsecase {
|
||||
return &authUserUsecase{
|
||||
config: cfg,
|
||||
mailer: m,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) CanRegister(user happydns.UserRegistration) error {
|
||||
if auu.config.DisableRegistration {
|
||||
return fmt.Errorf("Registration are closed on this instance.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) CheckNewPassword(user *happydns.UserAuth, request happydns.ChangePasswordForm) error {
|
||||
if !user.CheckPassword(request.Current) {
|
||||
return fmt.Errorf("The given current password is invalid.")
|
||||
}
|
||||
|
||||
return auu.CheckPassword(user, request)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) CheckPassword(user *happydns.UserAuth, request happydns.ChangePasswordForm) error {
|
||||
if err := user.CheckPasswordConstraints(request.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.Password != request.PasswordConfirm {
|
||||
return fmt.Errorf("The new password and its confirmation are different.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) ChangePassword(user *happydns.UserAuth, newPassword string) error {
|
||||
if err := user.DefinePassword(newPassword); err != nil {
|
||||
return fmt.Errorf("unable to change user password: %w", err)
|
||||
}
|
||||
|
||||
return auu.store.UpdateAuthUser(user)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) CloseAuthUserSessions(user *happydns.UserAuth) error {
|
||||
// Retrieve all user's sessions to disconnect them
|
||||
sessions, err := auu.store.ListAuthUserSessions(user)
|
||||
if err != nil {
|
||||
return happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to GetUserSessions in deleteAuthUser: %s", err.Error()),
|
||||
UserMessage: "Sorry, we are currently unable to delete your profile. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
var errs error
|
||||
for _, session := range sessions {
|
||||
err = auu.store.DeleteSession(session.Id)
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) CreateAuthUser(uu happydns.UserRegistration) (*happydns.UserAuth, error) {
|
||||
if len(uu.Email) <= 3 || strings.Index(uu.Email, "@") == -1 {
|
||||
return nil, fmt.Errorf("The given email is invalid.")
|
||||
}
|
||||
|
||||
if len(uu.Password) <= 7 {
|
||||
return nil, fmt.Errorf("The given password is invalid.")
|
||||
}
|
||||
|
||||
exists, err := auu.store.AuthUserExists(uu.Email)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to AuthUserExists in CreateAuthUser: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("An account already exists with the given address. Try login now.")
|
||||
}
|
||||
|
||||
user, err := happydns.NewUserAuth(uu.Email, uu.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user.AllowCommercials = uu.Newsletter
|
||||
|
||||
if err := auu.store.CreateAuthUser(user); err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to CreateUser in CreateAuthUser: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
if auu.mailer != nil {
|
||||
if err = auu.SendValidationLink(user); err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to SendValidationLink in registerUser: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return user, auu.store.CreateAuthUser(user)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) DeleteAuthUser(user *happydns.UserAuth, password string) error {
|
||||
if !user.CheckPassword(password) {
|
||||
return fmt.Errorf("The given current password is invalid.")
|
||||
}
|
||||
|
||||
if err := auu.store.DeleteAuthUser(user); err != nil {
|
||||
return happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to DeleteAuthUser in deleteauthuser: %s", err.Error()),
|
||||
UserMessage: "Sorry, we are currently unable to delete your profile. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
return auu.CloseAuthUserSessions(user)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) GetAuthUser(uid happydns.Identifier) (*happydns.UserAuth, error) {
|
||||
return auu.store.GetAuthUser(uid)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) GetAuthUserByEmail(email string) (*happydns.UserAuth, error) {
|
||||
return auu.store.GetAuthUserByEmail(email)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) ListAuthUserSessions(user *happydns.UserAuth) ([]*happydns.Session, error) {
|
||||
return auu.store.ListAuthUserSessions(user)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) GenerateRecoveryLink(user *happydns.UserAuth) string {
|
||||
return user.GetAccountRecoveryURL(auu.config.GetBaseURL())
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) SendRecoveryLink(user *happydns.UserAuth) error {
|
||||
toName := helpers.GenUsername(user.Email)
|
||||
|
||||
link := auu.GenerateRecoveryLink(user)
|
||||
|
||||
err := auu.store.UpdateAuthUser(user)
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
|
||||
return auu.mailer.SendMail(
|
||||
&mail.Address{Name: toName, Address: user.Email},
|
||||
"Recover your happyDomain account",
|
||||
`Hi `+toName+`,
|
||||
|
||||
You've just ask on our platform to recover your account.
|
||||
|
||||
In order to define a new password, please follow this link now:
|
||||
|
||||
[Recover my account](`+link+`)`,
|
||||
)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) GenerateValidationLink(user *happydns.UserAuth) string {
|
||||
return user.GetRegistrationURL(auu.config.GetBaseURL())
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) SendValidationLink(user *happydns.UserAuth) error {
|
||||
if auu.mailer == nil {
|
||||
return fmt.Errorf("No mailer configured")
|
||||
}
|
||||
|
||||
toName := helpers.GenUsername(user.Email)
|
||||
return auu.mailer.SendMail(
|
||||
&mail.Address{Name: toName, Address: user.Email},
|
||||
"Your new account on happyDomain",
|
||||
`Welcome to happyDomain!
|
||||
--------------------
|
||||
|
||||
Hi `+toName+`,
|
||||
|
||||
We are pleased that you created an account on our great domain name
|
||||
management platform!
|
||||
|
||||
In order to validate your account, please follow this link now:
|
||||
|
||||
[Validate my account](`+auu.GenerateValidationLink(user)+`)`,
|
||||
)
|
||||
}
|
||||
|
||||
func (auu *authUserUsecase) ValidateEmail(user *happydns.UserAuth, form happydns.AddressValidationForm) error {
|
||||
if err := user.ValidateEmail(form.Key); err != nil {
|
||||
return happydns.ValidationError{fmt.Sprintf("bad email validation key: %s", err.Error())}
|
||||
}
|
||||
|
||||
if err := auu.store.UpdateAuthUser(user); err != nil {
|
||||
return happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to UpdateUser in ValidateUserAddress: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to update your profile. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
132
internal/usecase/authuser/account_recovery.go
Normal file
132
internal/usecase/authuser/account_recovery.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
"git.happydns.org/happyDomain/internal/mailer"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// AccountRecoveryHashValidityis the time during which the recovery link is at least valid.
|
||||
const AccountRecoveryHashValidity = 2 * time.Hour
|
||||
|
||||
// GenAccountRecoveryHash generates the recovery hash for the current or previous period.
|
||||
// It updates the UserAuth structure in some cases, when it needs to generate a new recovery key,
|
||||
// so don't forget to save the changes made.
|
||||
func GenAccountRecoveryHash(recoveryKey []byte, previous bool) string {
|
||||
date := time.Now()
|
||||
date = date.Truncate(AccountRecoveryHashValidity)
|
||||
if previous {
|
||||
date = date.Add(AccountRecoveryHashValidity * -1)
|
||||
}
|
||||
|
||||
if len(recoveryKey) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
h := hmac.New(sha512.New, recoveryKey)
|
||||
h.Write(date.AppendFormat([]byte{}, time.RFC3339))
|
||||
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// CanRecoverAccount checks if the given key is a valid recovery hash.
|
||||
func CanRecoverAccount(u *happydns.UserAuth, key string) error {
|
||||
if key == GenAccountRecoveryHash(u.PasswordRecoveryKey, false) || key == GenAccountRecoveryHash(u.PasswordRecoveryKey, true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("The account recovery link you follow is invalid or has expired (it is valid during %d hours)", AccountRecoveryHashValidity/time.Hour)
|
||||
}
|
||||
|
||||
type RecoverAccountUsecase struct {
|
||||
store AuthUserStorage
|
||||
mailer *mailer.Mailer
|
||||
config *happydns.Options
|
||||
changePassword *ChangePasswordUsecase
|
||||
}
|
||||
|
||||
func NewRecoverAccountUsecase(store AuthUserStorage, mailer *mailer.Mailer, config *happydns.Options, changePassword *ChangePasswordUsecase) *RecoverAccountUsecase {
|
||||
return &RecoverAccountUsecase{
|
||||
store: store,
|
||||
mailer: mailer,
|
||||
config: config,
|
||||
changePassword: changePassword,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateLink returns the absolute URL corresponding to the recovery
|
||||
// URL of the given account.
|
||||
func (uc *RecoverAccountUsecase) GenerateLink(user *happydns.UserAuth) (string, error) {
|
||||
if user.PasswordRecoveryKey == nil {
|
||||
user.PasswordRecoveryKey = make([]byte, 64)
|
||||
if _, err := rand.Read(user.PasswordRecoveryKey); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := uc.store.UpdateAuthUser(user); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return uc.config.GetBaseURL() + fmt.Sprintf("/forgotten-password?u=%s&k=%s", base64.RawURLEncoding.EncodeToString(user.Id), GenAccountRecoveryHash(user.PasswordRecoveryKey, false)), nil
|
||||
}
|
||||
|
||||
func (uc *RecoverAccountUsecase) SendLink(user *happydns.UserAuth) error {
|
||||
link, err := uc.GenerateLink(user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate recovery link: %w", err)
|
||||
}
|
||||
|
||||
toName := helpers.GenUsername(user.Email)
|
||||
return uc.mailer.SendMail(
|
||||
&mail.Address{Name: toName, Address: user.Email},
|
||||
"Recover your happyDomain account",
|
||||
fmt.Sprintf(`Hi %s,
|
||||
|
||||
You've just ask on our platform to recover your account.
|
||||
|
||||
In order to define a new password, please follow this link now:
|
||||
|
||||
[Recover my account](%s)`, toName, link),
|
||||
)
|
||||
}
|
||||
|
||||
func (uc *RecoverAccountUsecase) ResetPassword(user *happydns.UserAuth, form happydns.AccountRecoveryForm) error {
|
||||
if err := CanRecoverAccount(user, form.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := uc.changePassword.Change(user, form.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
43
internal/usecase/authuser/can_register.go
Normal file
43
internal/usecase/authuser/can_register.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// CanRegisterUsecase struct holds the configuration to check if registration is allowed.
|
||||
type CanRegisterUsecase struct {
|
||||
config *happydns.Options
|
||||
}
|
||||
|
||||
// NewCanRegisterUsecase creates a new instance of CanRegisterUsecase.
|
||||
func NewCanRegisterUsecase(cfg *happydns.Options) *CanRegisterUsecase {
|
||||
return &CanRegisterUsecase{
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// IsOpened returns true if user registrations are enabled on this instance.
|
||||
func (uc *CanRegisterUsecase) IsOpened() bool {
|
||||
return !uc.config.DisableRegistration
|
||||
}
|
||||
85
internal/usecase/authuser/change_password.go
Normal file
85
internal/usecase/authuser/change_password.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// ChangePasswordUsecase handles the logic for changing a user's password.
|
||||
type ChangePasswordUsecase struct {
|
||||
store AuthUserStorage
|
||||
checkPasswordConstraints *CheckPasswordConstraintsUsecase
|
||||
}
|
||||
|
||||
// NewChangePasswordUsecase creates a new instance of ChangePasswordUsecase.
|
||||
func NewChangePasswordUsecase(store AuthUserStorage, checkPasswordConstraints *CheckPasswordConstraintsUsecase) *ChangePasswordUsecase {
|
||||
return &ChangePasswordUsecase{
|
||||
store: store,
|
||||
checkPasswordConstraints: checkPasswordConstraints,
|
||||
}
|
||||
}
|
||||
|
||||
// Change changes the password of the given user after verifying the current password
|
||||
// and checking new password constraints (length, confirmation, etc.).
|
||||
func (uc *ChangePasswordUsecase) Change(user *happydns.UserAuth, password string) error {
|
||||
// Validate the new password according to application constraints
|
||||
if err := uc.checkPasswordConstraints.Check(password); err != nil {
|
||||
return happydns.ValidationError{Msg: err.Error()}
|
||||
}
|
||||
|
||||
// Apply the new password to the user
|
||||
if err := user.DefinePassword(password); err != nil {
|
||||
return fmt.Errorf("unable to change user password: %w", err)
|
||||
}
|
||||
|
||||
// Persist the updated user information
|
||||
if err := uc.store.UpdateAuthUser(user); err != nil {
|
||||
return fmt.Errorf("unable to save new password: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *ChangePasswordUsecase) CheckNewPassword(user *happydns.UserAuth, form happydns.ChangePasswordForm) error {
|
||||
if !user.CheckPassword(form.Current) {
|
||||
return happydns.ValidationError{Msg: "bad current password"}
|
||||
}
|
||||
|
||||
return uc.CheckResetPassword(user, form)
|
||||
}
|
||||
|
||||
func (uc *ChangePasswordUsecase) CheckResetPassword(user *happydns.UserAuth, form happydns.ChangePasswordForm) error {
|
||||
// Validate the new password according to application constraints
|
||||
if err := uc.checkPasswordConstraints.Check(form.Password); err != nil {
|
||||
return happydns.ValidationError{Msg: err.Error()}
|
||||
}
|
||||
|
||||
// Confirm the new password matches its confirmation
|
||||
if form.Password != form.PasswordConfirm {
|
||||
return happydns.ValidationError{Msg: "the new password and its confirmation are different."}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
59
internal/usecase/authuser/check_password_constraints.go
Normal file
59
internal/usecase/authuser/check_password_constraints.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// CheckPasswordConstraintsUsecase struct contains the necessary dependencies
|
||||
// for the usecase to check the password constraints.
|
||||
type CheckPasswordConstraintsUsecase struct {
|
||||
// No external dependencies are required.
|
||||
}
|
||||
|
||||
func NewCheckPasswordConstraintsUsecase() *CheckPasswordConstraintsUsecase {
|
||||
return &CheckPasswordConstraintsUsecase{}
|
||||
}
|
||||
|
||||
// Check checks the given password to see if it adheres to the system's constraints.
|
||||
// It validates length and format as necessary.
|
||||
// Returns an error if the password is invalid.
|
||||
func (uc *CheckPasswordConstraintsUsecase) Check(password string) error {
|
||||
if len(password) < 8 {
|
||||
return fmt.Errorf("password must be at least 8 characters long")
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`[a-z]`).MatchString(password) {
|
||||
return fmt.Errorf("Password must contain lower case letters.")
|
||||
} else if !regexp.MustCompile(`[A-Z]`).MatchString(password) {
|
||||
return fmt.Errorf("Password must contain upper case letters.")
|
||||
} else if !regexp.MustCompile(`[0-9]`).MatchString(password) {
|
||||
return fmt.Errorf("Password must contain numbers.")
|
||||
} else if len(password) < 11 && !regexp.MustCompile(`[^a-zA-Z0-9]`).MatchString(password) {
|
||||
return fmt.Errorf("Password must be longer or contain symbols.")
|
||||
}
|
||||
|
||||
// If all conditions are met, return nil (no error)
|
||||
return nil
|
||||
}
|
||||
104
internal/usecase/authuser/create_auth_user.go
Normal file
104
internal/usecase/authuser/create_auth_user.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/mailer"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// CreateAuthUserUsecase handles the creation of a new authenticated user account.
|
||||
type CreateAuthUserUsecase struct {
|
||||
store AuthUserStorage
|
||||
mailer *mailer.Mailer
|
||||
checkPasswordConstraints *CheckPasswordConstraintsUsecase
|
||||
emailValidation *EmailValidationUsecase
|
||||
}
|
||||
|
||||
// NewCreateAuthUserUsecase initializes a new instance of CreateAuthUserUsecase.
|
||||
func NewCreateAuthUserUsecase(store AuthUserStorage, mailer *mailer.Mailer, checkPasswordConstraints *CheckPasswordConstraintsUsecase, emailValidation *EmailValidationUsecase) *CreateAuthUserUsecase {
|
||||
return &CreateAuthUserUsecase{
|
||||
store: store,
|
||||
mailer: mailer,
|
||||
checkPasswordConstraints: checkPasswordConstraints,
|
||||
emailValidation: emailValidation,
|
||||
}
|
||||
}
|
||||
|
||||
// Create validates the registration request, creates the user, and optionally sends a validation email.
|
||||
func (uc *CreateAuthUserUsecase) Create(uu happydns.UserRegistration) (*happydns.UserAuth, error) {
|
||||
// Validate email format
|
||||
if len(uu.Email) <= 3 || !strings.Contains(uu.Email, "@") {
|
||||
return nil, happydns.ValidationError{Msg: "the given email is invalid"}
|
||||
}
|
||||
|
||||
// Validate password strength
|
||||
err := uc.checkPasswordConstraints.Check(uu.Password)
|
||||
if err != nil {
|
||||
return nil, happydns.ValidationError{Msg: err.Error()}
|
||||
}
|
||||
|
||||
// Check if an account already exists with this email
|
||||
exists, err := uc.store.AuthUserExists(uu.Email)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to check if user exists: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
return nil, happydns.ValidationError{Msg: "an account already exists with the given address. Try logging in."}
|
||||
}
|
||||
|
||||
// Create the user object
|
||||
user, err := happydns.NewUserAuth(uu.Email, uu.Password)
|
||||
if err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to create user object: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
user.AllowCommercials = uu.Newsletter
|
||||
|
||||
// Persist the new user in the storage layer
|
||||
if err := uc.store.CreateAuthUser(user); err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to create user in storage: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally send the validation email if mailer is configured
|
||||
if uc.mailer != nil {
|
||||
if err = uc.emailValidation.SendLink(user); err != nil {
|
||||
return nil, happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to send validation email: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to create your account. Please try again later.",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
64
internal/usecase/authuser/delete_auth_user.go
Normal file
64
internal/usecase/authuser/delete_auth_user.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sessionUC "git.happydns.org/happyDomain/internal/usecase/session"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// DeleteAuthUserUsecase represents the use case for deleting an authenticated user and their sessions.
|
||||
type DeleteAuthUserUsecase struct {
|
||||
store AuthUserStorage
|
||||
closeUserSessions *sessionUC.CloseUserSessionsUsecase
|
||||
}
|
||||
|
||||
// NewDeleteAuthUserUsecase creates a new instance of DeleteAuthUserUsecase.
|
||||
func NewDeleteAuthUserUsecase(store AuthUserStorage, closeUserSessions *sessionUC.CloseUserSessionsUsecase) *DeleteAuthUserUsecase {
|
||||
return &DeleteAuthUserUsecase{
|
||||
store: store,
|
||||
closeUserSessions: closeUserSessions,
|
||||
}
|
||||
}
|
||||
|
||||
// Do deletes an authenticated user from the system, ensuring their sessions are also removed.
|
||||
// It first verifies the current password, then removes the user and their associated sessions from the storage.
|
||||
func (uc *DeleteAuthUserUsecase) Delete(user *happydns.UserAuth, password string) error {
|
||||
// Step 1: Verify the current password.
|
||||
if !user.CheckPassword(password) {
|
||||
return fmt.Errorf("invalid current password")
|
||||
}
|
||||
|
||||
// Step 2: Delete the user's sessions.
|
||||
if err := uc.closeUserSessions.CloseAll(user); err != nil {
|
||||
return fmt.Errorf("unable to delete user sessions: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Delete the user from the storage.
|
||||
if err := uc.store.DeleteAuthUser(user); err != nil {
|
||||
return fmt.Errorf("unable to delete user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
119
internal/usecase/authuser/email_validation.go
Normal file
119
internal/usecase/authuser/email_validation.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/helpers"
|
||||
"git.happydns.org/happyDomain/internal/mailer"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// RegistrationHashValidity is the time during which the email validation link is at least valid.
|
||||
const RegistrationHashValidity = 24 * time.Hour
|
||||
|
||||
// GenRegistrationHash generates the validation hash for the current or previous period.
|
||||
// The hash computation is based on some already filled fields in the structure.
|
||||
func GenRegistrationHash(u *happydns.UserAuth, previous bool) string {
|
||||
date := time.Now()
|
||||
if previous {
|
||||
date = date.Add(RegistrationHashValidity * -1)
|
||||
}
|
||||
date = date.Truncate(RegistrationHashValidity)
|
||||
|
||||
h := hmac.New(
|
||||
sha512.New,
|
||||
[]byte(u.CreatedAt.Format(time.RFC3339Nano)),
|
||||
)
|
||||
h.Write(date.AppendFormat([]byte{}, time.RFC3339))
|
||||
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
type EmailValidationUsecase struct {
|
||||
store AuthUserStorage
|
||||
mailer *mailer.Mailer
|
||||
config *happydns.Options
|
||||
}
|
||||
|
||||
func NewEmailValidationUsecase(store AuthUserStorage, mailer *mailer.Mailer, config *happydns.Options) *EmailValidationUsecase {
|
||||
return &EmailValidationUsecase{
|
||||
store: store,
|
||||
mailer: mailer,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateLink returns the absolute URL corresponding to the recovery
|
||||
// URL of the given account.
|
||||
func (uc *EmailValidationUsecase) GenerateLink(user *happydns.UserAuth) string {
|
||||
return uc.config.GetBaseURL() + fmt.Sprintf("/email-validation?u=%s&k=%s", base64.RawURLEncoding.EncodeToString(user.Id), GenRegistrationHash(user, false))
|
||||
}
|
||||
|
||||
func (uc *EmailValidationUsecase) SendLink(user *happydns.UserAuth) error {
|
||||
if uc.mailer == nil {
|
||||
return fmt.Errorf("no mailer configured")
|
||||
}
|
||||
|
||||
toName := helpers.GenUsername(user.Email)
|
||||
return uc.mailer.SendMail(
|
||||
&mail.Address{Name: toName, Address: user.Email},
|
||||
"Your new account on happyDomain",
|
||||
fmt.Sprintf(`
|
||||
Welcome to happyDomain!
|
||||
-----------------------
|
||||
|
||||
Hi %s,
|
||||
|
||||
We are pleased that you created an account on our great domain name
|
||||
management platform!
|
||||
|
||||
In order to validate your account, please follow this link now:
|
||||
|
||||
[Validate my account](%s)
|
||||
`, toName, uc.GenerateLink(user)),
|
||||
)
|
||||
}
|
||||
|
||||
// Validate tries to validate the email address by comparing the given key to the expected one.
|
||||
func (uc *EmailValidationUsecase) Validate(user *happydns.UserAuth, form happydns.AddressValidationForm) error {
|
||||
if form.Key != GenRegistrationHash(user, false) && form.Key != GenRegistrationHash(user, true) {
|
||||
return happydns.ValidationError{Msg: fmt.Sprintf("bad email validation key: the validation address link you follow is invalid or has expired (it is valid during %d hours)", RegistrationHashValidity/time.Hour)}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
user.EmailVerification = &now
|
||||
|
||||
if err := uc.store.UpdateAuthUser(user); err != nil {
|
||||
return happydns.InternalError{
|
||||
Err: fmt.Errorf("unable to update auth user: %w", err),
|
||||
UserMessage: "Sorry, we are currently unable to update your profile. Please try again later.",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
128
internal/usecase/authuser/factory.go
Normal file
128
internal/usecase/authuser/factory.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.happydns.org/happyDomain/internal/mailer"
|
||||
sessionUC "git.happydns.org/happyDomain/internal/usecase/session"
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// Service groups all use cases related to user authentication and management.
|
||||
type Service struct {
|
||||
// Usecases for user management actions
|
||||
CanRegisterUC *CanRegisterUsecase
|
||||
ChangePasswordUC *ChangePasswordUsecase
|
||||
CheckPasswordConstraintsUC *CheckPasswordConstraintsUsecase
|
||||
CreateAuthUserUC *CreateAuthUserUsecase
|
||||
DeleteAuthUserUC *DeleteAuthUserUsecase
|
||||
EmailValidationUC *EmailValidationUsecase
|
||||
GetAuthUserUC *GetAuthUserUsecase
|
||||
RecoverAccountUC *RecoverAccountUsecase
|
||||
}
|
||||
|
||||
// NewAuthUserService initializes and returns a new AuthUserService, containing all use cases.
|
||||
func NewAuthUserUsecases(
|
||||
cfg *happydns.Options,
|
||||
mailer *mailer.Mailer,
|
||||
store AuthUserStorage,
|
||||
closeUserSessionsUseCase *sessionUC.CloseUserSessionsUsecase,
|
||||
) *Service {
|
||||
checkPasswordConstraintsUC := NewCheckPasswordConstraintsUsecase()
|
||||
changePasswordUC := NewChangePasswordUsecase(store, checkPasswordConstraintsUC)
|
||||
emailValidationUC := NewEmailValidationUsecase(store, mailer, cfg)
|
||||
getAuthUserUC := NewGetAuthUserUsecase(store)
|
||||
|
||||
// Initialize each usecase by injecting required dependencies.
|
||||
return &Service{
|
||||
CanRegisterUC: NewCanRegisterUsecase(cfg),
|
||||
ChangePasswordUC: changePasswordUC,
|
||||
CheckPasswordConstraintsUC: checkPasswordConstraintsUC,
|
||||
CreateAuthUserUC: NewCreateAuthUserUsecase(store, mailer, checkPasswordConstraintsUC, emailValidationUC),
|
||||
DeleteAuthUserUC: NewDeleteAuthUserUsecase(store, closeUserSessionsUseCase),
|
||||
EmailValidationUC: emailValidationUC,
|
||||
GetAuthUserUC: getAuthUserUC,
|
||||
RecoverAccountUC: NewRecoverAccountUsecase(store, mailer, cfg, changePasswordUC),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CanRegister(user happydns.UserRegistration) error {
|
||||
if !s.CanRegisterUC.IsOpened() {
|
||||
return fmt.Errorf("Registration are closed on this instance.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) CheckPassword(user *happydns.UserAuth, request happydns.ChangePasswordForm) error {
|
||||
return s.ChangePasswordUC.CheckResetPassword(user, request)
|
||||
}
|
||||
|
||||
func (s *Service) CheckNewPassword(user *happydns.UserAuth, request happydns.ChangePasswordForm) error {
|
||||
return s.ChangePasswordUC.CheckNewPassword(user, request)
|
||||
}
|
||||
|
||||
func (s *Service) ChangePassword(user *happydns.UserAuth, newPassword string) error {
|
||||
return s.ChangePasswordUC.Change(user, newPassword)
|
||||
}
|
||||
|
||||
func (s *Service) CreateAuthUser(uu happydns.UserRegistration) (*happydns.UserAuth, error) {
|
||||
return s.CreateAuthUserUC.Create(uu)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteAuthUser(user *happydns.UserAuth, password string) error {
|
||||
return s.DeleteAuthUserUC.Delete(user, password)
|
||||
}
|
||||
|
||||
func (s *Service) GetAuthUser(userID happydns.Identifier) (*happydns.UserAuth, error) {
|
||||
return s.GetAuthUserUC.ByID(userID)
|
||||
}
|
||||
|
||||
func (s *Service) GetAuthUserByEmail(email string) (*happydns.UserAuth, error) {
|
||||
return s.GetAuthUserUC.ByEmail(email)
|
||||
}
|
||||
|
||||
func (s *Service) GenerateRecoveryLink(user *happydns.UserAuth) (string, error) {
|
||||
return s.RecoverAccountUC.GenerateLink(user)
|
||||
}
|
||||
|
||||
func (s *Service) SendRecoveryLink(user *happydns.UserAuth) error {
|
||||
return s.RecoverAccountUC.SendLink(user)
|
||||
}
|
||||
|
||||
func (s *Service) GenerateValidationLink(user *happydns.UserAuth) string {
|
||||
return s.EmailValidationUC.GenerateLink(user)
|
||||
}
|
||||
|
||||
func (s *Service) ResetPassword(user *happydns.UserAuth, form happydns.AccountRecoveryForm) error {
|
||||
return s.RecoverAccountUC.ResetPassword(user, form)
|
||||
}
|
||||
|
||||
func (s *Service) SendValidationLink(user *happydns.UserAuth) error {
|
||||
return s.EmailValidationUC.SendLink(user)
|
||||
}
|
||||
|
||||
func (s *Service) ValidateEmail(user *happydns.UserAuth, form happydns.AddressValidationForm) error {
|
||||
return s.EmailValidationUC.Validate(user, form)
|
||||
}
|
||||
60
internal/usecase/authuser/get_auth_user.go
Normal file
60
internal/usecase/authuser/get_auth_user.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package authuser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
||||
// GetAuthUserUsecase handles retrieval of authenticated users by ID.
|
||||
type GetAuthUserUsecase struct {
|
||||
store AuthUserStorage
|
||||
}
|
||||
|
||||
// NewGetAuthUserUsecase creates a new instance of GetAuthUserUsecase.
|
||||
func NewGetAuthUserUsecase(store AuthUserStorage) *GetAuthUserUsecase {
|
||||
return &GetAuthUserUsecase{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// ByID retrieves an authenticated user from the storage by their unique identifier.
|
||||
// Returns the user if found, or an error otherwise.
|
||||
func (uc *GetAuthUserUsecase) ByID(id happydns.Identifier) (*happydns.UserAuth, error) {
|
||||
user, err := uc.store.GetAuthUser(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get user by ID: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ByEmail retrieves an authenticated user from the storage by their email address.
|
||||
// Returns the user if found, or an error otherwise.
|
||||
func (uc *GetAuthUserUsecase) ByEmail(email string) (*happydns.UserAuth, error) {
|
||||
user, err := uc.store.GetAuthUserByEmail(email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get user by email: %w", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
|
@ -22,12 +22,6 @@
|
|||
package happydns
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
|
@ -89,31 +83,8 @@ func (u *UserAuth) JoinNewsletter() bool {
|
|||
return u.AllowCommercials
|
||||
}
|
||||
|
||||
// CheckPasswordConstraints checks the given password is strong enough.
|
||||
func (u *UserAuth) CheckPasswordConstraints(password string) (err error) {
|
||||
if len(password) < 8 {
|
||||
return fmt.Errorf("Password has to be at least 8 characters long.")
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`[a-z]`).MatchString(password) {
|
||||
return fmt.Errorf("Password has to contain lower case letters.")
|
||||
} else if !regexp.MustCompile(`[A-Z]`).MatchString(password) {
|
||||
return fmt.Errorf("Password has to contain upper case letters.")
|
||||
} else if !regexp.MustCompile(`[0-9]`).MatchString(password) {
|
||||
return fmt.Errorf("Password has to contain numbers.")
|
||||
} else if len(password) < 11 && !regexp.MustCompile(`[^a-zA-Z0-9]`).MatchString(password) {
|
||||
return fmt.Errorf("Password has to be longer or contain symbols.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefinePassword erases the current UserAuth's password by the new one given.
|
||||
func (u *UserAuth) DefinePassword(password string) (err error) {
|
||||
if err = u.CheckPasswordConstraints(password); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
u.Password, err = bcrypt.GenerateFromPassword([]byte(password), 0)
|
||||
u.PasswordRecoveryKey = nil
|
||||
|
||||
|
|
@ -129,101 +100,17 @@ func (u *UserAuth) CheckPassword(password string) bool {
|
|||
return bcrypt.CompareHashAndPassword(u.Password, []byte(password)) == nil
|
||||
}
|
||||
|
||||
// RegistrationHashValidity is the time during which the email validation link is at least valid.
|
||||
const RegistrationHashValidity = 24 * time.Hour
|
||||
|
||||
// GenRegistrationHash generates the validation hash for the current or previous period.
|
||||
// The hash computation is based on some already filled fields in the structure.
|
||||
func (u *UserAuth) GenRegistrationHash(previous bool) string {
|
||||
date := time.Now()
|
||||
if previous {
|
||||
date = date.Add(RegistrationHashValidity * -1)
|
||||
}
|
||||
date = date.Truncate(RegistrationHashValidity)
|
||||
|
||||
h := hmac.New(
|
||||
sha512.New,
|
||||
[]byte(u.CreatedAt.Format(time.RFC3339Nano)),
|
||||
)
|
||||
h.Write(date.AppendFormat([]byte{}, time.RFC3339))
|
||||
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// ValidateEmail tries to validate the email address by comparing the given key to the expected one.
|
||||
func (u *UserAuth) ValidateEmail(key string) error {
|
||||
if key == u.GenRegistrationHash(false) || key == u.GenRegistrationHash(true) {
|
||||
now := time.Now()
|
||||
u.EmailVerification = &now
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("The validation address link you follow is invalid or has expired (it is valid during %d hours)", RegistrationHashValidity/time.Hour)
|
||||
}
|
||||
|
||||
// AccountRecoveryHashValidityis the time during which the recovery link is at least valid.
|
||||
const AccountRecoveryHashValidity = 2 * time.Hour
|
||||
|
||||
// GenAccountRecoveryHash generates the recovery hash for the current or previous period.
|
||||
// It updates the UserAuth structure in some cases, when it needs to generate a new recovery key,
|
||||
// so don't forget to save the changes made.
|
||||
func (u *UserAuth) GenAccountRecoveryHash(previous bool) string {
|
||||
if u.PasswordRecoveryKey == nil {
|
||||
u.PasswordRecoveryKey = make([]byte, 64)
|
||||
if _, err := rand.Read(u.PasswordRecoveryKey); err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
date := time.Now()
|
||||
date = date.Truncate(AccountRecoveryHashValidity)
|
||||
if previous {
|
||||
date = date.Add(AccountRecoveryHashValidity * -1)
|
||||
}
|
||||
|
||||
if len(u.PasswordRecoveryKey) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
h := hmac.New(
|
||||
sha512.New,
|
||||
u.PasswordRecoveryKey,
|
||||
)
|
||||
h.Write(date.AppendFormat([]byte{}, time.RFC3339))
|
||||
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// CanRecoverAccount checks if the given key is a valid recovery hash.
|
||||
func (u *UserAuth) CanRecoverAccount(key string) error {
|
||||
if key == u.GenAccountRecoveryHash(false) || key == u.GenAccountRecoveryHash(true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("The account recovery link you follow is invalid or has expired (it is valid during %d hours)", AccountRecoveryHashValidity/time.Hour)
|
||||
}
|
||||
|
||||
// GetAccountRecoveryURL returns the absolute URL corresponding to the recovery
|
||||
// URL of the given account.
|
||||
func (u *UserAuth) GetAccountRecoveryURL(baseurl string) string {
|
||||
return baseurl + fmt.Sprintf("/forgotten-password?u=%s&k=%s", base64.RawURLEncoding.EncodeToString(u.Id), u.GenAccountRecoveryHash(false))
|
||||
}
|
||||
|
||||
// GetAccountRecoveryURL returns the absolute URL corresponding to the recovery
|
||||
// URL of the given account.
|
||||
func (u *UserAuth) GetRegistrationURL(baseurl string) string {
|
||||
return baseurl + fmt.Sprintf("/email-validation?u=%s&k=%s", base64.RawURLEncoding.EncodeToString(u.Id), u.GenRegistrationHash(false))
|
||||
}
|
||||
|
||||
type AuthUserUsecase interface {
|
||||
CanRegister(UserRegistration) error
|
||||
CheckPassword(*UserAuth, ChangePasswordForm) error
|
||||
CheckNewPassword(*UserAuth, ChangePasswordForm) error
|
||||
ChangePassword(*UserAuth, string) error
|
||||
CloseAuthUserSessions(*UserAuth) error
|
||||
CreateAuthUser(UserRegistration) (*UserAuth, error)
|
||||
DeleteAuthUser(*UserAuth, string) error
|
||||
GenerateRecoveryLink(*UserAuth) string
|
||||
GenerateRecoveryLink(*UserAuth) (string, error)
|
||||
GenerateValidationLink(*UserAuth) string
|
||||
GetAuthUser(Identifier) (*UserAuth, error)
|
||||
GetAuthUserByEmail(string) (*UserAuth, error)
|
||||
ResetPassword(*UserAuth, AccountRecoveryForm) error
|
||||
SendRecoveryLink(*UserAuth) error
|
||||
SendValidationLink(*UserAuth) error
|
||||
ValidateEmail(*UserAuth, AddressValidationForm) error
|
||||
|
|
|
|||
|
|
@ -21,6 +21,14 @@
|
|||
|
||||
package happydns
|
||||
|
||||
type AccountRecoveryForm struct {
|
||||
// Key is the secret sent by email to the user.
|
||||
Key string
|
||||
|
||||
// Password is the new password to use with this account.
|
||||
Password string
|
||||
}
|
||||
|
||||
type AddressValidationForm struct {
|
||||
// Key able to validate the email address.
|
||||
Key string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue