feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -21,12 +22,33 @@ const UserIDKey ctxKey = "user_id"
|
||||
const DeviceKeyHeader = "X-Device-Key"
|
||||
|
||||
// DeviceAuth resolves or creates a Visitor→User via device key.
|
||||
// If Authorization Bearer session is valid, that account user wins and device rebinds.
|
||||
func DeviceAuth(pool *pgxpool.Pool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
key := c.GetHeader(DeviceKeyHeader)
|
||||
if key == "" {
|
||||
key = newDeviceKey()
|
||||
c.Header(DeviceKeyHeader, key)
|
||||
c.Request.Header.Set(DeviceKeyHeader, key)
|
||||
}
|
||||
if tok := bearerFromHeader(c.GetHeader("Authorization")); tok != "" {
|
||||
var uid uuid.UUID
|
||||
err := pool.QueryRow(c.Request.Context(), `
|
||||
SELECT user_id FROM user_sessions
|
||||
WHERE token=$1 AND revoked_at IS NULL AND expires_at > now()`, tok,
|
||||
).Scan(&uid)
|
||||
if err == nil {
|
||||
_, _ = pool.Exec(c.Request.Context(), `
|
||||
INSERT INTO device_identities(device_key, user_id)
|
||||
VALUES ($1,$2)
|
||||
ON CONFLICT (device_key) DO UPDATE SET user_id=$2, updated_at=now(), deleted_at=NULL`,
|
||||
key, uid,
|
||||
)
|
||||
c.Set(string(UserIDKey), uid.String())
|
||||
c.Header(DeviceKeyHeader, key)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
userID, err := ensureUser(c.Request.Context(), pool, key)
|
||||
if err != nil {
|
||||
@@ -40,6 +62,39 @@ func DeviceAuth(pool *pgxpool.Pool) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func bearerFromHeader(h string) string {
|
||||
if len(h) < 8 {
|
||||
return ""
|
||||
}
|
||||
if strings.EqualFold(h[:7], "bearer ") {
|
||||
return strings.TrimSpace(h[7:])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RequireRegistered rejects anonymous (no phone) users.
|
||||
func RequireRegistered(pool *pgxpool.Pool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, ok := UserIDFromContext(c)
|
||||
if !ok {
|
||||
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
var okReg bool
|
||||
err := pool.QueryRow(c.Request.Context(), `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM users WHERE id=$1 AND phone IS NOT NULL AND deleted_at IS NULL
|
||||
)`, userID).Scan(&okReg)
|
||||
if err != nil || !okReg {
|
||||
response.Fail(c, http.StatusUnauthorized, 40112, "请先登录后再使用")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// UserIDFromContext returns the authenticated user id.
|
||||
func UserIDFromContext(c *gin.Context) (uuid.UUID, bool) {
|
||||
v, ok := c.Get(string(UserIDKey))
|
||||
|
||||
Reference in New Issue
Block a user