feat(gateway): 真实鉴权片1 —— JWT 注册/登录 + 校验中间件(后端核心)
替掉"裸 X-User-ID 头当身份"的临时方案,落地无状态 JWT 鉴权后端: - internal/auth:JWT 签发/校验(HS256,密钥 env JWT_SECRET,仅接受 HMAC 防 alg 混淆) + bcrypt 密码哈希/校验。纯包,含单测。 - User 模型加 Name + PasswordHash(json:"-" 不外泄);store 加 CreateUser/GetUserByEmail/ GetUserByID(邮箱唯一冲突 → ErrUserExists)。 - handler/auth:POST /auth/register(建用户+签发)· POST /auth/login(校验+签发, 用户不存在与密码错同一文案防枚举)· GET /auth/me。 - middleware/auth:解析 Bearer JWT,校验通过把已验证 userID 注入上下文(非阻断)。 - userID(c) 改为优先取 JWT 注入的 uid,兜底 X-User-ID 头(前端尚未接登录,保持可用)。 验证: - 单测:JWT 签发/解析往返、过期拒绝、篡改/非法拒绝、bcrypt 哈希校验。 - 实跑(nats+pg+gateway):注册→token+user(无密码)、重复注册 409、错密码 401、 /auth/me 带 token 200 / 无 token 401;owner 隔离改用已验证 uid —— 带 token 建的库 匿名/伪造 header 都看不到(JWT 用户数据归于雪花 id,header 无法臆测)。 片 2 待做:前端登录页 + 存令牌带 Bearer + 处理 401 + 去掉 header 兜底 + 保护路由。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ require (
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -58,11 +59,11 @@ require (
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -36,6 +36,8 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -135,15 +137,25 @@ golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
|
||||
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Package auth 提供无状态鉴权能力:JWT 签发/校验 + 密码哈希。
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// TokenTTL 是访问令牌有效期。
|
||||
const TokenTTL = 24 * time.Hour
|
||||
|
||||
// secret 是 JWT 签名密钥。生产经环境变量 JWT_SECRET 注入;缺省仅供开发(务必覆盖)。
|
||||
var secret = []byte(envOr("JWT_SECRET", "sundynix-dev-secret-change-me"))
|
||||
|
||||
// ErrInvalidToken 表示令牌无效/过期/签名不符。
|
||||
var ErrInvalidToken = errors.New("invalid token")
|
||||
|
||||
// Issue 为某用户签发 JWT(subject = userID)。
|
||||
func Issue(userID string) (string, error) { return issue(userID, TokenTTL) }
|
||||
|
||||
func issue(userID string, ttl time.Duration) (string, error) {
|
||||
now := time.Now()
|
||||
claims := jwt.RegisteredClaims{
|
||||
Subject: userID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
|
||||
}
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret)
|
||||
}
|
||||
|
||||
// Parse 校验 JWT 并返回 userID(subject)。无效/过期/签名不符返回 ErrInvalidToken。
|
||||
func Parse(token string) (string, error) {
|
||||
t, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // 仅接受 HMAC,防 alg 混淆攻击
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return secret, nil
|
||||
})
|
||||
if err != nil || !t.Valid {
|
||||
return "", ErrInvalidToken
|
||||
}
|
||||
c, ok := t.Claims.(*jwt.RegisteredClaims)
|
||||
if !ok || c.Subject == "" {
|
||||
return "", ErrInvalidToken
|
||||
}
|
||||
return c.Subject, nil
|
||||
}
|
||||
|
||||
// HashPassword 用 bcrypt 哈希明文密码。
|
||||
func HashPassword(pw string) (string, error) {
|
||||
b, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// CheckPassword 校验明文与 bcrypt 哈希是否匹配。
|
||||
func CheckPassword(hash, pw string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil
|
||||
}
|
||||
|
||||
func envOr(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJWT_RoundTrip(t *testing.T) {
|
||||
tok, err := Issue("user-123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
uid, err := Parse(tok)
|
||||
if err != nil {
|
||||
t.Fatalf("应能解析自签令牌: %v", err)
|
||||
}
|
||||
if uid != "user-123" {
|
||||
t.Errorf("subject = %q, want user-123", uid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWT_Expired(t *testing.T) {
|
||||
tok, _ := issue("u", -time.Hour) // 已过期
|
||||
if _, err := Parse(tok); err == nil {
|
||||
t.Error("过期令牌应拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWT_Tampered(t *testing.T) {
|
||||
tok, _ := Issue("u")
|
||||
if _, err := Parse(tok + "x"); err == nil {
|
||||
t.Error("被篡改令牌应拒绝")
|
||||
}
|
||||
if _, err := Parse("not.a.jwt"); err == nil {
|
||||
t.Error("非法令牌应拒绝")
|
||||
}
|
||||
if _, err := Parse(""); err == nil {
|
||||
t.Error("空令牌应拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassword_HashAndCheck(t *testing.T) {
|
||||
hash, err := HashPassword("s3cret-pw")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hash == "s3cret-pw" || hash == "" {
|
||||
t.Error("应为 bcrypt 哈希,非明文")
|
||||
}
|
||||
if !CheckPassword(hash, "s3cret-pw") {
|
||||
t.Error("正确密码应校验通过")
|
||||
}
|
||||
if CheckPassword(hash, "wrong-pw") {
|
||||
t.Error("错误密码应校验失败")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sundynix/sundynix-gateway/internal/auth"
|
||||
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||
)
|
||||
|
||||
// userJSON 是对外的用户视图(绝不含密码哈希)。
|
||||
func userJSON(u *store.User) gin.H {
|
||||
return gin.H{"id": u.ID, "email": u.Email, "name": u.Name}
|
||||
}
|
||||
|
||||
// Register: POST /api/v1/auth/register {email, password, name} —— 注册并签发 JWT。
|
||||
func (h *Handler) Register(c *gin.Context) {
|
||||
var body struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(strings.ToLower(body.Email))
|
||||
if !strings.Contains(email, "@") || len(body.Password) < 6 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "需合法邮箱且密码至少 6 位"})
|
||||
return
|
||||
}
|
||||
hash, err := auth.HashPassword(body.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"})
|
||||
return
|
||||
}
|
||||
u, err := h.db.CreateUser(c.Request.Context(), email, strings.TrimSpace(body.Name), hash)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrUserExists) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "该邮箱已注册"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
issueToken(c, u)
|
||||
}
|
||||
|
||||
// Login: POST /api/v1/auth/login {email, password} —— 校验并签发 JWT。
|
||||
func (h *Handler) Login(c *gin.Context) {
|
||||
var body struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
email := strings.TrimSpace(strings.ToLower(body.Email))
|
||||
u, err := h.db.GetUserByEmail(c.Request.Context(), email)
|
||||
// 用户不存在与密码错误返回同一文案,避免邮箱枚举。
|
||||
if err != nil || u == nil || !auth.CheckPassword(u.PasswordHash, body.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
|
||||
return
|
||||
}
|
||||
issueToken(c, u)
|
||||
}
|
||||
|
||||
// Me: GET /api/v1/auth/me —— 返回当前登录用户(无有效令牌则 401)。
|
||||
func (h *Handler) Me(c *gin.Context) {
|
||||
uid := userID(c)
|
||||
if uid == "" || uid == "anonymous" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
|
||||
return
|
||||
}
|
||||
u, err := h.db.GetUserByID(c.Request.Context(), uid)
|
||||
if err != nil || u == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"user": userJSON(u)})
|
||||
}
|
||||
|
||||
// issueToken 为用户签发 JWT 并返回 {token, user}。
|
||||
func issueToken(c *gin.Context, u *store.User) {
|
||||
token, err := auth.Issue(u.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "签发令牌失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"token": token, "user": userJSON(u)})
|
||||
}
|
||||
@@ -184,8 +184,14 @@ func (h *Handler) SetMemory(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": res.Content})
|
||||
}
|
||||
|
||||
// userID 从请求取已登录用户标识(真实场景应由鉴权中间件注入)。
|
||||
// userID 取当前用户标识:优先 JWT 鉴权中间件注入的已验证 uid;
|
||||
// 兜底 X-User-ID 头(开发期 / 前端尚未接登录),都没有则匿名。
|
||||
func userID(c *gin.Context) string {
|
||||
if v, ok := c.Get("uid"); ok {
|
||||
if s, _ := v.(string); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
if u := c.GetHeader("X-User-ID"); u != "" {
|
||||
return u
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/sundynix/sundynix-gateway/internal/auth"
|
||||
)
|
||||
|
||||
// CtxUserID 是鉴权后写入 gin.Context 的已验证用户 ID 键。
|
||||
const CtxUserID = "uid"
|
||||
|
||||
// Auth 解析 Authorization: Bearer <JWT>,校验通过则把已验证 userID 写入上下文。
|
||||
// 非阻断:无 token / 无效 token 时不报错,由各 handler(经 userID 兜底 header)或
|
||||
// 后续 RequireAuth 决定是否放行。
|
||||
func Auth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
h := c.GetHeader("Authorization")
|
||||
if strings.HasPrefix(h, "Bearer ") {
|
||||
if uid, err := auth.Parse(strings.TrimSpace(h[len("Bearer "):])); err == nil {
|
||||
c.Set(CtxUserID, uid)
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,16 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
r := gin.Default()
|
||||
r.Use(cors()) // 桌面端/浏览器跨源访问(开发期放开)
|
||||
r.Use(middleware.RateLimit(cache))
|
||||
r.Use(middleware.Guardrail()) // Harness: Input/Output Guardrail
|
||||
r.Use(middleware.Auth()) // 解析 Bearer JWT,注入已验证 userID(非阻断)
|
||||
r.Use(middleware.Guardrail()) // Harness: Input Guardrail
|
||||
|
||||
h := handler.New(db, cache, bus, blobStore)
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
api.POST("/auth/register", h.Register) // 注册 + 签发 JWT
|
||||
api.POST("/auth/login", h.Login) // 登录 + 签发 JWT
|
||||
api.GET("/auth/me", h.Me) // 当前登录用户
|
||||
|
||||
api.POST("/tasks", h.SubmitTask) // 1. 解析 DSL 并 Publish 到 NATS
|
||||
api.GET("/tasks/:id/stream", h.StreamTask) // 4. SSE/WS 回流 Token Stream
|
||||
api.GET("/tasks/:id/exec", h.StreamExec) // 4b. SSE 回流执行轨迹事件(运行·观测)
|
||||
|
||||
@@ -8,7 +8,9 @@ package store
|
||||
// User 是平台用户(Users)。
|
||||
type User struct {
|
||||
BaseModel
|
||||
Email string `gorm:"uniqueIndex;size:255"`
|
||||
Email string `gorm:"uniqueIndex;size:255"`
|
||||
Name string `gorm:"size:64"`
|
||||
PasswordHash string `gorm:"size:255" json:"-"` // bcrypt;绝不出 JSON
|
||||
}
|
||||
|
||||
// Task 是一次提交的 Agent 编排任务(DSL)。
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ErrUserExists 表示邮箱已注册(唯一约束冲突)。
|
||||
var ErrUserExists = errors.New("email already registered")
|
||||
|
||||
// CreateUser 新建用户(密码已在调用方 bcrypt 哈希)。邮箱重复返回 ErrUserExists。
|
||||
func (p *Postgres) CreateUser(ctx context.Context, email, name, passwordHash string) (*User, error) {
|
||||
if p.db == nil {
|
||||
return nil, errStoreDisabled
|
||||
}
|
||||
var existed int64
|
||||
p.db.WithContext(ctx).Model(&User{}).Where("email = ?", email).Count(&existed)
|
||||
if existed > 0 {
|
||||
return nil, ErrUserExists
|
||||
}
|
||||
u := &User{Email: email, Name: name, PasswordHash: passwordHash}
|
||||
if err := p.db.WithContext(ctx).Create(u).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail 按邮箱取用户(登录用)。
|
||||
func (p *Postgres) GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||
if p.db == nil {
|
||||
return nil, errStoreDisabled
|
||||
}
|
||||
var u User
|
||||
if err := p.db.WithContext(ctx).Where("email = ?", email).First(&u).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// GetUserByID 按主键取用户(鉴权后取当前用户)。
|
||||
func (p *Postgres) GetUserByID(ctx context.Context, id string) (*User, error) {
|
||||
if p.db == nil {
|
||||
return nil, errStoreDisabled
|
||||
}
|
||||
var u User
|
||||
if err := p.db.WithContext(ctx).First(&u, "id = ?", id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
Reference in New Issue
Block a user