Files
dudu/server/internal/auth/handlers.go
T
wangjia 40760aa884
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s
dudu MVP:五端语音输入法初始提交
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:38:37 +08:00

139 lines
4.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"encoding/json"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
"dudu/server/internal/store"
"dudu/server/pkg/protocol"
)
// Handlers 认证路由:扫码(5B)、微信 OAuth(5C)、logout。
type Handlers struct {
DB *gorm.DB
RDB *redis.Client
JWT *JWT
Wechat WechatClient
// QrAuthURL 二维码内容模板(真实环境为微信开放平台授权页,%s 为 state)
QrAuthURL string
}
const qrTTL = 2 * time.Minute
type qrState struct {
Status string `json:"status"` // pending | confirmed
Token string `json:"token,omitempty"`
UserID string `json:"user_id,omitempty"`
Nick string `json:"nick,omitempty"`
}
// CreateQr POST /v1/auth/qr 🔓
func (h *Handlers) CreateQr(c *gin.Context) {
state := uuid.NewString()
b, _ := json.Marshal(qrState{Status: "pending"})
if err := h.RDB.Set(c, store.KeyAuthQr(state), b, qrTTL).Err(); err != nil {
c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
return
}
url := h.QrAuthURL
if url == "" {
url = "https://dudu.app/auth/qr/" // mock 占位,接入开放平台后替换
}
c.JSON(http.StatusOK, protocol.AuthQrResponse{State: state, QrURL: url + state})
}
// PollQr GET /v1/auth/qr/:state 🔓(桌面端 1s 轮询)
func (h *Handlers) PollQr(c *gin.Context) {
b, err := h.RDB.Get(c, store.KeyAuthQr(c.Param("state"))).Bytes()
if err == redis.Nil {
c.JSON(http.StatusOK, protocol.AuthQrStatusResponse{Status: "expired"})
return
}
if err != nil {
c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
return
}
var s qrState
_ = json.Unmarshal(b, &s)
resp := protocol.AuthQrStatusResponse{Status: s.Status}
if s.Status == "confirmed" {
resp.Token = s.Token
resp.User = &protocol.UserInfo{UserID: s.UserID, NicknameMasked: s.Nick}
// 一次性:下发后即删,防复用
h.RDB.Del(c, store.KeyAuthQr(c.Param("state")))
}
c.JSON(http.StatusOK, resp)
}
// QrCallback GET /v1/auth/wechat/callback?code=&state= 🔓
// 手机微信内授权后回调:code 换身份 → 建号 → 标记 state confirmed。
func (h *Handlers) QrCallback(c *gin.Context) {
code, state := c.Query("code"), c.Query("state")
key := store.KeyAuthQr(state)
if n, _ := h.RDB.Exists(c, key).Result(); n == 0 {
c.String(http.StatusBadRequest, "二维码已过期,请回到 dudu 重新获取")
return
}
info, err := h.Wechat.ExchangeCode(c, code, "web")
if err != nil {
c.String(http.StatusBadRequest, "微信授权失败,请重试")
return
}
user, err := FindOrCreateUser(h.DB, info, "web")
if err != nil {
c.String(http.StatusInternalServerError, "服务繁忙,请重试")
return
}
token, err := h.JWT.Sign(user.ID)
if err != nil {
c.String(http.StatusInternalServerError, "服务繁忙,请重试")
return
}
b, _ := json.Marshal(qrState{
Status: "confirmed", Token: token, UserID: user.ID, Nick: MaskNickname(user.Nickname),
})
_ = h.RDB.Set(c, key, b, qrTTL).Err()
c.String(http.StatusOK, "登录成功,回到 dudu 继续")
}
// MobileLogin POST /v1/auth/wechat 🔓(移动端 OpenSDK code
func (h *Handlers) MobileLogin(c *gin.Context) {
var req protocol.AuthWechatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
return
}
info, err := h.Wechat.ExchangeCode(c, req.Code, "mobile")
if err != nil {
c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
return
}
user, err := FindOrCreateUser(h.DB, info, "mobile")
if err != nil {
c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
return
}
token, err := h.JWT.Sign(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
return
}
c.JSON(http.StatusOK, protocol.AuthTokenResponse{
Token: token,
User: protocol.UserInfo{UserID: user.ID, NicknameMasked: MaskNickname(user.Nickname)},
})
}
// Logout POST /v1/auth/logout(需登录)
func (h *Handlers) Logout(c *gin.Context) {
jti := c.GetString(CtxJTI)
_ = h.JWT.Revoke(c, jti, time.Now().Add(8*24*time.Hour)) // 覆盖最长 TTL
c.Status(http.StatusNoContent)
}