233
data/user.go
233
data/user.go
@@ -1,20 +1,10 @@
|
||||
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
|
||||
@@ -24,16 +14,15 @@ import (
|
||||
// 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"`
|
||||
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"`
|
||||
PermissionLevel uint `json:"permission_level" gorm:"default:10;not null"`
|
||||
}
|
||||
|
||||
type UserSearchDoc struct {
|
||||
@@ -46,88 +35,61 @@ type UserSearchDoc struct {
|
||||
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
|
||||
func (self *User) GetByEmail(email string) (*User, error) {
|
||||
var user User
|
||||
|
||||
err := Database.
|
||||
Where("email = ?", email).
|
||||
First(&user).Error
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (self *User) GetByUserId(userId uuid.UUID) error {
|
||||
if err := Database.Where("user_id = ?", userId).First(&self).Error; err != nil {
|
||||
return err
|
||||
func (self *User) GetByUserId(userId uuid.UUID) (*User, error) {
|
||||
var user User
|
||||
|
||||
err := Database.
|
||||
Where("user_id = ?", userId).
|
||||
First(&user).Error
|
||||
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, 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
|
||||
})
|
||||
return &user, err
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
self.UUID = uuid.New()
|
||||
self.UserId = uuid.New()
|
||||
|
||||
// DB transaction only
|
||||
if err := Database.Transaction(func(tx *gorm.DB) error {
|
||||
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
|
||||
})
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Search index (eventual consistency)
|
||||
if err := self.UpdateSearchIndex(); err != nil {
|
||||
// TODO: async retry / log
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *User) UpdateByUserID(userId uuid.UUID) error {
|
||||
@@ -150,6 +112,8 @@ func (self *User) GetFullTable() (*[]User, error) {
|
||||
|
||||
func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) {
|
||||
index := MeiliSearch.Index("user")
|
||||
|
||||
// Fast read from MeiliSearch, no DB involved
|
||||
result, err := index.Search("", &meilisearch.SearchRequest{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
@@ -157,75 +121,44 @@ func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) {
|
||||
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) UpdateSearchIndex() error {
|
||||
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")
|
||||
|
||||
primaryKey := "user_id"
|
||||
opts := &meilisearch.DocumentOptions{
|
||||
PrimaryKey: &primaryKey,
|
||||
}
|
||||
|
||||
if _, err := index.UpdateDocuments(
|
||||
[]UserSearchDoc{doc},
|
||||
opts,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return 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
|
||||
func (self *User) DeleteSearchIndex() error {
|
||||
index := MeiliSearch.Index("user")
|
||||
_, err := index.DeleteDocument(self.UserId.String(), nil)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user