package models import ( "GoMembership/internal/config" "GoMembership/internal/constants" "GoMembership/internal/utils" "GoMembership/pkg/errors" "GoMembership/pkg/logger" "fmt" "slices" "strings" "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 { ID uint `gorm:"primaryKey" json:"id"` CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time DateOfBirth time.Time `gorm:"not null" json:"dateofbirth" binding:"required_unless=RoleID 0,safe_content"` Company string `json:"company" binding:"omitempty,omitnil,safe_content"` Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"` Notes string `json:"notes" binding:"safe_content"` FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"` Password string `json:"password" binding:"safe_content"` Email string `gorm:"uniqueIndex:idx_users_email,not null" json:"email" binding:"required,email,safe_content"` LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"` Address string `gorm:"not null" json:"address" binding:"required,safe_content"` ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"` City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"` Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"` BankAccount *BankAccount `gorm:"foreignkey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"bank_account"` Verifications []Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Membership *Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"membership"` Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"licence"` Status int8 `json:"status"` RoleID int8 `json:"role_id"` } func (u *User) AfterCreate(tx *gorm.DB) (err error) { if u.BankAccount != nil && u.BankAccount.MandateReference == "" { u.BankAccount.MandateReference = u.GenerateMandateReference() u.BankAccount.Update(tx) } return nil } func (u *User) BeforeSave(tx *gorm.DB) (err error) { u.Email = strings.ToLower(u.Email) return nil } func (u *User) GenerateMandateReference() string { return fmt.Sprintf("%s%d%s", time.Now().Format("20060102"), u.ID, u.BankAccount.IBAN) } func (u *User) SetPassword(plaintextPassword string) error { if plaintextPassword == "" { return nil } hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams) if err != nil { return err } u.Password = hash return nil } 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 { if err := tx.Preload(clause.Associations).Create(u).Error; err != nil { return err } return nil }) } // return db.Transaction(func(tx *gorm.DB) error { // // Initialize slices/pointers if nil // if u.Verifications == nil { // u.Verifications = &[]Verification{} // } // // Create base user first // if err := tx.Omit(clause.Associations).Create(u).Error; err != nil { // return fmt.Errorf("failed to create user: %w", err) // } // // Handle BankAccount // if u.BankAccount != (BankAccount{}) { // u.BankAccount.MandateReference = u.GenerateMandateReference() // if err := tx.Create(&u.BankAccount).Error; err != nil { // return fmt.Errorf("failed to create bank account: %w", err) // } // if err := tx.Model(u).Update("bank_account_id", u.BankAccount.ID).Error; err != nil { // return fmt.Errorf("failed to link bank account: %w", err) // } // } // // Handle Membership and SubscriptionModel // if u.Membership != (Membership{}) { // if err := tx.Create(&u.Membership).Error; err != nil { // return fmt.Errorf("failed to create membership: %w", err) // } // if err := tx.Model(u).Update("membership_id", u.Membership.ID).Error; err != nil { // return fmt.Errorf("failed to link membership: %w", err) // } // } // // Handle Licence and Categories // if u.Licence != nil { // u.Licence.UserID = u.ID // if err := tx.Create(u.Licence).Error; err != nil { // return fmt.Errorf("failed to create licence: %w", err) // } // if len(u.Licence.Categories) > 0 { // if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil { // return fmt.Errorf("failed to link categories: %w", err) // } // } // if err := tx.Model(u).Update("licence_id", u.Licence.ID).Error; err != nil { // return fmt.Errorf("failed to link licence: %w", err) // } // } // // Handle Consents // for i := range u.Consents { // u.Consents[i].UserID = u.ID // } // if len(u.Consents) > 0 { // if err := tx.Create(&u.Consents).Error; err != nil { // return fmt.Errorf("failed to create consents: %w", err) // } // } // // Handle Verifications // for i := range *u.Verifications { // (*u.Verifications)[i].UserID = u.ID // } // if len(*u.Verifications) > 0 { // if err := tx.Create(u.Verifications).Error; err != nil { // return fmt.Errorf("failed to create verifications: %w", err) // } // } // // Reload the complete user with all associations // return tx.Preload(clause.Associations). // Preload("Membership.SubscriptionModel"). // Preload("Licence.Categories"). // First(u, 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 // } // for i := range u.Consents { // u.Consents[i].UserID = u.ID // } // for i := range *u.Verifications { // (*u.Verifications)[i].UserID = u.ID // } // if err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(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(clause.Associations). // Preload("Membership.SubscriptionModel"). // 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. First(&existingUser, u.ID).Error; err != nil { return err } // Update the user's main fields result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Omit("Password", "Verifications", "Licence.Categories").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).Where("id = ?", existingUser.Membership.ID).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 } } if u.Licence != nil { if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil { return err } } return nil }) if err != nil { return err } return db. Preload(clause.Associations). Preload("Membership.SubscriptionModel"). Preload("Licence.Categories"). 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.SubscriptionModel"). Preload("Licence.Categories"). 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.SubscriptionModel"). Preload("Licence.Categories"). 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 = []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{} { var membership map[string]interface{} = nil var licence map[string]interface{} = nil var bankAccount map[string]interface{} = nil if u.Membership != nil { membership = map[string]interface{}{ "id": u.Membership.ID, "start_date": u.Membership.StartDate, "end_date": u.Membership.EndDate, "status": u.Membership.Status, "subscription_model": map[string]interface{}{ "id": u.Membership.SubscriptionModel.ID, "name": u.Membership.SubscriptionModel.Name, "details": u.Membership.SubscriptionModel.Details, "conditions": u.Membership.SubscriptionModel.Conditions, "monthly_fee": u.Membership.SubscriptionModel.MonthlyFee, "hourly_rate": u.Membership.SubscriptionModel.HourlyRate, "included_per_year": u.Membership.SubscriptionModel.IncludedPerYear, "included_per_month": u.Membership.SubscriptionModel.IncludedPerMonth, }, } } if u.Licence != nil { licence = map[string]interface{}{ "id": u.Licence.ID, "number": u.Licence.Number, "status": u.Licence.Status, "issued_date": u.Licence.IssuedDate, "expiration_date": u.Licence.ExpirationDate, "country": u.Licence.IssuingCountry, "categories": u.Licence.Categories, } } if u.BankAccount != nil { bankAccount = map[string]interface{}{ "id": u.BankAccount.ID, "mandate_date_signed": u.BankAccount.MandateDateSigned, "bank": u.BankAccount.Bank, "account_holder_name": u.BankAccount.AccountHolderName, "iban": u.BankAccount.IBAN, "bic": u.BankAccount.BIC, "mandate_reference": u.BankAccount.MandateReference, } } result := map[string]interface{}{ "email": u.Email, "first_name": u.FirstName, "last_name": u.LastName, "phone": u.Phone, "notes": u.Notes, "address": u.Address, "zip_code": u.ZipCode, "city": u.City, "status": u.Status, "id": u.ID, "role_id": u.RoleID, "company": u.Company, "dateofbirth": u.DateOfBirth, "membership": membership, "licence": licence, "bank_account": bankAccount, } 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) { logger.Error.Printf("where: %#v", where) var users []User result := db. Preload(clause.Associations). Preload("Membership.SubscriptionModel"). Preload("Licence.Categories"). Where(where).Find(&users) if result.Error != nil { if result.Error == gorm.ErrRecordNotFound { return nil, gorm.ErrRecordNotFound } return nil, result.Error } return &users, nil }