diff --git a/config.default.yaml b/config.default.yaml index 962257f..19cf27e 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -16,6 +16,9 @@ cache: username: "" password: "" db: 0 +search: + host: 127.0.0.1 + api_key: "" email: resend_api_key: abc from: diff --git a/config/types.go b/config/types.go index 4cec893..a4b30e5 100644 --- a/config/types.go +++ b/config/types.go @@ -4,6 +4,7 @@ type config struct { Server server `yaml:"server"` Database database `yaml:"database"` Cache cache `yaml:"cache"` + Search search `yaml:"search"` Email email `yaml:"email"` Secrets secrets `yaml:"secrets"` TTL ttl `yaml:"ttl"` @@ -33,6 +34,11 @@ type cache struct { DB int `yaml:"db"` } +type search struct { + Host string `yaml:"host"` + ApiKey string `yaml:"api_key"` +} + type email struct { ResendApiKey string `yaml:"resend_api_key"` From string `yaml:"from"` diff --git a/data/data.go b/data/data.go index 6a1089b..58037bf 100644 --- a/data/data.go +++ b/data/data.go @@ -3,13 +3,16 @@ package data import ( "nixcn-cms/data/drivers" + "github.com/meilisearch/meilisearch-go" "github.com/redis/go-redis/v9" log "github.com/sirupsen/logrus" "github.com/spf13/viper" + "gorm.io/gorm" ) -var Database *drivers.DBClient +var Database *gorm.DB var Redis redis.UniversalClient +var MeiliSearch meilisearch.ServiceManager func Init() { // Init database @@ -32,7 +35,7 @@ func Init() { } // Auto migrate - err = db.DB.AutoMigrate(&User{}, &Event{}) + err = db.AutoMigrate(&User{}, &Event{}) if err != nil { log.Error("[Database] Error migrating database: ", err) } @@ -40,16 +43,24 @@ func Init() { // Init redis conection rdbAddress := viper.GetStringSlice("cache.hosts") - dsn := drivers.RedisDSN{ + rDSN := drivers.RedisDSN{ Hosts: rdbAddress, Master: viper.GetString("cache.master"), Username: viper.GetString("cache.username"), Password: viper.GetString("cache.password"), DB: viper.GetInt("cache.db"), } - rdb, err := drivers.Redis(dsn) + rdb, err := drivers.Redis(rDSN) if err != nil { log.Fatal("[Redis] Error connecting to Redis: ", err) } Redis = rdb + + // Init meilisearch + mDSN := drivers.MeiliDSN{ + Host: viper.GetString("search.host"), + ApiKey: viper.GetString("search.api_key"), + } + mdb := drivers.MeiliSearch(mDSN) + MeiliSearch = mdb } diff --git a/data/drivers/meilisearch.go b/data/drivers/meilisearch.go new file mode 100644 index 0000000..0bd8bb6 --- /dev/null +++ b/data/drivers/meilisearch.go @@ -0,0 +1,13 @@ +package drivers + +import "github.com/meilisearch/meilisearch-go" + +func MeiliSearch(dsn MeiliDSN) meilisearch.ServiceManager { + return meilisearch.New(dsn.Host, + meilisearch.WithAPIKey(dsn.ApiKey), + meilisearch.WithContentEncoding( + meilisearch.GzipEncoding, + meilisearch.BestCompression, + ), + ) +} diff --git a/data/drivers/postgres.go b/data/drivers/postgres.go index be96afb..01c349a 100644 --- a/data/drivers/postgres.go +++ b/data/drivers/postgres.go @@ -16,9 +16,9 @@ func SplitHostPort(url string) (host, port string) { return split[0], split[1] } -func Postgres(dsn ExternalDSN) (*DBClient, error) { +func Postgres(dsn ExternalDSN) (*gorm.DB, error) { host, port := SplitHostPort(dsn.Host) conn := "host=" + host + " user=" + dsn.Username + " password=" + dsn.Password + " dbname=" + dsn.Name + " port=" + port + " sslmode=disable TimeZone=" + config.TZ() db, err := gorm.Open(postgres.Open(conn), &gorm.Config{}) - return &DBClient{db}, err + return db, err } diff --git a/data/drivers/types.go b/data/drivers/types.go index f662b05..4c1687a 100644 --- a/data/drivers/types.go +++ b/data/drivers/types.go @@ -1,9 +1,5 @@ package drivers -import ( - "gorm.io/gorm" -) - type ExternalDSN struct { Host string Name string @@ -19,6 +15,7 @@ type RedisDSN struct { DB int } -type DBClient struct { - *gorm.DB +type MeiliDSN struct { + Host string + ApiKey string } diff --git a/data/event.go b/data/event.go index 35f4308..2dd1de5 100644 --- a/data/event.go +++ b/data/event.go @@ -5,7 +5,9 @@ import ( "slices" "time" + "github.com/go-viper/mapstructure/v2" "github.com/google/uuid" + "github.com/meilisearch/meilisearch-go" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -21,6 +23,13 @@ type Event struct { JoinedUsers datatypes.JSONSlice[uuid.UUID] `json:"joined_users"` } +type EventSearchDoc struct { + EventId string `json:"event_id"` + Name string `json:"name"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` +} + func (self *Event) GetEventById(eventId uuid.UUID) error { return Database.Transaction(func(tx *gorm.DB) error { if err := tx.Where("event_id = ?", eventId).First(&self).Error; err != nil { @@ -35,6 +44,21 @@ func (self *Event) UpdateEventById(eventId uuid.UUID) error { if err := tx.Model(&Event{}).Where("event_id = ?", eventId).Updates(&self).Error; err != nil { return err } + + // Update event to document index + doc := EventSearchDoc{ + EventId: self.EventId.String(), + Name: self.Name, + StartTime: self.StartTime, + EndTime: self.EndTime, + } + index := MeiliSearch.Index("event") + docPrimaryKey := "event_id" + meiliOptions := &meilisearch.DocumentOptions{PrimaryKey: &docPrimaryKey} + if _, err := index.UpdateDocuments([]EventSearchDoc{doc}, meiliOptions); err != nil { + return err + } + return nil }) } @@ -51,6 +75,21 @@ func (self *Event) CreateEvent() error { if err := tx.Create(&self).Error; err != nil { return err } + + // Add event to document index + doc := EventSearchDoc{ + EventId: self.EventId.String(), + Name: self.Name, + StartTime: self.StartTime, + EndTime: self.EndTime, + } + index := MeiliSearch.Index("event") + docPrimaryKey := "event_id" + meiliOptions := &meilisearch.DocumentOptions{PrimaryKey: &docPrimaryKey} + if _, err := index.AddDocuments([]EventSearchDoc{doc}, meiliOptions); err != nil { + return err + } + return nil }) } @@ -79,3 +118,19 @@ func (self *Event) UserJoinEvent(userId, eventId uuid.UUID) error { return nil }) } + +func (self *Event) FastListEvents(limit, offset int64) ([]EventSearchDoc, error) { + index := MeiliSearch.Index("event") + result, err := index.Search("", &meilisearch.SearchRequest{ + Limit: limit, + Offset: offset, + }) + if err != nil { + return nil, err + } + var list []EventSearchDoc + if err := mapstructure.Decode(result.Hits, &list); err != nil { + return nil, err + } + return list, nil +} diff --git a/data/user.go b/data/user.go index c692500..8630d90 100644 --- a/data/user.go +++ b/data/user.go @@ -3,7 +3,9 @@ package data import ( "time" + "github.com/go-viper/mapstructure/v2" "github.com/google/uuid" + "github.com/meilisearch/meilisearch-go" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -28,6 +30,16 @@ type User struct { 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 @@ -57,6 +69,23 @@ func (self *User) UpdateCheckin(userId, eventId uuid.UUID, time time.Time) error 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 }) } @@ -74,6 +103,23 @@ func (self *User) Create() error { 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 }) } @@ -86,3 +132,19 @@ func (self *User) UpdateByUserID(userId uuid.UUID) error { return 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 +} diff --git a/go.mod b/go.mod index 7191380..cff107a 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.5 require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect @@ -21,6 +22,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -34,6 +36,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/meilisearch/meilisearch-go v0.35.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index fbfe72b..46e26e1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= @@ -37,6 +39,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -64,6 +68,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/meilisearch/meilisearch-go v0.35.0 h1:Gh4vO+PinVQZ58iiFdUX9Hld8uXKzKh+C7mSSsCDlI8= +github.com/meilisearch/meilisearch-go v0.35.0/go.mod h1:cUVJZ2zMqTvvwIMEEAdsWH+zrHsrLpAw6gm8Lt1MXK0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -108,6 +114,7 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/server/router.go b/server/router.go index 90aa3ea..ef06164 100644 --- a/server/router.go +++ b/server/router.go @@ -2,6 +2,7 @@ package server import ( "nixcn-cms/service/auth" + "nixcn-cms/service/event" "nixcn-cms/service/user" "github.com/gin-gonic/gin" @@ -12,4 +13,5 @@ func Router(e *gin.Engine) { api := e.Group("/api/v1") auth.Handler(api.Group("/auth")) user.Handler(api.Group("/user")) + event.Handler(api.Group("/event")) } diff --git a/service/auth/magic.go b/service/auth/magic.go index 30d01b2..fd9845b 100644 --- a/service/auth/magic.go +++ b/service/auth/magic.go @@ -107,6 +107,7 @@ func VerifyMagicLink(c *gin.Context) { c.JSON(500, gin.H{ "status": "error generating tokens", }) + return } c.JSON(200, gin.H{ diff --git a/service/event/create.go b/service/event/create.go new file mode 100644 index 0000000..e69de29 diff --git a/service/event/handler.go b/service/event/handler.go new file mode 100644 index 0000000..3496e9d --- /dev/null +++ b/service/event/handler.go @@ -0,0 +1,7 @@ +package event + +import "github.com/gin-gonic/gin" + +func Handler(r *gin.RouterGroup) { + r.GET("/info", Info) +} diff --git a/service/event/info.go b/service/event/info.go new file mode 100644 index 0000000..917d73d --- /dev/null +++ b/service/event/info.go @@ -0,0 +1,44 @@ +package event + +import ( + "nixcn-cms/data" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func Info(c *gin.Context) { + event := new(data.Event) + eventIdOrig, ok := c.GetQuery("event_id") + if !ok { + c.JSON(400, gin.H{ + "status": "undefinded event id", + }) + return + } + + // Parse event id + eventId, err := uuid.Parse(eventIdOrig) + if err != nil { + c.JSON(500, gin.H{ + "status": "error parsing string to uuid", + }) + return + } + + err = event.GetEventById(eventId) + if err != nil { + c.JSON(404, gin.H{ + "status": "event id not found", + }) + return + } + + c.JSON(200, gin.H{ + "event_id": event.EventId, + "name": event.Name, + "start_time": event.StartTime, + "end_time": event.EndTime, + "joined_users": event.JoinedUsers, + }) +} diff --git a/service/event/list.go b/service/event/list.go new file mode 100644 index 0000000..e69de29 diff --git a/service/event/update.go b/service/event/update.go new file mode 100644 index 0000000..e69de29 diff --git a/service/user/checkin.go b/service/user/checkin.go index 54ab139..eaa82b9 100644 --- a/service/user/checkin.go +++ b/service/user/checkin.go @@ -21,7 +21,7 @@ func Checkin(c *gin.Context) { // Get event id from query eventIdOrig, ok := c.GetQuery("event_id") if !ok { - c.JSON(403, gin.H{ + c.JSON(400, gin.H{ "status": "undefinded event id", }) return @@ -33,6 +33,7 @@ func Checkin(c *gin.Context) { c.JSON(500, gin.H{ "status": "error parsing string to uuid", }) + return } data.UpdateCheckin(userId.(uuid.UUID), eventId, time.Now()) c.JSON(200, gin.H{ diff --git a/service/user/info.go b/service/user/info.go index 5252182..57a607d 100644 --- a/service/user/info.go +++ b/service/user/info.go @@ -12,14 +12,16 @@ func Info(c *gin.Context) { data := new(data.User) userId, ok := c.Get("user_id") if !ok { - c.JSON(403, gin.H{ + c.JSON(404, gin.H{ "status": "user not found", }) return } + + // Get user from database err := data.GetByUserId(userId.(uuid.UUID)) if err != nil { - c.JSON(403, gin.H{ + c.JSON(404, gin.H{ "status": "user not found", }) return