refactor: restructure passkey with dedicated package

- Create internal/passkey/ package with challenge session management
- Add HTTP handler layer in internal/api/passkey.go
- Simplify Passkey model to use blob credential storage
- Register passkey routes in router
- Update frontend to match new API endpoints
- Add .env.example template
This commit is contained in:
Sakurasan
2026-09-01 22:34:44 +08:00
parent d28fca8ee6
commit 9733b3c20b
11 changed files with 546 additions and 268 deletions
+4 -1
View File
@@ -3,6 +3,7 @@ package api
import (
"net/http"
"opencatd-open/internal/dao"
"opencatd-open/internal/passkey"
"opencatd-open/internal/store"
"opencatd-open/internal/pkg/apikey"
"opencatd-open/internal/pkg/crypto"
@@ -23,9 +24,10 @@ type Handler struct {
modelDAO *dao.ModelDAO
usageDAO *dao.UsageDAO
dailyDAO *dao.DailyUsageDAO
passkeys *passkey.Service
}
func NewHandler(db *gorm.DB) *Handler {
func NewHandler(db *gorm.DB, passkeys *passkey.Service) *Handler {
return &Handler{
db: db,
userDAO: dao.NewUserDAO(db),
@@ -34,6 +36,7 @@ func NewHandler(db *gorm.DB) *Handler {
modelDAO: dao.NewModelDAO(db),
usageDAO: dao.NewUsageDAO(db),
dailyDAO: dao.NewDailyUsageDAO(db),
passkeys: passkeys,
}
}
+162
View File
@@ -0,0 +1,162 @@
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}})
}
+13 -15
View File
@@ -8,23 +8,21 @@ import (
)
type Api struct {
cfg *config.Config
db *gorm.DB
userService *service.UserServiceImpl
tokenService *service.TokenServiceImpl
keyService *service.ApiKeyServiceImpl
webAuthService *service.WebAuthnService
usageService *service.UsageService
cfg *config.Config
db *gorm.DB
userService *service.UserServiceImpl
tokenService *service.TokenServiceImpl
keyService *service.ApiKeyServiceImpl
usageService *service.UsageService
}
func NewApi(cfg *config.Config, db *gorm.DB, userService *service.UserServiceImpl, tokenService *service.TokenServiceImpl, keyService *service.ApiKeyServiceImpl, webAuthService *service.WebAuthnService, usageService *service.UsageService) *Api {
func NewApi(cfg *config.Config, db *gorm.DB, userService *service.UserServiceImpl, tokenService *service.TokenServiceImpl, keyService *service.ApiKeyServiceImpl, usageService *service.UsageService) *Api {
return &Api{
cfg: cfg,
db: db,
userService: userService,
tokenService: tokenService,
keyService: keyService,
webAuthService: webAuthService,
usageService: usageService,
cfg: cfg,
db: db,
userService: userService,
tokenService: tokenService,
keyService: keyService,
usageService: usageService,
}
}
-11
View File
@@ -1,11 +0,0 @@
package dto
type Passkey struct {
ID int64 `json:"id" gorm:"column:id;primaryKey;autoIncrement"`
Name string `json:"name" gorm:"column:name"` // 凭证名称,用于用户识别不同的设备
SignCount uint32 `json:"sign_count" gorm:"column:sign_count"` // 签名计数器,用于防止重放攻击
DeviceType string `json:"device_type" gorm:"column:device_type"` // 设备类型,如"platform"或"cross-platform"
LastUsedAt int64 `json:"last_used_at" gorm:"column:last_used_at"` // 最后使用时间
CreatedAt int64 `json:"created_at,omitempty" gorm:"autoCreateTime"`
UpdatedAt int64 `json:"updated_at,omitempty" gorm:"autoUpdateTime"`
}
+275
View File
@@ -0,0 +1,275 @@
// Package passkey 封装 WebAuthn(passkey)注册与登录。
package passkey
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"sync"
"time"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"opencatd-open/internal/store"
"gorm.io/gorm"
)
type Config struct {
RPID string
Origins []string
Name string
}
// Service WebAuthn 服务:凭据存储 + challenge 会话(内存)。
type Service struct {
wa *webauthn.WebAuthn
db *gorm.DB
mu sync.Mutex
sessions map[string]webauthn.SessionData // keyed by challenge
}
func New(db *gorm.DB, cfg Config) (*Service, error) {
wa, err := webauthn.New(&webauthn.Config{
RPDisplayName: cfg.Name,
RPID: cfg.RPID,
RPOrigins: cfg.Origins,
})
if err != nil {
return nil, err
}
return &Service{wa: wa, db: db, sessions: map[string]webauthn.SessionData{}}, nil
}
// webUser 实现 go-webauthn 的 User 接口。
type webUser struct {
id uint64
name string
displayName string
credentials []webauthn.Credential
}
func (u *webUser) WebAuthnID() []byte { return []byte(strconv.FormatUint(u.id, 10)) }
func (u *webUser) WebAuthnName() string { return u.name }
func (u *webUser) WebAuthnDisplayName() string { return u.displayName }
func (u *webUser) WebAuthnIcon() string { return "" }
func (u *webUser) WebAuthnCredentials() []webauthn.Credential { return u.credentials }
func (s *Service) loadWebUser(u *store.User) (*webUser, error) {
var pks []store.Passkey
s.db.Where("user_id = ?", u.ID).Find(&pks)
creds := make([]webauthn.Credential, 0, len(pks))
for _, pk := range pks {
var c webauthn.Credential
if err := json.Unmarshal(pk.Credential, &c); err == nil {
creds = append(creds, c)
}
}
return &webUser{id: u.ID, name: u.Username, displayName: u.Username, credentials: creds}, nil
}
// GetUserByUsername 通过用户名或邮箱查找用户
func (s *Service) GetUserByUsername(username string) (*store.User, error) {
var u store.User
if err := s.db.Where("username = ? OR email = ?", username, username).First(&u).Error; err != nil {
return nil, err
}
return &u, nil
}
// GetUserByID 通过 ID 查找用户
func (s *Service) GetUserByID(id uint64) (*store.User, error) {
var u store.User
if err := s.db.First(&u, id).Error; err != nil {
return nil, err
}
return &u, nil
}
// ---------------------------------------------------------------------------
// 注册
// BeginRegistration 生成注册选项并暂存 challenge。
func (s *Service) BeginRegistration(u *store.User) (*protocol.CredentialCreation, error) {
wu, err := s.loadWebUser(u)
if err != nil {
return nil, err
}
creation, session, err := s.wa.BeginRegistration(wu)
if err != nil {
return nil, err
}
s.storeSession(session)
return creation, nil
}
// FinishRegistration 校验浏览器返回的凭据并落库。
func (s *Service) FinishRegistration(u *store.User, challenge string, body []byte, name string) error {
session, ok := s.takeSession(challenge)
if !ok {
return errors.New("challenge 已过期或不存在")
}
wu, err := s.loadWebUser(u)
if err != nil {
return err
}
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
cred, err := s.wa.FinishRegistration(wu, session, req)
if err != nil {
return err
}
raw, _ := json.Marshal(cred)
nm := name
if nm == "" {
nm = "passkey"
}
return s.db.Create(&store.Passkey{
UserID: u.ID, Name: nm, CredentialID: cred.ID, Credential: raw,
}).Error
}
// ---------------------------------------------------------------------------
// 登录
// BeginLogin 已知用户(按用户名)发起断言。
func (s *Service) BeginLogin(u *store.User) (*protocol.CredentialAssertion, error) {
wu, err := s.loadWebUser(u)
if err != nil {
return nil, err
}
assertion, session, err := s.wa.BeginLogin(wu)
if err != nil {
return nil, err
}
s.storeSession(session)
return assertion, nil
}
// BeginDiscoverableLogin 无用户名(使用平台/漫游器上的可发现凭据)。
func (s *Service) BeginDiscoverableLogin() (*protocol.CredentialAssertion, error) {
assertion, session, err := s.wa.BeginDiscoverableLogin()
if err != nil {
return nil, err
}
s.storeSession(session)
return assertion, nil
}
// FinishLogin 校验断言并更新签名计数。
func (s *Service) FinishLogin(u *store.User, challenge string, body []byte) error {
session, ok := s.takeSession(challenge)
if !ok {
return errors.New("challenge 已过期或不存在")
}
wu, err := s.loadWebUser(u)
if err != nil {
return err
}
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
cred, err := s.wa.FinishLogin(wu, session, req)
if err != nil {
return err
}
return s.updateCredential(u.ID, cred)
}
// FinishDiscoverableLogin 通过凭据定位用户并校验断言。
func (s *Service) FinishDiscoverableLogin(challenge string, body []byte) (*store.User, error) {
session, ok := s.takeSession(challenge)
if !ok {
return nil, errors.New("challenge 已过期或不存在")
}
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
// 先通过 credential_id 反查用户
var pks []store.Passkey
if err := s.db.Find(&pks).Error; err != nil {
return nil, err
}
// 遍历所有 passkey 找到匹配的
for _, pk := range pks {
var c webauthn.Credential
if err := json.Unmarshal(pk.Credential, &c); err != nil {
continue
}
// 尝试用这个用户的凭据进行登录
var u store.User
if err := s.db.First(&u, pk.UserID).Error; err != nil {
continue
}
wu, err := s.loadWebUser(&u)
if err != nil {
continue
}
cred, err := s.wa.FinishLogin(wu, session, req)
if err != nil {
// 不是这个用户的 passkey,继续尝试
session, _ = s.takeSession(challenge)
if !ok {
return nil, errors.New("challenge 已过期或不存在")
}
continue
}
_ = s.updateCredential(u.ID, cred)
return &u, nil
}
return nil, errors.New("no matching passkey found")
}
// ---------------------------------------------------------------------------
// 管理
// List 列出用户的 passkey。
func (s *Service) List(userID uint64) ([]store.Passkey, error) {
var pks []store.Passkey
err := s.db.Where("user_id = ?", userID).Order("id DESC").Find(&pks).Error
return pks, err
}
// Delete 删除用户的 passkey。
func (s *Service) Delete(userID, id uint64) error {
res := s.db.Where("id = ? AND user_id = ?", id, userID).Delete(&store.Passkey{})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (s *Service) updateCredential(userID uint64, cred *webauthn.Credential) error {
raw, _ := json.Marshal(cred)
return s.db.Model(&store.Passkey{}).
Where("user_id = ? AND credential_id = ?", userID, cred.ID).
Update("credential", raw).Error
}
// ---------------------------------------------------------------------------
// challenge 会话
func (s *Service) storeSession(session *webauthn.SessionData) {
s.mu.Lock()
s.sessions[session.Challenge] = *session
s.mu.Unlock()
}
func (s *Service) takeSession(challenge string) (webauthn.SessionData, bool) {
s.mu.Lock()
sess, ok := s.sessions[challenge]
if ok {
delete(s.sessions, challenge)
}
s.mu.Unlock()
// Expires 可能为零值:go-webauthn 默认 Enforce=false 不设过期时间。
// 零值时间恒早于 now,直接 After 会把每个 challenge 都判为过期,
// 与库内部一致,仅当显式设置了过期时间才做校验。
if ok && !sess.Expires.IsZero() && time.Now().After(sess.Expires) {
return webauthn.SessionData{}, false
}
return sess, ok
}
-203
View File
@@ -1,203 +0,0 @@
package service
import (
"encoding/base64"
"fmt"
"net/http"
"opencatd-open/internal/store"
"opencatd-open/pkg/config"
"strconv"
"strings"
"time"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"gorm.io/gorm"
)
type WebAuthnUser struct {
User *store.User
Credentials []webauthn.Credential
}
func (u *WebAuthnUser) WebAuthnID() []byte {
return []byte(strconv.FormatUint(u.User.ID, 10))
}
func (u *WebAuthnUser) WebAuthnName() string {
return u.User.Username
}
func (u *WebAuthnUser) WebAuthnDisplayName() string {
return u.User.Username
}
func (u *WebAuthnUser) WebAuthnCredentials() []webauthn.Credential {
return u.Credentials
}
func (u *WebAuthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) {
credentials := u.WebAuthnCredentials()
descriptors = make([]protocol.CredentialDescriptor, len(credentials))
for i, credential := range credentials {
descriptors[i] = credential.Descriptor()
}
return descriptors
}
type WebAuthnService struct {
cfg *config.Config
DB *gorm.DB
WebAuthn *webauthn.WebAuthn
}
func NewWebAuthnService(cfg *config.Config, db *gorm.DB) (*WebAuthnService, error) {
wconfig := &webauthn.Config{
RPDisplayName: cfg.AppName,
RPID: cfg.RPID,
RPOrigins: cfg.RPOrigins,
AuthenticatorSelection: protocol.AuthenticatorSelection{
RequireResidentKey: protocol.ResidentKeyRequired(),
ResidentKey: protocol.ResidentKeyRequirementRequired,
UserVerification: protocol.VerificationPreferred,
},
}
wa, err := webauthn.New(wconfig)
if err != nil {
return nil, err
}
return &WebAuthnService{
cfg: cfg,
DB: db,
WebAuthn: wa,
}, nil
}
func (s *WebAuthnService) GetUserWithCredentials(userID uint64) (*WebAuthnUser, error) {
var user store.User
if err := s.DB.First(&user, userID).Error; err != nil {
return nil, err
}
var passkeys []store.Passkey
if err := s.DB.Where("user_id = ?", userID).Find(&passkeys).Error; err != nil {
return nil, err
}
credentials := make([]webauthn.Credential, len(passkeys))
for i, pk := range passkeys {
credentialIDBytes, err := base64.StdEncoding.DecodeString(pk.CredentialID)
if err != nil {
return nil, fmt.Errorf("failed to decode CredentialID: %w", err)
}
publicKeyBytes, err := base64.StdEncoding.DecodeString(pk.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decode PublicKey: %w", err)
}
aaguidBytes, err := base64.StdEncoding.DecodeString(pk.AAGUID)
if err != nil {
return nil, fmt.Errorf("failed to decode AAGUID: %w", err)
}
var transport []protocol.AuthenticatorTransport
if pk.Transport != "" {
transport = []protocol.AuthenticatorTransport{protocol.AuthenticatorTransport(pk.Transport)}
}
credentials[i] = webauthn.Credential{
ID: credentialIDBytes,
PublicKey: publicKeyBytes,
AttestationType: pk.AttestationType,
Transport: transport,
Flags: webauthn.CredentialFlags{
UserPresent: true,
UserVerified: true,
BackupEligible: pk.BackupEligible,
BackupState: pk.BackupState,
},
Authenticator: webauthn.Authenticator{
AAGUID: aaguidBytes,
SignCount: uint32(pk.SignCount),
},
}
}
return &WebAuthnUser{
User: &user,
Credentials: credentials,
}, nil
}
func (s *WebAuthnService) BeginRegistration(userID uint64) (*protocol.CredentialCreation, error) {
user, err := s.GetUserWithCredentials(userID)
if err != nil {
return nil, err
}
options, _, err := s.WebAuthn.BeginRegistration(user)
if err != nil {
return nil, err
}
return options, nil
}
func (s *WebAuthnService) FinishRegistration(userID uint64, response *http.Request, deviceName string) (*store.Passkey, error) {
user, err := s.GetUserWithCredentials(userID)
if err != nil {
return nil, err
}
credential, err := s.WebAuthn.FinishRegistration(user, webauthn.SessionData{}, response)
if err != nil {
return nil, err
}
var transport string
if len(credential.Transport) > 0 {
transport = string(credential.Transport[0])
}
passkey := &store.Passkey{
UserID: userID,
CredentialID: base64.StdEncoding.EncodeToString(credential.ID),
PublicKey: base64.StdEncoding.EncodeToString(credential.PublicKey),
AttestationType: string(credential.AttestationType),
AAGUID: base64.StdEncoding.EncodeToString(credential.Authenticator.AAGUID),
SignCount: uint64(credential.Authenticator.SignCount),
Name: deviceName,
DeviceType: strings.TrimSpace(fmt.Sprintf("%s", deviceName)),
LastUsedAt: time.Now().Unix(),
BackupEligible: credential.Flags.BackupEligible,
BackupState: credential.Flags.BackupState,
Transport: transport,
}
if err := s.DB.Create(passkey).Error; err != nil {
return nil, err
}
return passkey, nil
}
func (s *WebAuthnService) BeginLogin() (*protocol.CredentialAssertion, error) {
options, _, err := s.WebAuthn.BeginDiscoverableLogin()
if err != nil {
return nil, err
}
return options, nil
}
func (s *WebAuthnService) ListPasskeys(userID uint64) ([]store.Passkey, error) {
var passkeys []store.Passkey
if err := s.DB.Where("user_id = ?", userID).Find(&passkeys).Error; err != nil {
return nil, err
}
return passkeys, nil
}
func (s *WebAuthnService) DeletePasskey(userID uint64, passkeyID uint64) error {
return s.DB.Where("id = ? AND user_id = ?", passkeyID, userID).Delete(&store.Passkey{}).Error
}
+2 -10
View File
@@ -197,16 +197,8 @@ type Passkey struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID uint64 `gorm:"index;not null" json:"user_id"`
Name string `gorm:"size:64" json:"name"`
CredentialID string `gorm:"size:255;not null" json:"-"`
PublicKey string `gorm:"size:512;not null" json:"-"`
AttestationType string `gorm:"size:64" json:"-"`
AAGUID string `gorm:"size:64" json:"-"`
SignCount uint64 `json:"-"`
DeviceType string `gorm:"size:255" json:"device_type,omitempty"`
LastUsedAt int64 `json:"last_used_at,omitempty"`
BackupEligible bool `json:"-"`
BackupState bool `json:"-"`
Transport string `gorm:"size:32" json:"-"`
CredentialID []byte `gorm:"size:255;not null" json:"-"`
Credential []byte `gorm:"type:blob;not null" json:"-"`
CreatedAt time.Time `json:"created_at"`
}
+21 -1
View File
@@ -10,6 +10,7 @@ import (
"opencatd-open/internal/api"
"opencatd-open/internal/channel"
"opencatd-open/internal/dao"
"opencatd-open/internal/passkey"
"opencatd-open/internal/proxy"
"opencatd-open/internal/usage"
"opencatd-open/middleware"
@@ -61,8 +62,19 @@ func SetRouter(cfg *config.Config, db *gorm.DB, web *embed.FS) {
gateway := proxy.NewGateway(ctx, cfg, db, &wg, userDAO, apiKeyDAO, usageDAO, dailyDAO)
gateway.SetChannelService(channelSvc)
gateway.SetUsageRecorder(usageRecorder)
// Initialize passkey service
passkeySvc, err := passkey.New(db, passkey.Config{
RPID: cfg.RPID,
Origins: cfg.RPOrigins,
Name: cfg.AppName,
})
if err != nil {
log.Fatalf("Failed to initialize passkey service: %v", err)
}
// Initialize API handler
apiHandler := api.NewHandler(db)
apiHandler := api.NewHandler(db, passkeySvc)
r := gin.Default()
r.Use(middleware.CORS())
@@ -72,6 +84,8 @@ func SetRouter(cfg *config.Config, db *gorm.DB, web *embed.FS) {
{
public.POST("/register", apiHandler.Register)
public.POST("/login", apiHandler.Login)
public.POST("/passkey/begin", apiHandler.PasskeyLoginBegin)
public.POST("/passkey/finish", apiHandler.PasskeyLoginComplete)
}
// API routes (authenticated)
@@ -83,6 +97,12 @@ func SetRouter(cfg *config.Config, db *gorm.DB, web *embed.FS) {
apiGroup.POST("/profile/update", apiHandler.UpdateProfile)
apiGroup.POST("/profile/update/password", apiHandler.UpdatePassword)
// Passkey management
apiGroup.POST("/webauthn/register/begin", apiHandler.PasskeyRegisterBegin)
apiGroup.POST("/webauthn/register/complete", apiHandler.PasskeyRegisterComplete)
apiGroup.GET("/webauthn/passkeys", apiHandler.PasskeyList)
apiGroup.DELETE("/webauthn/passkeys/:id", apiHandler.PasskeyDelete)
// User management (admin)
apiGroup.GET("/users", apiHandler.ListUsers)
apiGroup.GET("/users/:id", apiHandler.GetUser)