Files
sundynix-pets/pets-be/internal/httpapi/server.go
T
Blizzard 6c37785ce1
ci-cd / verify (push) Successful in 1m40s
ci-cd / deploy (push) Skipped
Complete admin dashboard with paginated user pet trees and walk relations
2026-10-04 11:45:15 +08:00

168 lines
5.1 KiB
Go

package httpapi
import (
"errors"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/sundynix/pets-be/internal/config"
"github.com/sundynix/pets-be/internal/environment"
"github.com/sundynix/pets-be/internal/model"
"github.com/sundynix/pets-be/internal/storage"
)
type API struct {
env *environment.Client
db *gorm.DB
storage *storage.Storage
cfg *config.Config
}
func New(db *gorm.DB, storage *storage.Storage, cfg *config.Config) *API {
return &API{db: db, storage: storage, cfg: cfg, env: environment.New(cfg.AMap.Key)}
}
func (a *API) Register(r *gin.Engine) {
r.Use(gin.Logger(), gin.Recovery())
_ = r.SetTrustedProxies(nil)
api := r.Group("/api")
api.GET("/ping", func(c *gin.Context) { ok(c, gin.H{"pong": true}) })
api.GET("/share/walk/:token", a.publicWalkLocation)
api.POST("/auth/login", a.login)
api.POST("/auth/dev", a.devLogin)
api.POST("/admin/login", a.adminLogin)
user := api.Group("")
user.Use(a.requireRole("user"))
user.GET("/environment", a.environment)
user.GET("/me", a.me)
user.GET("/pets", a.listPets)
user.POST("/pets", a.createPet)
user.GET("/pets/:id", a.getPet)
user.PUT("/pets/:id", a.updatePet)
user.DELETE("/pets/:id", a.archivePet)
user.POST("/files", a.uploadFile)
user.GET("/files/:id", a.getFile)
user.GET("/tasks", a.listTasks)
user.POST("/tasks", a.createTask)
user.PUT("/tasks/:id", a.updateTask)
user.DELETE("/tasks/:id", a.deleteTask)
user.PUT("/tasks/:id/completion", a.setTaskCompletion)
user.GET("/walks", a.listWalks)
user.POST("/walks", a.startWalk)
user.GET("/walks/:id", a.getWalk)
user.POST("/walks/:id/points", a.addWalkPoint)
user.POST("/walks/:id/pause", a.pauseWalk)
user.POST("/walks/:id/resume", a.resumeWalk)
user.POST("/walks/:id/finish", a.finishWalk)
user.PUT("/walks/:id/share", a.setWalkShare)
user.GET("/meetups", a.listMeetups)
user.POST("/meetups", a.createMeetup)
user.GET("/meetups/:id", a.getMeetup)
user.POST("/meetups/:id/join", a.joinMeetup)
user.DELETE("/meetups/:id/join", a.leaveMeetup)
user.POST("/meetups/:id/cancel", a.cancelMeetup)
admin := api.Group("/admin")
admin.Use(a.requireRole("admin"))
admin.GET("/stats", a.adminStats)
admin.GET("/users", a.adminUsers)
admin.GET("/users/:id/pets", a.adminUserPets)
admin.GET("/pets", a.adminPets)
admin.GET("/walks", a.adminWalks)
admin.GET("/walks/:id", a.adminWalkDetail)
admin.GET("/meetups", a.adminMeetups)
admin.GET("/tasks", a.adminTasks)
admin.GET("/files", a.adminFiles)
admin.PUT("/users/:id/disabled", a.adminSetUserDisabled)
admin.PUT("/meetups/:id/status", a.adminSetMeetupStatus)
}
type claims struct {
Role string `json:"role"`
jwt.RegisteredClaims
}
func (a *API) token(id, role string) (string, error) {
expire := a.cfg.JWT.ExpireHours
if expire <= 0 {
expire = 168
}
now := time.Now()
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims{Role: role, RegisteredClaims: jwt.RegisteredClaims{
Subject: id, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(expire) * time.Hour)),
}})
return t.SignedString([]byte(a.cfg.JWT.Secret))
}
func (a *API) requireRole(role string) gin.HandlerFunc {
return func(c *gin.Context) {
header := c.GetHeader("Authorization")
if !strings.HasPrefix(header, "Bearer ") {
fail(c, http.StatusUnauthorized, "请先登录")
c.Abort()
return
}
t, err := jwt.ParseWithClaims(strings.TrimPrefix(header, "Bearer "), &claims{}, func(t *jwt.Token) (any, error) {
if t.Method != jwt.SigningMethodHS256 {
return nil, errors.New("unexpected signing method")
}
return []byte(a.cfg.JWT.Secret), nil
})
if err != nil || !t.Valid {
fail(c, http.StatusUnauthorized, "登录已过期")
c.Abort()
return
}
cl, valid := t.Claims.(*claims)
if !valid || cl.Role != role || uuid.Validate(cl.Subject) != nil {
fail(c, http.StatusForbidden, "没有访问权限")
c.Abort()
return
}
if role == "user" {
var user model.User
if err := a.db.Select("id", "disabled").First(&user, "id = ?", cl.Subject).Error; err != nil || user.Disabled {
fail(c, http.StatusForbidden, "账号不可用")
c.Abort()
return
}
}
c.Set("actor_id", cl.Subject)
c.Next()
}
}
func actor(c *gin.Context) string { return c.GetString("actor_id") }
func ok(c *gin.Context, data any) {
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok", "data": data})
}
func created(c *gin.Context, data any) {
c.JSON(http.StatusCreated, gin.H{"code": 0, "message": "ok", "data": data})
}
func fail(c *gin.Context, status int, message string) {
c.JSON(status, gin.H{"code": status, "message": message})
}
func validID(id string) bool { return uuid.Validate(id) == nil }
func parseDay(value string) (time.Time, error) {
return time.ParseInLocation("2006-01-02", value, time.Local)
}
func (a *API) ownedPet(userID, petID string) (model.Pet, error) {
var pet model.Pet
if !validID(petID) {
return pet, gorm.ErrRecordNotFound
}
err := a.db.Where("id = ? AND user_id = ? AND archived = false", petID, userID).First(&pet).Error
return pet, err
}