557 lines
17 KiB
Go
557 lines
17 KiB
Go
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
|
|
}
|