moved repo to user model
This commit is contained in:
@@ -1,11 +1,20 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"GoMembership/internal/config"
|
||||||
|
"GoMembership/internal/constants"
|
||||||
|
"GoMembership/internal/utils"
|
||||||
|
"GoMembership/pkg/errors"
|
||||||
|
"GoMembership/pkg/logger"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alexedwards/argon2id"
|
"github.com/alexedwards/argon2id"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -28,9 +37,8 @@ type User struct {
|
|||||||
Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"`
|
Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"`
|
||||||
BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"`
|
BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"`
|
||||||
BankAccountID uint
|
BankAccountID uint
|
||||||
Verification Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
Verifications *[]Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
VerificationID uint
|
Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"membership"`
|
||||||
Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"membership"`
|
|
||||||
MembershipID uint
|
MembershipID uint
|
||||||
Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"licence"`
|
Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"licence"`
|
||||||
LicenceID uint
|
LicenceID uint
|
||||||
@@ -69,6 +77,268 @@ func (u *User) PasswordMatches(plaintextPassword string) (bool, error) {
|
|||||||
return argon2id.ComparePasswordAndHash(plaintextPassword, u.Password)
|
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{} {
|
func (u *User) Safe() map[string]interface{} {
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
"email": u.Email,
|
"email": u.Email,
|
||||||
@@ -128,3 +398,61 @@ func (u *User) Safe() map[string]interface{} {
|
|||||||
|
|
||||||
return result
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user