Major rewrite of the Go backend to support:
- Three API format imports: openai, anthropic, compatible
- Three protocol conversions: Chat Completions, Responses, Messages
- Hub-and-spoke architecture with Chat as intermediate format
Deleted:
- opencat.go (old entry)
- store/, team/, pkg/team/, pkg/store/ (old data layer)
- internal/model/, internal/consts/ (old types)
- internal/service/team/, internal/controller/team/ (old handlers)
- llm/ (removed LLM client library, pure proxy mode)
- dist/, assets/ (old build artifacts)
Added:
- internal/store/ — 9 GORM models + multi-DB support
- internal/pkg/ — crypto (AES-GCM), apikey, jwt, ratelimit, resp, tokenizer
- internal/channel/ — channel selection, weighted LB, health checks
- internal/proxy/convert/ — 6 protocol conversion functions + SSE streaming
- internal/proxy/ — gateway with request dispatch and upstream selection
- internal/usage/ — async usage recorder with batch writes
- internal/api/ — management API (auth, users, keys, channels, models)
- Makefile for build/test/deploy
Fixed API to match frontend expectations:
- Login response wraps token in { data: { token } }
- GET /api/profile route added
- Profile response wraps user in { code, data }
- Role returned as number (10=admin, 1=user)
204 lines
5.4 KiB
Go
204 lines
5.4 KiB
Go
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
|
|
}
|