Files
GoMembership/go-backend/internal/models/user.go
2025-03-11 20:44:29 +01:00

459 lines
14 KiB
Go

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 {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
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:"unique;not null" json:"email" binding:"required,email,safe_content"`
LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"`
ProfilePicture string `json:"profile_picture" binding:"omitempty,omitnil,image,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:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"`
BankAccountID uint
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
PaymentStatus int8 `json:"payment_status"`
Status int8 `json:"status"`
RoleID int8 `json:"role_id"`
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.BankAccount.ID != 0 && u.BankAccount.MandateReference == "" {
mandateReference := u.GenerateMandateReference()
return tx.Model(&u.BankAccount).Update("MandateReference", mandateReference).Error
}
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 {
// 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,
"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": 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,
},
},
"licence": map[string]interface{}{
"id": 0,
},
"bank_account": 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,
},
}
if u.Licence != nil {
result["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,
}
}
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
}