feat(ECR-006): 落地运营后台 Phase A(admin API + admin-h5)
新增独立鉴权的 /api/v1/admin 与 Vue 控制台;会员授予与审计同事务,并补集成/单测。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/service/admin"
|
||||
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
|
||||
)
|
||||
|
||||
// AdminHandler serves /api/v1/admin/* (no DeviceAuth).
|
||||
type AdminHandler struct {
|
||||
Svc *admin.Service
|
||||
}
|
||||
|
||||
// Register mounts public login + authed admin routes.
|
||||
func (h *AdminHandler) Register(api *gin.RouterGroup) {
|
||||
g := api.Group("/admin")
|
||||
g.POST("/auth/login", h.Login)
|
||||
|
||||
authed := g.Group("")
|
||||
authed.Use(middleware.AdminAuth(h.Svc))
|
||||
authed.POST("/auth/logout", h.Logout)
|
||||
authed.GET("/me", h.Me)
|
||||
authed.GET("/users", h.ListUsers)
|
||||
authed.GET("/users/:id", h.GetUser)
|
||||
authed.POST("/users/:id/membership/grant", h.GrantMembership)
|
||||
authed.GET("/orders", h.ListOrders)
|
||||
authed.GET("/audit-logs", h.ListAudit)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) Login(c *gin.Context) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.Username == "" || body.Password == "" {
|
||||
response.Fail(c, http.StatusBadRequest, 40001, "username and password required")
|
||||
return
|
||||
}
|
||||
res, err := h.Svc.Login(c.Request.Context(), body.Username, body.Password)
|
||||
if err != nil {
|
||||
if errors.Is(err, admin.ErrBadCredentials) {
|
||||
response.Fail(c, http.StatusUnauthorized, 40103, "invalid credentials")
|
||||
return
|
||||
}
|
||||
response.Fail(c, http.StatusInternalServerError, 50010, "login failed")
|
||||
return
|
||||
}
|
||||
response.OK(c, res)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) Logout(c *gin.Context) {
|
||||
token := middleware.BearerToken(c.GetHeader("Authorization"))
|
||||
_ = h.Svc.Logout(c.Request.Context(), token)
|
||||
response.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) Me(c *gin.Context) {
|
||||
adminID, ok := middleware.AdminIDFromContext(c)
|
||||
if !ok {
|
||||
response.Fail(c, http.StatusUnauthorized, 40101, "admin auth required")
|
||||
return
|
||||
}
|
||||
me, err := h.Svc.Me(c.Request.Context(), adminID)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 50011, "me failed")
|
||||
return
|
||||
}
|
||||
response.OK(c, me)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListUsers(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
items, err := h.Svc.ListUsers(c.Request.Context(), c.Query("q"), limit, offset)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 50012, "list users failed")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) GetUser(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40002, "invalid user id")
|
||||
return
|
||||
}
|
||||
detail, err := h.Svc.GetUser(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, admin.ErrUserNotFound) {
|
||||
response.Fail(c, http.StatusNotFound, 40401, "user not found")
|
||||
return
|
||||
}
|
||||
response.Fail(c, http.StatusInternalServerError, 50013, "get user failed")
|
||||
return
|
||||
}
|
||||
response.OK(c, detail)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) GrantMembership(c *gin.Context) {
|
||||
adminID, ok := middleware.AdminIDFromContext(c)
|
||||
if !ok {
|
||||
response.Fail(c, http.StatusUnauthorized, 40101, "admin auth required")
|
||||
return
|
||||
}
|
||||
userID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusBadRequest, 40002, "invalid user id")
|
||||
return
|
||||
}
|
||||
var body admin.GrantInput
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.Plan == "" {
|
||||
response.Fail(c, http.StatusBadRequest, 40003, "plan required")
|
||||
return
|
||||
}
|
||||
if err := h.Svc.GrantMembership(c.Request.Context(), adminID, userID, body.Plan); err != nil {
|
||||
if errors.Is(err, admin.ErrInvalidPlan) {
|
||||
response.Fail(c, http.StatusBadRequest, 40004, "invalid plan")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, admin.ErrUserNotFound) {
|
||||
response.Fail(c, http.StatusNotFound, 40401, "user not found")
|
||||
return
|
||||
}
|
||||
response.Fail(c, http.StatusInternalServerError, 50014, "grant failed")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListOrders(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
items, err := h.Svc.ListOrders(c.Request.Context(), limit, offset)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 50015, "list orders failed")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ListAudit(c *gin.Context) {
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
items, err := h.Svc.ListAuditLogs(c.Request.Context(), limit, offset)
|
||||
if err != nil {
|
||||
response.Fail(c, http.StatusInternalServerError, 50016, "list audit failed")
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
Reference in New Issue
Block a user