package data import ( "context" "errors" "fmt" "math/rand" "strings" "time" "github.com/go-viper/mapstructure/v2" "github.com/google/uuid" "github.com/meilisearch/meilisearch-go" "github.com/spf13/viper" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) // Permission Level // Banned User: 0 // Normal User: 10 // Admin User: 20 // Super User: 30 type User struct { Id uint `json:"id" gorm:"primarykey;autoincrement"` UUID uuid.UUID `json:"uuid" gorm:"type:uuid;uniqueindex;not null"` UserId uuid.UUID `json:"user_id" gorm:"type:uuid;uniqueindex;not null"` Email string `json:"email" gorm:"type:varchar(255);uniqueindex;not null"` Type string `json:"type" gorm:"type:varchar(32);index;not null"` Nickname string `json:"nickname"` Subtitle string `json:"subtitle"` Avatar string `json:"avatar"` Checkin datatypes.JSONMap `json:"checkin"` PermissionLevel uint `json:"permission_level" gorm:"default:10;not null"` } type UserSearchDoc struct { UserId string `json:"user_id"` Email string `json:"email"` Type string `json:"type"` Nickname string `json:"nickname"` Subtitle string `json:"subtitle"` Avatar string `json:"avatar"` PermissionLevel uint `json:"permission_level"` } func (self *User) GetByEmail(email string) error { if err := Database.Where("email = ?", email).First(&self).Error; err != nil { return err } return nil } func (self *User) GetByUserId(userId uuid.UUID) error { if err := Database.Where("user_id = ?", userId).First(&self).Error; err != nil { return err } return nil } func (self *User) UpdateCheckin(userId, eventId uuid.UUID, time time.Time) error { return Database.Transaction(func(tx *gorm.DB) error { if err := tx. Clauses(clause.Locking{Strength: "UPDATE"}). Where("user_id = ?", userId). First(self).Error; err != nil { return err // if error then rollback } self.Checkin = datatypes.JSONMap{eventId.String(): time} if err := tx.Save(self).Error; err != nil { return err // rollback } // Update user to document index doc := UserSearchDoc{ UserId: self.UserId.String(), Email: self.Email, Type: self.Type, Nickname: self.Nickname, Subtitle: self.Subtitle, Avatar: self.Avatar, PermissionLevel: self.PermissionLevel, } index := MeiliSearch.Index("user") docPrimaryKey := "user_id" meiliOptions := &meilisearch.DocumentOptions{PrimaryKey: &docPrimaryKey} if _, err := index.UpdateDocuments([]UserSearchDoc{doc}, meiliOptions); err != nil { return err } return nil // commit }) } func (self *User) Create() error { return Database.Transaction(func(tx *gorm.DB) error { if self.UUID == uuid.Nil { self.UUID = uuid.New() } if self.UserId == uuid.Nil { self.UserId = uuid.New() } if err := tx.Create(self).Error; err != nil { return err } // Create user to document index doc := UserSearchDoc{ UserId: self.UserId.String(), Email: self.Email, Type: self.Type, Nickname: self.Nickname, Subtitle: self.Subtitle, Avatar: self.Avatar, PermissionLevel: self.PermissionLevel, } index := MeiliSearch.Index("user") docPrimaryKey := "user_id" meiliOptions := &meilisearch.DocumentOptions{PrimaryKey: &docPrimaryKey} if _, err := index.AddDocuments([]UserSearchDoc{doc}, meiliOptions); err != nil { return err } return nil }) } func (self *User) UpdateByUserID(userId uuid.UUID) error { return Database.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&User{}).Where("user_id = ?", userId).Updates(&self).Error; err != nil { return err } return nil }) } func (self *User) GetFullTable() (*[]User, error) { var users []User err := Database.Find(&users).Error if err != nil { return nil, err } return &users, nil } func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) { index := MeiliSearch.Index("user") result, err := index.Search("", &meilisearch.SearchRequest{ Limit: limit, Offset: offset, }) if err != nil { return nil, err } var list []UserSearchDoc if err := mapstructure.Decode(result.Hits, &list); err != nil { return nil, err } return &list, nil } func (self *User) GenCheckinCode(eventId uuid.UUID) (*string, error) { ctx := context.Background() ttl := viper.GetDuration("ttl.checkin_code_ttl") rng := rand.New(rand.NewSource(time.Now().UnixNano())) for { code := fmt.Sprintf("%06d", rng.Intn(900000)+100000) ok, err := Redis.SetNX( ctx, "checkin_code:"+code, "user_id:"+self.UserId.String()+":event_id:"+eventId.String(), ttl, ).Result() if err != nil { return nil, err } if ok { return &code, nil } } } func (self *User) VerifyCheckinCode(checkinCode string) (*uuid.UUID, error) { ctx := context.Background() result := Redis.Get(ctx, "checkin_code:"+checkinCode).String() if result == "" { return nil, errors.New("invalid or expired checkin code") } split := strings.Split(result, ":") if len(split) < 2 { return nil, errors.New("invalid checkin code format") } userId := split[0] eventId := split[1] var returnedUserId uuid.UUID err := Database.Transaction(func(tx *gorm.DB) error { checkinData := map[string]interface{}{ eventId: time.Now(), } if err := tx.Model(&User{}).Where("user_id = ?", userId).Updates(map[string]interface{}{ "checkin": checkinData, }).Error; err != nil { return err } parsedUserId, err := uuid.Parse(userId) if err != nil { return err } returnedUserId = parsedUserId return nil }) if err != nil { return nil, err } return &returnedUserId, nil }