package api import ( "encoding/json" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "opencatd-open/internal/auth" "opencatd-open/internal/pkg/jwt" "opencatd-open/internal/store" ) // PasskeyRegisterBegin POST /api/webauthn/register/begin — 生成注册选项。 func (h *Handler) PasskeyRegisterBegin(c *gin.Context) { userID, _ := c.Get("user_id") u, err := h.passkeys.GetUserByID(userID.(uint64)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"}) return } creation, err := h.passkeys.BeginRegistration(u) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to begin registration: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": gin.H{"creation": creation, "challenge": creation.Response.Challenge}}) } // PasskeyRegisterComplete POST /api/webauthn/register/complete — 校验并保存凭据。 func (h *Handler) PasskeyRegisterComplete(c *gin.Context) { userID, _ := c.Get("user_id") u, err := h.passkeys.GetUserByID(userID.(uint64)) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"}) return } var req struct { Challenge string `json:"challenge"` Name string `json:"name"` Credential json.RawMessage `json:"credential"` } if err := c.ShouldBindJSON(&req); err != nil || len(req.Credential) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid input"}) return } if err := h.passkeys.FinishRegistration(u, req.Challenge, req.Credential, req.Name); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "passkey 注册失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) } // PasskeyLoginBegin POST /api/auth/passkey/begin — 生成断言选项。 // 传 username 用指定用户;不传则用可发现凭据(平台 passkey)。 func (h *Handler) PasskeyLoginBegin(c *gin.Context) { var req struct { Username string `json:"username"` } _ = c.ShouldBindJSON(&req) if req.Username != "" { u, err := h.passkeys.GetUserByUsername(req.Username) if err != nil || u.Status != store.UserStatusActive { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } assertion, err := h.passkeys.BeginLogin(u) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to begin login: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": gin.H{"assertion": assertion, "challenge": assertion.Response.Challenge, "user_id": u.ID}}) return } assertion, err := h.passkeys.BeginDiscoverableLogin() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to begin login: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": gin.H{"assertion": assertion, "challenge": assertion.Response.Challenge}}) } // PasskeyLoginComplete POST /api/auth/passkey/finish — 校验断言并发放令牌。 func (h *Handler) PasskeyLoginComplete(c *gin.Context) { var req struct { Challenge string `json:"challenge"` Credential json.RawMessage `json:"credential"` UserID uint64 `json:"user_id"` } if err := c.ShouldBindJSON(&req); err != nil || len(req.Credential) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid input"}) return } var u *store.User if req.UserID > 0 { var err error u, err = h.passkeys.GetUserByID(req.UserID) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"}) return } if err := h.passkeys.FinishLogin(u, req.Challenge, req.Credential); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "passkey 校验失败: " + err.Error()}) return } } else { var err error u, err = h.passkeys.FinishDiscoverableLogin(req.Challenge, req.Credential) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "passkey 校验失败: " + err.Error()}) return } } if u.Status != store.UserStatusActive { c.JSON(http.StatusForbidden, gin.H{"error": "user account disabled"}) return } secret := auth.GetSecretKey() accessToken, refreshToken, err := jwt.GenerateTokenPair(u.ID, u.Username, u.Role, secret, 24*time.Hour, 7*24*time.Hour) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to issue token"}) return } c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "token": accessToken, "access_token": accessToken, "refresh_token": refreshToken, }, }) } // PasskeyList GET /api/profile/passkeys — 当前用户的 passkey 列表。 func (h *Handler) PasskeyList(c *gin.Context) { userID, _ := c.Get("user_id") pks, err := h.passkeys.List(userID.(uint64)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load passkeys"}) return } out := make([]gin.H, 0, len(pks)) for _, pk := range pks { out = append(out, gin.H{"id": pk.ID, "name": pk.Name, "created_at": pk.CreatedAt}) } c.JSON(http.StatusOK, gin.H{"data": out}) } // PasskeyDelete DELETE /api/profile/passkeys/:id — 解除绑定。 func (h *Handler) PasskeyDelete(c *gin.Context) { userID, _ := c.Get("user_id") id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid passkey id"}) return } if err := h.passkeys.Delete(userID.(uint64), id); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "passkey not found"}) return } c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) }