diff --git a/go-backend/internal/models/user.go b/go-backend/internal/models/user.go index b5919b0..c4130c1 100644 --- a/go-backend/internal/models/user.go +++ b/go-backend/internal/models/user.go @@ -1,11 +1,20 @@ package models import ( + "GoMembership/internal/config" + "GoMembership/internal/constants" + "GoMembership/internal/utils" + "GoMembership/pkg/errors" + "GoMembership/pkg/logger" "fmt" + "slices" "time" "github.com/alexedwards/argon2id" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type User struct { @@ -28,9 +37,8 @@ type User struct { Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"` BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"` BankAccountID uint - Verification Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` - VerificationID uint - Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"membership"` + Verifications *[]Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` + Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"membership"` MembershipID uint Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"licence"` LicenceID uint @@ -69,6 +77,268 @@ func (u *User) PasswordMatches(plaintextPassword string) (bool, error) { return argon2id.ComparePasswordAndHash(plaintextPassword, u.Password) } +func (u *User) PasswordExists() bool { + return u.Password != "" +} + +func (u *User) Delete(db *gorm.DB) error { + return db.Delete(&User{}, "id = ?", u.ID).Error +} + +func (u *User) Create(db *gorm.DB) error { + return db.Transaction(func(tx *gorm.DB) error { + // Create the base User record (omit associations to handle them separately) + if err := tx.Create(u).Error; err != nil { + return err + } + + // Replace associated Categories (assumes Categories already exist) + if u.Licence != nil && len(u.Licence.Categories) > 0 { + if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil { + return err + } + } + logger.Info.Printf("user created: %#v", u.Safe()) + // Preload all associations to return the fully populated User + return tx. + Preload("Membership"). + Preload("Membership.SubscriptionModel"). + Preload("Licence"). + Preload("Licence.Categories"). + First(u, u.ID).Error // Refresh the user object with all associations + }) +} + +func (u *User) Update(db *gorm.DB) error { + + err := db.Transaction(func(tx *gorm.DB) error { + // Check if the user exists in the database + var existingUser User + + logger.Info.Printf("updating user: %#v", u) + if err := tx. + Preload("Membership"). + Preload("Membership.SubscriptionModel"). + Preload("Licence"). + Preload("Licence.Categories"). + Preload("Verifications"). + First(&existingUser, u.ID).Error; err != nil { + return err + } + // Update the user's main fields + result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Omit("Password", "Membership", "Licence", "Verifications").Updates(u) + if result.Error != nil { + logger.Error.Printf("User update error in update user: %#v", result.Error) + return result.Error + } + if result.RowsAffected == 0 { + return errors.ErrNoRowsAffected + } + + if u.Password != "" { + if err := tx.Model(&existingUser). + Update("Password", u.Password).Error; err != nil { + logger.Error.Printf("Password update error in update user: %#v", err) + return err + } + } + + // Update the Membership if provided + if u.Membership.ID != 0 { + if err := tx.Model(&existingUser.Membership).Updates(u.Membership).Error; err != nil { + logger.Error.Printf("Membership update error in update user: %#v", err) + return err + } + } + if u.Licence != nil { + u.Licence.UserID = existingUser.ID + + if err := tx.Save(u.Licence).Error; err != nil { + return err + } + + if err := tx.Model(&existingUser).Update("LicenceID", u.Licence.ID).Error; err != nil { + return err + } + + if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil { + return err + } + } + // if u.Licence != nil { + // if existingUser.Licence == nil || existingUser.LicenceID == 0 { + // u.Licence.UserID = existingUser.ID // Ensure Licence belongs to User + // if err := tx.Create(u.Licence).Error; err != nil { + // return err + // } + // existingUser.Licence = u.Licence + // existingUser.LicenceID = u.Licence.ID + // if err := tx.Model(&existingUser).Update("LicenceID", u.Licence.ID).Error; err != nil { + // return err + // } + // } + // if err := tx.Model(existingUser.Licence).Updates(u.Licence).Error; err != nil { + // return err + // } + // // Update Categories association + // if err := tx.Model(existingUser.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil { + // return err + // } + // } + + if u.Verifications != nil { + if err := tx.Save(*u.Verifications).Error; err != nil { + return err + } + } + return nil + }) + + if err != nil { + return err + } + + return db. + Preload("Membership"). + Preload("Membership.SubscriptionModel"). + Preload("Licence"). + Preload("Licence.Categories"). + Preload("Verifications"). + First(&u, u.ID).Error +} + +func (u *User) FromID(db *gorm.DB, userID *uint) error { + var user User + result := db. + Preload(clause.Associations). + Preload("Membership"). + Preload("Membership.SubscriptionModel"). + Preload("Licence"). + Preload("Licence.Categories"). + Preload("Verifications"). + First(&user, userID) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return gorm.ErrRecordNotFound + } + return result.Error + } + *u = user + return nil +} + +func (u *User) FromEmail(db *gorm.DB, email *string) error { + var user User + result := db. + Preload(clause.Associations). + Preload("Membership"). + Preload("Membership.SubscriptionModel"). + Preload("Licence"). + Preload("Licence.Categories"). + Preload("Verifications"). + Where("email = ?", email).First(&user) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return gorm.ErrRecordNotFound + } + return result.Error + } + *u = user + return nil +} + +func (u *User) FromContext(db *gorm.DB, c *gin.Context) error { + tokenString, err := c.Cookie("jwt") + if err != nil { + return err + } + jwtUserID, err := extractUserIDFrom(tokenString) + if err != nil { + return err + } + if err = u.FromID(db, &jwtUserID); err != nil { + return err + } + return nil +} + +func (u *User) IsVerified() bool { + return u.Status > constants.DisabledStatus +} + +func (u *User) HasPrivilege(privilege int8) bool { + return u.RoleID >= privilege +} + +func (u *User) IsAdmin() bool { + return u.RoleID == constants.Roles.Admin +} + +func (u *User) IsMember() bool { + return u.RoleID == constants.Roles.Member +} + +func (u *User) IsSupporter() bool { + return u.RoleID == constants.Roles.Supporter +} + +func (u *User) SetVerification(verificationType string) (*Verification, error) { + if u.Verifications == nil { + u.Verifications = new([]Verification) + } + token, err := utils.GenerateVerificationToken() + if err != nil { + return nil, err + } + v := Verification{ + UserID: u.ID, + VerificationToken: token, + Type: verificationType, + } + if vi := slices.IndexFunc(*u.Verifications, func(vsl Verification) bool { return vsl.Type == v.Type }); vi > -1 { + (*u.Verifications)[vi] = v + } else { + *u.Verifications = append(*u.Verifications, v) + } + return &v, nil +} + +func (u *User) GetVerification(verificationType string) (*Verification, error) { + if u.Verifications == nil { + return nil, errors.ErrNoData + } + vi := slices.IndexFunc(*u.Verifications, func(vsl Verification) bool { return vsl.Type == verificationType }) + if vi == -1 { + return nil, errors.ErrNotFound + } + return &(*u.Verifications)[vi], nil +} + +func (u *User) Verify(token string, verificationType string) bool { + if token == "" || verificationType == "" { + logger.Error.Printf("token or verification type are empty in user.Verify") + return false + } + + vi := slices.IndexFunc(*u.Verifications, func(vsl Verification) bool { + return vsl.Type == verificationType && vsl.VerificationToken == token + }) + + if vi == -1 { + logger.Error.Printf("Couldn't find verification in users verifications") + return false + } + + if (*u.Verifications)[vi].VerifiedAt != nil { + logger.Error.Printf("VerifiedAt is not nil, already verified?: %#v", (*u.Verifications)[vi]) + return false + } + + t := time.Now() + (*u.Verifications)[vi].VerifiedAt = &t + return true +} + func (u *User) Safe() map[string]interface{} { result := map[string]interface{}{ "email": u.Email, @@ -128,3 +398,61 @@ func (u *User) Safe() map[string]interface{} { return result } + +func extractUserIDFrom(tokenString string) (uint, error) { + jwtSigningMethod := jwt.SigningMethodHS256 + jwtParser := jwt.NewParser(jwt.WithValidMethods([]string{jwtSigningMethod.Alg()})) + token, err := jwtParser.Parse(tokenString, func(_ *jwt.Token) (interface{}, error) { + return []byte(config.Auth.JWTSecret), nil + }) + + // Handle parsing errors (excluding expiration error) + if err != nil && !errors.Is(err, jwt.ErrTokenExpired) || token == nil { + logger.Error.Printf("Error parsing token: %v", err) + return 0, err + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + logger.Error.Print("Invalid token claims structure") + return 0, fmt.Errorf("invalid token claims format") + } + + // Validate required session_id claim + if _, exists := claims["session_id"]; !exists { + logger.Error.Print("Missing session_id in token claims") + return 0, fmt.Errorf("missing session_id claim") + } + + // Return token, claims, and original error (might be expiration) + + if _, exists := claims["session_id"]; !exists { + logger.Error.Print("Missing session_id in token claims") + return 0, fmt.Errorf("missing session_id claim") + } + id, ok := claims["user_id"] + if !ok { + return 0, fmt.Errorf("missing user_id claim") + } + + return uint(id.(float64)), nil +} + +func GetUsersWhere(db *gorm.DB, where map[string]interface{}) (*[]User, error) { + var users []User + result := db. + Preload(clause.Associations). + Preload("Membership"). + Preload("Membership.SubscriptionModel"). + Preload("Licence"). + Preload("Licence.Categories"). + Preload("Verifications"). + Where(where).Find(&users) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return nil, gorm.ErrRecordNotFound + } + return nil, result.Error + } + return &users, nil +}