feat: rebuild pet app and backend around core care flows
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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/model"
|
||||
"github.com/sundynix/pets-be/internal/storage"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
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}
|
||||
}
|
||||
|
||||
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("/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("/pets", a.adminPets)
|
||||
admin.GET("/walks", a.adminWalks)
|
||||
admin.GET("/meetups", a.adminMeetups)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user