diff --git a/account/account.go b/account/account.go index b574dbb..352c022 100644 --- a/account/account.go +++ b/account/account.go @@ -3,12 +3,13 @@ package account import ( "crypto/ed25519" "errors" + "time" + "git.lumeweb.com/LumeWeb/portal/db/models" "github.com/spf13/viper" "go.uber.org/fx" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" - "time" ) var ( @@ -38,7 +39,7 @@ func NewAccountService(params AccountServiceParams) *AccountServiceDefault { return &AccountServiceDefault{db: params.Db, config: params.Config, identity: params.Identity} } -func (s *AccountServiceDefault) EmailExists(email string) (bool, *models.User, error) { +func (s *AccountServiceDefault) EmailExists(email string) (bool, *models.User, *AccountError) { user := &models.User{} exists, model, err := s.exists(user, map[string]interface{}{"email": email}) if !exists || err != nil { @@ -47,7 +48,7 @@ func (s *AccountServiceDefault) EmailExists(email string) (bool, *models.User, e return true, model.(*models.User), nil // Type assertion since `exists` returns interface{} } -func (s *AccountServiceDefault) PubkeyExists(pubkey string) (bool, *models.PublicKey, error) { +func (s *AccountServiceDefault) PubkeyExists(pubkey string) (bool, *models.PublicKey, *AccountError) { publicKey := &models.PublicKey{} exists, model, err := s.exists(publicKey, map[string]interface{}{"key": pubkey}) if !exists || err != nil { @@ -56,7 +57,7 @@ func (s *AccountServiceDefault) PubkeyExists(pubkey string) (bool, *models.Publi return true, model.(*models.PublicKey), nil // Type assertion is necessary } -func (s *AccountServiceDefault) AccountExists(id uint) (bool, *models.User, error) { +func (s *AccountServiceDefault) AccountExists(id uint) (bool, *models.User, *AccountError) { user := &models.User{} exists, model, err := s.exists(user, map[string]interface{}{"id": id}) if !exists || err != nil { @@ -65,15 +66,15 @@ func (s *AccountServiceDefault) AccountExists(id uint) (bool, *models.User, erro return true, model.(*models.User), nil // Ensure to assert the type correctly } -func (s *AccountServiceDefault) HashPassword(password string) (string, error) { +func (s *AccountServiceDefault) HashPassword(password string) (string, *AccountError) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { - return "", err + return "", NewAccountError(ErrKeyHashingFailed, err) } return string(bytes), nil } -func (s *AccountServiceDefault) CreateAccount(email string, password string) (*models.User, error) { +func (s *AccountServiceDefault) CreateAccount(email string, password string) (*models.User, *AccountError) { passwordHash, err := s.HashPassword(password) if err != nil { return nil, err @@ -86,7 +87,7 @@ func (s *AccountServiceDefault) CreateAccount(email string, password string) (*m result := s.db.Create(&user) if result.Error != nil { - return nil, result.Error + return nil, NewAccountError(ErrKeyAccountCreationFailed, result.Error) } return &user, nil @@ -96,7 +97,7 @@ func (s AccountServiceDefault) UpdateAccountName(userId uint, firstName string, return s.updateAccountInfo(userId, models.User{FirstName: firstName, LastName: lastName}) } -func (s AccountServiceDefault) AddPubkeyToAccount(user models.User, pubkey string) error { +func (s AccountServiceDefault) AddPubkeyToAccount(user models.User, pubkey string) *AccountError { var model models.PublicKey model.Key = pubkey @@ -105,12 +106,16 @@ func (s AccountServiceDefault) AddPubkeyToAccount(user models.User, pubkey strin result := s.db.Create(&model) if result.Error != nil { - return result.Error + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + return NewAccountError(ErrKeyPublicKeyExists, result.Error) + } + + return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error) } return nil } -func (s AccountServiceDefault) LoginPassword(email string, password string, ip string) (string, *models.User, error) { +func (s AccountServiceDefault) LoginPassword(email string, password string, ip string) (string, *models.User, *AccountError) { valid, user, err := s.ValidLoginByEmail(email, password) if err != nil { @@ -130,7 +135,7 @@ func (s AccountServiceDefault) LoginPassword(email string, password string, ip s return token, user, nil } -func (s AccountServiceDefault) LoginOTP(userId uint, code string) (string, error) { +func (s AccountServiceDefault) LoginOTP(userId uint, code string) (string, *AccountError) { valid, err := s.OTPVerify(userId, code) if err != nil { @@ -138,14 +143,14 @@ func (s AccountServiceDefault) LoginOTP(userId uint, code string) (string, error } if !valid { - return "", ErrInvalidOTPCode + return "", NewAccountError(ErrKeyInvalidOTPCode, nil) } var user models.User user.ID = userId - token, err := JWTGenerateToken(s.config.GetString("core.domain"), s.identity, user.ID, JWTPurposeLogin) - if err != nil { + token, tokenErr := JWTGenerateToken(s.config.GetString("core.domain"), s.identity, user.ID, JWTPurposeLogin) + if tokenErr != nil { return "", err } @@ -156,13 +161,17 @@ func (s AccountServiceDefault) ValidLoginByUserObj(user *models.User, password s return s.validPassword(user, password) } -func (s AccountServiceDefault) ValidLoginByEmail(email string, password string) (bool, *models.User, error) { +func (s AccountServiceDefault) ValidLoginByEmail(email string, password string) (bool, *models.User, *AccountError) { var user models.User result := s.db.Model(&models.User{}).Where(&models.User{Email: email}).First(&user) if result.RowsAffected == 0 || result.Error != nil { - return false, nil, result.Error + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return false, nil, NewAccountError(ErrKeyInvalidLogin, result.Error) + } + + return false, nil, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error) } valid := s.ValidLoginByUserObj(&user, password) @@ -174,7 +183,7 @@ func (s AccountServiceDefault) ValidLoginByEmail(email string, password string) return true, nil, nil } -func (s AccountServiceDefault) ValidLoginByUserID(id uint, password string) (bool, *models.User, error) { +func (s AccountServiceDefault) ValidLoginByUserID(id uint, password string) (bool, *models.User, *AccountError) { var user models.User user.ID = id @@ -182,7 +191,11 @@ func (s AccountServiceDefault) ValidLoginByUserID(id uint, password string) (boo result := s.db.Model(&user).Where(&user).First(&user) if result.RowsAffected == 0 || result.Error != nil { - return false, nil, result.Error + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return false, nil, NewAccountError(ErrKeyInvalidLogin, result.Error) + } + + return false, nil, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error) } valid := s.ValidLoginByUserObj(&user, password) @@ -194,13 +207,17 @@ func (s AccountServiceDefault) ValidLoginByUserID(id uint, password string) (boo return true, &user, nil } -func (s AccountServiceDefault) LoginPubkey(pubkey string) (string, error) { +func (s AccountServiceDefault) LoginPubkey(pubkey string) (string, *AccountError) { var model models.PublicKey result := s.db.Model(&models.PublicKey{}).Preload("User").Where(&models.PublicKey{Key: pubkey}).First(&model) if result.RowsAffected == 0 || result.Error != nil { - return "", result.Error + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return "", NewAccountError(ErrKeyInvalidLogin, result.Error) + } + + return "", NewAccountError(ErrKeyDatabaseOperationFailed, result.Error) } user := model.User @@ -214,7 +231,7 @@ func (s AccountServiceDefault) LoginPubkey(pubkey string) (string, error) { return token, nil } -func (s AccountServiceDefault) AccountPins(id uint, createdAfter uint64) ([]models.Pin, error) { +func (s AccountServiceDefault) AccountPins(id uint, createdAfter uint64) ([]models.Pin, *AccountError) { var pins []models.Pin result := s.db.Model(&models.Pin{}). @@ -225,7 +242,7 @@ func (s AccountServiceDefault) AccountPins(id uint, createdAfter uint64) ([]mode Find(&pins) if result.Error != nil { - return nil, result.Error + return nil, NewAccountError(ErrKeyPinsRetrievalFailed, result.Error) } return pins, nil @@ -303,23 +320,23 @@ func (s AccountServiceDefault) PinByID(uploadId uint, userId uint) error { return nil } -func (s AccountServiceDefault) OTPGenerate(userId uint) (string, error) { +func (s AccountServiceDefault) OTPGenerate(userId uint) (string, *AccountError) { exists, user, err := s.AccountExists(userId) if !exists || err != nil { return "", err } - otp, err := TOTPGenerate(user.Email, s.config.GetString("core.domain")) - if err != nil { - return "", err + otp, otpErr := TOTPGenerate(user.Email, s.config.GetString("core.domain")) + if otpErr != nil { + return "", NewAccountError(ErrKeyOTPGenerationFailed, otpErr) } err = s.updateAccountInfo(user.ID, models.User{OTPSecret: otp}) return otp, nil } -func (s AccountServiceDefault) OTPVerify(userId uint, code string) (bool, error) { +func (s AccountServiceDefault) OTPVerify(userId uint, code string) (bool, *AccountError) { exists, user, err := s.AccountExists(userId) if !exists || err != nil { @@ -351,21 +368,21 @@ func (s AccountServiceDefault) OTPDisable(userId uint) error { return s.updateAccountInfo(userId, models.User{OTPEnabled: false, OTPSecret: ""}) } -func (s AccountServiceDefault) doLogin(user *models.User, ip string) (string, error) { +func (s AccountServiceDefault) doLogin(user *models.User, ip string) (string, *AccountError) { purpose := JWTPurposeLogin if user.OTPEnabled { purpose = JWTPurpose2FA } - token, err := JWTGenerateToken(s.config.GetString("core.domain"), s.identity, user.ID, purpose) - if err != nil { - return "", err + token, jwtErr := JWTGenerateToken(s.config.GetString("core.domain"), s.identity, user.ID, purpose) + if jwtErr != nil { + return "", NewAccountError(ErrKeyJWTGenerationFailed, jwtErr) } now := time.Now() - err = s.updateAccountInfo(user.ID, models.User{LastLoginIP: ip, LastLogin: &now}) + err := s.updateAccountInfo(user.ID, models.User{LastLoginIP: ip, LastLogin: &now}) if err != nil { return "", err } @@ -373,7 +390,7 @@ func (s AccountServiceDefault) doLogin(user *models.User, ip string) (string, er return token, nil } -func (s AccountServiceDefault) updateAccountInfo(userId uint, info interface{}) error { +func (s AccountServiceDefault) updateAccountInfo(userId uint, info interface{}) *AccountError { var user models.User user.ID = userId @@ -381,13 +398,13 @@ func (s AccountServiceDefault) updateAccountInfo(userId uint, info interface{}) result := s.db.Model(&models.User{}).Where(&user).Updates(info) if result.Error != nil { - return result.Error + return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error) } return nil } -func (s AccountServiceDefault) exists(model interface{}, conditions map[string]interface{}) (bool, interface{}, error) { +func (s AccountServiceDefault) exists(model interface{}, conditions map[string]interface{}) (bool, interface{}, *AccountError) { // Conduct a query with the provided model and conditions result := s.db.Model(model).Where(conditions).First(model) @@ -398,7 +415,7 @@ func (s AccountServiceDefault) exists(model interface{}, conditions map[string]i return false, nil, nil } - return exists, model, result.Error + return exists, model, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error) } func (s AccountServiceDefault) validPassword(user *models.User, password string) bool { diff --git a/account/errors.go b/account/errors.go new file mode 100644 index 0000000..bce99bc --- /dev/null +++ b/account/errors.go @@ -0,0 +1,115 @@ +package account + +import "fmt" + +const ( + // Account creation errors + ErrKeyAccountCreationFailed = "ErrAccountCreationFailed" + ErrKeyEmailAlreadyExists = "ErrEmailAlreadyExists" + ErrKeyPasswordHashingFailed = "ErrPasswordHashingFailed" + + // Account lookup and existence verification errors + ErrKeyUserNotFound = "ErrUserNotFound" + ErrKeyPublicKeyNotFound = "ErrPublicKeyNotFound" + + // Authentication and login errors + ErrKeyInvalidLogin = "ErrInvalidLogin" + ErrKeyInvalidPassword = "ErrInvalidPassword" + ErrKeyInvalidOTPCode = "ErrInvalidOTPCode" + ErrKeyOTPVerificationFailed = "ErrOTPVerificationFailed" + ErrKeyLoginFailed = "ErrLoginFailed" + ErrKeyHashingFailed = "ErrHashingFailed" + + // Account update errors + ErrKeyAccountUpdateFailed = "ErrAccountUpdateFailed" + + // JWT generation errors + ErrKeyJWTGenerationFailed = "ErrJWTGenerationFailed" + + // OTP management errors + ErrKeyOTPGenerationFailed = "ErrOTPGenerationFailed" + ErrKeyOTPEnableFailed = "ErrOTPEnableFailed" + ErrKeyOTPDisableFailed = "ErrOTPDisableFailed" + + // Public key management errors + ErrKeyAddPublicKeyFailed = "ErrAddPublicKeyFailed" + ErrKeyPublicKeyExists = "ErrPublicKeyExists" + + // Pin management errors + ErrKeyPinAddFailed = "ErrPinAddFailed" + ErrKeyPinDeleteFailed = "ErrPinDeleteFailed" + ErrKeyPinsRetrievalFailed = "ErrPinsRetrievalFailed" + + // General errors + ErrKeyDatabaseOperationFailed = "ErrDatabaseOperationFailed" +) + +var defaultErrorMessages = map[string]string{ + // Account creation errors + ErrKeyAccountCreationFailed: "Account creation failed due to an internal error.", + ErrKeyEmailAlreadyExists: "The email address provided is already in use.", + ErrKeyPasswordHashingFailed: "Failed to secure the password, please try again later.", + + // Account lookup and existence verification errors + ErrKeyUserNotFound: "The requested user was not found.", + ErrKeyPublicKeyNotFound: "The specified public key was not found.", + ErrKeyHashingFailed: "Failed to hash the password.", + + // Authentication and login errors + ErrKeyInvalidLogin: "The login credentials provided are invalid.", + ErrKeyInvalidPassword: "The password provided is incorrect.", + ErrKeyInvalidOTPCode: "The OTP code provided is invalid or expired.", + ErrKeyOTPVerificationFailed: "OTP verification failed, please try again.", + ErrKeyLoginFailed: "Login failed due to an internal error.", + + // Account update errors + ErrKeyAccountUpdateFailed: "Failed to update account information.", + + // JWT generation errors + ErrKeyJWTGenerationFailed: "Failed to generate a new JWT token.", + + // OTP management errors + ErrKeyOTPGenerationFailed: "Failed to generate a new OTP secret.", + ErrKeyOTPEnableFailed: "Enabling OTP authentication failed.", + ErrKeyOTPDisableFailed: "Disabling OTP authentication failed.", + + // Public key management errors + ErrKeyAddPublicKeyFailed: "Adding the public key to the account failed.", + ErrKeyPublicKeyExists: "The public key already exists for this account.", + + // Pin management errors + ErrKeyPinAddFailed: "Failed to add the pin.", + ErrKeyPinDeleteFailed: "Failed to delete the pin.", + ErrKeyPinsRetrievalFailed: "Failed to retrieve pins.", + + // General errors + ErrKeyDatabaseOperationFailed: "A database operation failed.", +} + +type AccountError struct { + Key string // A unique identifier for the error type + Message string // Human-readable error message + Err error // Underlying error, if any +} + +func (e *AccountError) Error() string { + if e.Err != nil { + return fmt.Sprintf("%s: %v", e.Message, e.Err) + } + return e.Message +} + +func NewAccountError(key string, err error, customMessage ...string) *AccountError { + message, exists := defaultErrorMessages[key] + if !exists { + message = "An unknown error occurred" + } + if len(customMessage) > 0 { + message = customMessage[0] + } + return &AccountError{ + Key: key, + Message: message, + Err: err, + } +}