Add event service, caddy test domain

Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
2025-12-27 03:45:31 +08:00
parent 2b99d415de
commit afc62f311b
12 changed files with 145 additions and 12 deletions

View File

@@ -1,9 +1,31 @@
SERVER_APPLICATION=nixcn-cms
SERVER_ADDRESS=:8000 SERVER_ADDRESS=:8000
SERVER_EXTERNAL_URL=http://test.sne.moe
SERVER_DEBUG_MODE=true SERVER_DEBUG_MODE=true
SERVER_FILE_LOGGER=false SERVER_FILE_LOGGER=false
SERVER_JWT_SECRET=test
DATABASE_TYPE=postgres DATABASE_TYPE=postgres
DATABASE_HOST=127.0.0.1 DATABASE_HOST=127.0.0.1
DATABASE_NAME=postgres DATABASE_NAME=postgres
DATABASE_USERNAME=postgres DATABASE_USERNAME=postgres
DATABASE_PASSWORD=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

View File

@@ -28,4 +28,5 @@ secrets:
ttl: ttl:
magic_link_ttl: 10m magic_link_ttl: 10m
jwt_ttl: 15s jwt_ttl: 15s
refresh_ttl: 48h refresh_ttl: 7d
checkin_code_ttl: 5m

View File

@@ -50,7 +50,8 @@ type secrets struct {
} }
type ttl struct { type ttl struct {
MagicLinkTTL string `yaml:"magic_link_ttl"` MagicLinkTTL string `yaml:"magic_link_ttl"`
JwtTTL string `yaml:"jwt_ttl"` JwtTTL string `yaml:"jwt_ttl"`
RefreshTTL string `yaml:"refresh_ttl"` RefreshTTL string `yaml:"refresh_ttl"`
CheckinCodeTTL string `yaml:"checkin_code_ttl"`
} }

View File

@@ -1,11 +1,17 @@
package data package data
import ( import (
"context"
"errors"
"fmt"
"math/rand"
"strings"
"time" "time"
"github.com/go-viper/mapstructure/v2" "github.com/go-viper/mapstructure/v2"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/meilisearch/meilisearch-go" "github.com/meilisearch/meilisearch-go"
"github.com/spf13/viper"
"gorm.io/datatypes" "gorm.io/datatypes"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
@@ -157,3 +163,62 @@ func (self *User) FastListUsers(limit, offset int64) (*[]UserSearchDoc, error) {
} }
return &list, nil 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
}

View File

@@ -26,7 +26,8 @@
enable = true; enable = true;
dataDir = "${config.env.DEVENV_STATE}/caddy"; dataDir = "${config.env.DEVENV_STATE}/caddy";
config = '' config = ''
:8080 { test.sne.moe {
tls internal
handle /api/* { handle /api/* {
reverse_proxy 127.0.0.1:8000 reverse_proxy 127.0.0.1:8000
} }

View File

@@ -5,6 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"nixcn-cms/data" "nixcn-cms/data"
"time" "time"
@@ -29,7 +30,7 @@ func (self *Token) NewClaims() JwtClaims {
return JwtClaims{ return JwtClaims{
UserID: self.UserID, UserID: self.UserID,
RegisteredClaims: jwt.RegisteredClaims{ 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()), IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: self.Application, Issuer: self.Application,
}, },
@@ -41,7 +42,11 @@ func (self *Token) GenerateAccessToken() (string, error) {
claims := self.NewClaims() claims := self.NewClaims()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
secret := viper.GetString("secrets.jwt_secret") 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 // Generate refresh token
@@ -50,7 +55,7 @@ func (self *Token) GenerateRefreshToken() (string, error) {
if _, err := rand.Read(b); err != nil { if _, err := rand.Read(b); err != nil {
return "", err return "", err
} }
return base64.RawURLEncoding.EncodeToString(b), nil return base64.URLEncoding.EncodeToString(b), nil
} }
// Issue both access and refresh token // Issue both access and refresh token

View File

@@ -0,0 +1 @@
package event

View File

@@ -35,7 +35,6 @@ func Info(c *gin.Context) {
} }
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"event_id": event.EventId,
"name": event.Name, "name": event.Name,
"start_time": event.StartTime, "start_time": event.StartTime,
"end_time": event.EndTime, "end_time": event.EndTime,

View File

@@ -0,0 +1 @@
package event

View File

@@ -0,0 +1 @@
package event

View File

@@ -2,7 +2,6 @@ package user
import ( import (
"nixcn-cms/data" "nixcn-cms/data"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
@@ -35,7 +34,43 @@ func Checkin(c *gin.Context) {
}) })
return 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{ c.JSON(200, gin.H{
"status": "success", "status": "success",
}) })

View File

@@ -10,6 +10,7 @@ func Handler(r *gin.RouterGroup) {
r.Use(middleware.JWTAuth()) r.Use(middleware.JWTAuth())
r.GET("/info", Info) r.GET("/info", Info)
r.POST("/checkin", Checkin) r.POST("/checkin", Checkin)
r.POST("/checkin/submit", CheckinSubmit)
r.PATCH("/update", Update) r.PATCH("/update", Update)
r.GET("/list", List) r.GET("/list", List)
} }