forked from nixcn/nixcn-cms
Add event service, caddy test domain
Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
65
data/user.go
65
data/user.go
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package event
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package event
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
package event
|
||||||
|
|||||||
@@ -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",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user