package api import ( "encoding/json" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/openteam/server/internal/pkg/resp" "github.com/openteam/server/internal/store" ) // PasskeyRegisterBegin POST /api/v1/webauthn/register/begin — 生成注册选项。 func (h *Handler) PasskeyRegisterBegin(c *gin.Context) { u := sessionUser(c) creation, err := h.passkeys.BeginRegistration(u) if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to begin registration: "+err.Error()) return } resp.OK(c, gin.H{"creation": creation, "challenge": creation.Response.Challenge}) } // PasskeyRegisterComplete POST /api/v1/webauthn/register/complete — 校验并保存凭据。 func (h *Handler) PasskeyRegisterComplete(c *gin.Context) { u := sessionUser(c) 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 { resp.Fail(c, http.StatusBadRequest, "invalid input") return } if err := h.passkeys.FinishRegistration(u, req.Challenge, req.Credential, []byte(req.Name)); err != nil { resp.Fail(c, http.StatusBadRequest, "passkey 注册失败: "+err.Error()) return } resp.OK(c, gin.H{"ok": true}) } // PasskeyLoginBegin POST /api/v1/webauthn/login/begin — 生成断言选项。 // 传 username 用指定用户;不传则用可发现凭据(平台 passkey)。 func (h *Handler) PasskeyLoginBegin(c *gin.Context) { var req struct { Username string `json:"username"` } _ = c.ShouldBindJSON(&req) if req.Username != "" { var u store.User err := h.a.DB.Where("username = ? OR email = ?", req.Username, req.Username).First(&u).Error if err != nil || u.Status != store.UserStatusActive { resp.Fail(c, http.StatusNotFound, "user not found") return } assertion, err := h.passkeys.BeginLogin(&u) if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to begin login: "+err.Error()) return } resp.OK(c, gin.H{"assertion": assertion, "challenge": assertion.Response.Challenge, "user_id": u.ID}) return } assertion, err := h.passkeys.BeginDiscoverableLogin() if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to begin login: "+err.Error()) return } resp.OK(c, gin.H{"assertion": assertion, "challenge": assertion.Response.Challenge}) } // PasskeyLoginComplete POST /api/v1/webauthn/login/complete — 校验断言并发放令牌。 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 { resp.Fail(c, http.StatusBadRequest, "invalid input") return } var u *store.User if req.UserID > 0 { var su store.User if err := h.a.DB.First(&su, req.UserID).Error; err != nil { resp.Fail(c, http.StatusUnauthorized, "user not found") return } u = &su if err := h.passkeys.FinishLogin(u, req.Challenge, req.Credential); err != nil { resp.Fail(c, http.StatusUnauthorized, "passkey 校验失败: "+err.Error()) return } } else { var err error u, err = h.passkeys.FinishDiscoverableLogin(req.Challenge, req.Credential) if err != nil { resp.Fail(c, http.StatusUnauthorized, "passkey 校验失败: "+err.Error()) return } } if u.Status != store.UserStatusActive { resp.Fail(c, http.StatusForbidden, "user account disabled") return } access, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "access") if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to issue token") return } refresh, _, err := h.a.JWT.Sign(u.ID, u.Username, u.Role, "refresh") if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to issue token") return } h.setRefreshCookie(c, refresh) resp.OK(c, gin.H{ "access_token": access, "expires_in": int(h.a.JWT.AccessTTL().Seconds()), "user": h.publicUser(u), }) } // PasskeyList GET /api/v1/webauthn/passkeys — 当前用户的 passkey 列表。 func (h *Handler) PasskeyList(c *gin.Context) { u := sessionUser(c) pks, err := h.passkeys.List(u.ID) if err != nil { resp.Fail(c, http.StatusInternalServerError, "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}) } resp.OK(c, gin.H{"items": out}) } // PasskeyDelete DELETE /api/v1/webauthn/passkeys/:id — 解除绑定。 func (h *Handler) PasskeyDelete(c *gin.Context) { u := sessionUser(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { resp.Fail(c, http.StatusBadRequest, "invalid passkey id") return } if err := h.passkeys.Delete(u.ID, id); err != nil { resp.Fail(c, http.StatusNotFound, "passkey not found") return } resp.OK(c, gin.H{"ok": true}) }