From afc62f311be8cd21d812e60e7ca501c87b5384a3 Mon Sep 17 00:00:00 2001 From: Asai Neko Date: Sat, 27 Dec 2025 03:45:31 +0800 Subject: [PATCH] Add event service, caddy test domain Signed-off-by: Asai Neko --- .env.development | 24 ++++++++++++- config.default.yaml | 3 +- config/types.go | 7 ++-- data/user.go | 65 ++++++++++++++++++++++++++++++++++ devenv.nix | 3 +- internal/cryptography/token.go | 11 ++++-- service/event/create.go | 1 + service/event/info.go | 1 - service/event/list.go | 1 + service/event/update.go | 1 + service/user/checkin.go | 39 ++++++++++++++++++-- service/user/handler.go | 1 + 12 files changed, 145 insertions(+), 12 deletions(-) diff --git a/.env.development b/.env.development index 5904d14..99b0787 100644 --- a/.env.development +++ b/.env.development @@ -1,9 +1,31 @@ +SERVER_APPLICATION=nixcn-cms SERVER_ADDRESS=:8000 +SERVER_EXTERNAL_URL=http://test.sne.moe SERVER_DEBUG_MODE=true SERVER_FILE_LOGGER=false -SERVER_JWT_SECRET=test + DATABASE_TYPE=postgres DATABASE_HOST=127.0.0.1 DATABASE_NAME=postgres DATABASE_USERNAME=postgres DATABASE_PASSWORD=postgres + +CACHE_HOSTS=["127.0.0.1:6379"] +CACHE_MASTER= +CACHE_USERNAME= +CACHE_PASSWORD= +CACHE_DB=0 + +SEARCH_HOST=127.0.0.1 +SEARCH_API_KEY= + +EMAIL_RESEND_API_KEY= +EMAIL_FROM= + +SECRETS_JWT_SECRET=something +SECRETS_TURNSTILE_SECRET=something + +TTL_MAGIC_LINK_TTL=10m +TTL_JWT_TTL=15s +TTL_REFRESH_TTL=7d +TTL_CHECKIN_COSE_TTL=10m diff --git a/config.default.yaml b/config.default.yaml index 19cf27e..ad19747 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -28,4 +28,5 @@ secrets: ttl: magic_link_ttl: 10m jwt_ttl: 15s - refresh_ttl: 48h + refresh_ttl: 7d + checkin_code_ttl: 5m diff --git a/config/types.go b/config/types.go index a4b30e5..9731473 100644 --- a/config/types.go +++ b/config/types.go @@ -50,7 +50,8 @@ type secrets struct { } type ttl struct { - MagicLinkTTL string `yaml:"magic_link_ttl"` - JwtTTL string `yaml:"jwt_ttl"` - RefreshTTL string `yaml:"refresh_ttl"` + MagicLinkTTL string `yaml:"magic_link_ttl"` + JwtTTL string `yaml:"jwt_ttl"` + RefreshTTL string `yaml:"refresh_ttl"` + CheckinCodeTTL string `yaml:"checkin_code_ttl"` } diff --git a/data/user.go b/data/user.go index 7bbfd55..8f5fcba 100644 --- a/data/user.go +++ b/data/user.go @@ -1,11 +1,17 @@ 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" @@ -157,3 +163,62 @@ func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) { } return &list, nil } + +func (self *User) GenCheckinCode(eventId uuid.UUID) (*string, error) { + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + randomNumber := rng.Intn(900000) + 100000 + randNumString := fmt.Sprintf("%06d", randomNumber) + ctx := context.Background() + ttl := viper.GetDuration("ttl.checkin_code_ttl") + Redis.Set( + ctx, + "checkin_code:"+randNumString, + self.UserId.String()+":"+eventId.String(), + ttl, + ) + return &randNumString, 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 +} diff --git a/devenv.nix b/devenv.nix index 29fe1d1..ff24a1a 100644 --- a/devenv.nix +++ b/devenv.nix @@ -26,7 +26,8 @@ enable = true; dataDir = "${config.env.DEVENV_STATE}/caddy"; config = '' - :8080 { + test.sne.moe { + tls internal handle /api/* { reverse_proxy 127.0.0.1:8000 } diff --git a/internal/cryptography/token.go b/internal/cryptography/token.go index 5986982..edc7acf 100644 --- a/internal/cryptography/token.go +++ b/internal/cryptography/token.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "encoding/base64" "errors" + "fmt" "nixcn-cms/data" "time" @@ -29,7 +30,7 @@ func (self *Token) NewClaims() JwtClaims { return JwtClaims{ UserID: self.UserID, RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("ttl.jwt_ttl"))), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("ttl.assess_ttl"))), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: self.Application, }, @@ -41,7 +42,11 @@ func (self *Token) GenerateAccessToken() (string, error) { claims := self.NewClaims() token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) secret := viper.GetString("secrets.jwt_secret") - return token.SignedString(secret) + signedToken, err := token.SignedString([]byte(secret)) + if err != nil { + return "", fmt.Errorf("error signing token: %v", err) + } + return signedToken, nil } // Generate refresh token @@ -50,7 +55,7 @@ func (self *Token) GenerateRefreshToken() (string, error) { if _, err := rand.Read(b); err != nil { return "", err } - return base64.RawURLEncoding.EncodeToString(b), nil + return base64.URLEncoding.EncodeToString(b), nil } // Issue both access and refresh token diff --git a/service/event/create.go b/service/event/create.go index e69de29..0e4b82e 100644 --- a/service/event/create.go +++ b/service/event/create.go @@ -0,0 +1 @@ +package event diff --git a/service/event/info.go b/service/event/info.go index 917d73d..723d14e 100644 --- a/service/event/info.go +++ b/service/event/info.go @@ -35,7 +35,6 @@ func Info(c *gin.Context) { } c.JSON(200, gin.H{ - "event_id": event.EventId, "name": event.Name, "start_time": event.StartTime, "end_time": event.EndTime, diff --git a/service/event/list.go b/service/event/list.go index e69de29..0e4b82e 100644 --- a/service/event/list.go +++ b/service/event/list.go @@ -0,0 +1 @@ +package event diff --git a/service/event/update.go b/service/event/update.go index e69de29..0e4b82e 100644 --- a/service/event/update.go +++ b/service/event/update.go @@ -0,0 +1 @@ +package event diff --git a/service/user/checkin.go b/service/user/checkin.go index eaa82b9..07f50fa 100644 --- a/service/user/checkin.go +++ b/service/user/checkin.go @@ -2,7 +2,6 @@ package user import ( "nixcn-cms/data" - "time" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -35,7 +34,43 @@ func Checkin(c *gin.Context) { }) return } - data.UpdateCheckin(userId.(uuid.UUID), eventId, time.Now()) + + data.UserId = userId.(uuid.UUID) + code, err := data.GenCheckinCode(eventId) + if err != nil { + c.JSON(500, gin.H{ + "status": "error generating code", + }) + return + } + + c.JSON(200, gin.H{ + "checkin_code": code, + }) +} + +func CheckinSubmit(c *gin.Context) { + var req struct { + ChekinCode string `json:"checkin_code"` + } + c.ShouldBindJSON(&req) + + data := new(data.User) + userId, err := data.VerifyCheckinCode(req.ChekinCode) + if err != nil { + c.JSON(400, gin.H{ + "status": "error verify checkin code", + }) + return + } + + data.GetByUserId(*userId) + if data.PermissionLevel <= 20 { + c.JSON(403, gin.H{ + "status": "access denied", + }) + } + c.JSON(200, gin.H{ "status": "success", }) diff --git a/service/user/handler.go b/service/user/handler.go index d439ca0..bb73682 100644 --- a/service/user/handler.go +++ b/service/user/handler.go @@ -10,6 +10,7 @@ func Handler(r *gin.RouterGroup) { r.Use(middleware.JWTAuth()) r.GET("/info", Info) r.POST("/checkin", Checkin) + r.POST("/checkin/submit", CheckinSubmit) r.PATCH("/update", Update) r.GET("/list", List) }