- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion. - Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects. - Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management. - Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering. - Improve database initialization by moving it from `internal/model` to `internal/database`. - Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool. - Enhance error handling and security by implementing fail-fast checks for encryption key length during startup. - Clean up unused code, including the `avatar` package and several unused DTOs.
231 lines
5.3 KiB
Go
231 lines
5.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"with_you/internal/pkg/response"
|
|
"with_you/internal/pkg/ws"
|
|
"with_you/internal/service"
|
|
)
|
|
|
|
// QRCodeHandler 二维码登录处理器
|
|
type QRCodeHandler struct {
|
|
qrcodeService service.QRCodeLoginService
|
|
}
|
|
|
|
// NewQRCodeHandler 创建二维码登录处理器
|
|
func NewQRCodeHandler(qrcodeService service.QRCodeLoginService) *QRCodeHandler {
|
|
return &QRCodeHandler{
|
|
qrcodeService: qrcodeService,
|
|
}
|
|
}
|
|
|
|
// GetQRCode 获取二维码
|
|
// GET /api/v1/auth/qrcode
|
|
func (h *QRCodeHandler) GetQRCode(c *gin.Context) {
|
|
session, err := h.qrcodeService.CreateSession(c.Request.Context())
|
|
if err != nil {
|
|
response.InternalServerError(c, "failed to create qrcode session")
|
|
return
|
|
}
|
|
|
|
// 生成二维码URL (deeplink格式)
|
|
qrcodeURL := fmt.Sprintf("carrotbbs://qrcode/login?session_id=%s", session.SessionID)
|
|
|
|
response.Success(c, gin.H{
|
|
"session_id": session.SessionID,
|
|
"qrcode_url": qrcodeURL,
|
|
"expires_in": 300, // 5分钟
|
|
"expires_at": session.ExpiresAt,
|
|
})
|
|
}
|
|
|
|
// WSEvents WebSocket事件流
|
|
// GET /api/v1/auth/qrcode/events?session_id=xxx
|
|
func (h *QRCodeHandler) WSEvents(c *gin.Context) {
|
|
sessionID := c.Query("session_id")
|
|
if sessionID == "" {
|
|
response.BadRequest(c, "session_id is required")
|
|
return
|
|
}
|
|
|
|
ch, cancel := h.qrcodeService.GetWSHub().Subscribe(sessionID, 0)
|
|
defer cancel()
|
|
|
|
w := c.Writer
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
response.InternalServerError(c, "streaming unsupported")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.Header().Set("X-Accel-Buffering", "no")
|
|
c.Status(http.StatusOK)
|
|
flusher.Flush()
|
|
|
|
writeEvent := func(ev ws.Event) bool {
|
|
data, err := ws.EncodeData(ev)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if _, err := fmt.Fprintf(w, "id: %d\nevent: %s\ndata: %s\n\n", ev.ID, ev.Event, data); err != nil {
|
|
return false
|
|
}
|
|
flusher.Flush()
|
|
return true
|
|
}
|
|
|
|
// 心跳
|
|
heartbeat := time.NewTicker(25 * time.Second)
|
|
defer heartbeat.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-c.Request.Context().Done():
|
|
return
|
|
case ev, ok := <-ch:
|
|
if !ok || !writeEvent(ev) {
|
|
return
|
|
}
|
|
case <-heartbeat.C:
|
|
if _, err := fmt.Fprint(w, "event: heartbeat\ndata: {}\n\n"); err != nil {
|
|
return
|
|
}
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scan 扫描二维码
|
|
// POST /api/v1/auth/qrcode/scan
|
|
func (h *QRCodeHandler) Scan(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
if userID == "" {
|
|
response.Unauthorized(c, "")
|
|
return
|
|
}
|
|
|
|
type ScanRequest struct {
|
|
SessionID string `json:"session_id" binding:"required"`
|
|
}
|
|
|
|
var req ScanRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.qrcodeService.Scan(c.Request.Context(), req.SessionID, userID); err != nil {
|
|
if err.Error() == "qrcode already scanned" {
|
|
response.BadRequest(c, "qrcode already scanned by others")
|
|
return
|
|
}
|
|
if err.Error() == "session not found or expired" {
|
|
response.NotFound(c, "qrcode expired")
|
|
return
|
|
}
|
|
response.InternalServerError(c, "failed to scan qrcode")
|
|
return
|
|
}
|
|
|
|
// 获取用户信息返回
|
|
user, err := h.qrcodeService.GetUserService().GetUserByID(c.Request.Context(), userID)
|
|
if err != nil {
|
|
response.InternalServerError(c, "failed to get user info")
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"user": gin.H{
|
|
"id": user.ID,
|
|
"nickname": user.Nickname,
|
|
"avatar": user.Avatar,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Confirm 确认登录
|
|
// POST /api/v1/auth/qrcode/confirm
|
|
func (h *QRCodeHandler) Confirm(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
if userID == "" {
|
|
response.Unauthorized(c, "")
|
|
return
|
|
}
|
|
|
|
type ConfirmRequest struct {
|
|
SessionID string `json:"session_id" binding:"required"`
|
|
}
|
|
|
|
var req ConfirmRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
ip := c.ClientIP()
|
|
userAgent := c.GetHeader("User-Agent")
|
|
|
|
result, err := h.qrcodeService.Confirm(c.Request.Context(), req.SessionID, userID, ip, userAgent)
|
|
if err != nil {
|
|
if err.Error() == "unauthorized" {
|
|
response.Unauthorized(c, "not authorized to confirm this login")
|
|
return
|
|
}
|
|
if err.Error() == "session not found or expired" {
|
|
response.NotFound(c, "qrcode expired")
|
|
return
|
|
}
|
|
response.InternalServerError(c, "failed to confirm login")
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{
|
|
"success": true,
|
|
"user": gin.H{
|
|
"id": result.User.ID,
|
|
"username": result.User.Username,
|
|
"nickname": result.User.Nickname,
|
|
"avatar": result.User.Avatar,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Cancel 取消登录
|
|
// POST /api/v1/auth/qrcode/cancel
|
|
func (h *QRCodeHandler) Cancel(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
if userID == "" {
|
|
response.Unauthorized(c, "")
|
|
return
|
|
}
|
|
|
|
type CancelRequest struct {
|
|
SessionID string `json:"session_id" binding:"required"`
|
|
}
|
|
|
|
var req CancelRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.qrcodeService.Cancel(c.Request.Context(), req.SessionID, userID); err != nil {
|
|
if err.Error() == "unauthorized" {
|
|
response.Unauthorized(c, "not authorized to cancel this login")
|
|
return
|
|
}
|
|
response.InternalServerError(c, "failed to cancel login")
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"success": true})
|
|
}
|