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 }