191 lines
6.3 KiB
Go
191 lines
6.3 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)
|
|
api.POST("/auth/refresh", a.refreshToken("user"))
|
|
api.POST("/admin/refresh", a.refreshToken("admin"))
|
|
api.POST("/auth/logout", a.revokeRefresh("user"))
|
|
api.POST("/admin/logout", a.revokeRefresh("admin"))
|
|
|
|
user := api.Group("")
|
|
user.Use(a.requireRole("user"))
|
|
user.GET("/environment", a.environment)
|
|
user.GET("/me", a.me)
|
|
user.GET("/breeds", a.listBreeds)
|
|
user.GET("/breeds/:id", a.breedDetail)
|
|
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.PUT("/tasks/completions", a.completeTasks)
|
|
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("/breeds", a.adminBreeds)
|
|
admin.POST("/breeds", a.createBreed)
|
|
admin.PUT("/breeds/:id", a.updateBreed)
|
|
admin.PUT("/breeds/:id/enabled", a.setBreedEnabled)
|
|
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"`
|
|
SessionID string `json:"sid,omitempty"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
func (a *API) token(id, role string) (string, error) { return a.sessionToken(id, role, "") }
|
|
func (a *API) sessionToken(id, role, sessionID string) (string, error) {
|
|
now := time.Now()
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims{Role: role, SessionID: sessionID, RegisteredClaims: jwt.RegisteredClaims{
|
|
ID: uuid.NewString(), Subject: id, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(2 * 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
|
|
}
|
|
// Reject credentials issued with the old seven-day lifetime.
|
|
if cl.IssuedAt == nil || cl.ExpiresAt == nil || cl.ExpiresAt.Sub(cl.IssuedAt.Time) > 2*time.Hour {
|
|
fail(c, http.StatusUnauthorized, "登录已过期")
|
|
c.Abort()
|
|
return
|
|
}
|
|
if cl.SessionID != "" {
|
|
var count int64
|
|
if !validID(cl.SessionID) || a.db.Model(&model.RefreshSession{}).Where("id = ? AND actor_id = ? AND role = ? AND revoked = false AND expires_at > ?", cl.SessionID, cl.Subject, role, time.Now()).Count(&count).Error != nil || count != 1 {
|
|
fail(c, http.StatusUnauthorized, "登录已过期")
|
|
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
|
|
}
|