Files
GovAI/server/internal/service/twofa/store.go
T
freedakgmail c949204662 feat(govai): 0617 优化首批 — 安全/私有化/深度研究/服务层/可观测性
借鉴 odysseus 的能力设计,全程净室实现、零 AGPL 代码、不引入 AGPL 依赖。

T1 提示注入防护: pkg/promptguard 包裹外部/知识库内容为不可信数据,buildMessages 移出 system 指令区。 T2 安全 CI: .github/workflows(ci+security: govulncheck/gitleaks/actionlint/hadolint/trivy)+dependabot+.hadolint.yaml;go.mod 加 toolchain go1.25.11 修复 20 个 stdlib CVE。 T3 管理员 2FA: 迁移 000016 + RFC6238 TOTP/备份码(pkg/auth, 零依赖) + 登录流程集成(后端)。 T4 本地模型: LLM/embedding 支持本地 vLLM/Ollama(OpenAI 兼容, 鉴权头条件发送, NoAuth) + docs/local-deploy.md。 T6 深度研究: 迁移 000017 + Python research-worker(净室多步流水线, 检索避开 SearXNG) + Go research 服务/handler/路由。 T7 service 层: 新增 internal/service/{research,twofa}, 2FA 业务逻辑从胖 handler 下沉, 接口注入可单测。 T10 缓存/可观测性: internal/cache(Redis+内存, 优雅降级) 接入 store 热点列表; Prometheus 指标+/metrics; docs/openapi.yaml。 验证: go build/vet/test ./... 全绿(8 包); research-worker 12 单测过; 真实 PG 应用迁移并烟测。
2026-06-17 17:52:47 +08:00

122 lines
3.3 KiB
Go

package twofa
import (
"context"
"github.com/enterprise-ai-platform/server/pkg/auth"
"github.com/jackc/pgx/v5/pgxpool"
)
// pgxStore 基于 pgx 连接池的 Store 实现。
type pgxStore struct {
pool *pgxpool.Pool
}
func NewPgxStore(pool *pgxpool.Pool) Store {
return &pgxStore{pool: pool}
}
func (s *pgxStore) GetStatus(ctx context.Context, userID string) (bool, int, error) {
var enabled bool
var remaining int
err := s.pool.QueryRow(ctx,
`SELECT u.totp_enabled,
(SELECT COUNT(*) FROM user_backup_codes b WHERE b.user_id = u.id AND b.used_at IS NULL)
FROM users u WHERE u.id = $1`, userID).Scan(&enabled, &remaining)
return enabled, remaining, err
}
func (s *pgxStore) GetSecret(ctx context.Context, userID string) (string, error) {
var secret *string
if err := s.pool.QueryRow(ctx,
`SELECT totp_secret FROM users WHERE id = $1`, userID).Scan(&secret); err != nil {
return "", err
}
if secret == nil {
return "", nil
}
return *secret, nil
}
func (s *pgxStore) GetSecretAndEnabled(ctx context.Context, userID string) (string, bool, error) {
var secret *string
var enabled bool
if err := s.pool.QueryRow(ctx,
`SELECT totp_secret, totp_enabled FROM users WHERE id = $1`, userID).Scan(&secret, &enabled); err != nil {
return "", false, err
}
if secret == nil {
return "", enabled, nil
}
return *secret, enabled, nil
}
func (s *pgxStore) SaveEnrollment(ctx context.Context, userID, secret string, codeHashes []string) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx,
`UPDATE users SET totp_secret = $2, totp_enabled = false WHERE id = $1`, userID, secret); err != nil {
return err
}
if _, err = tx.Exec(ctx, `DELETE FROM user_backup_codes WHERE user_id = $1`, userID); err != nil {
return err
}
for _, h := range codeHashes {
if _, err = tx.Exec(ctx,
`INSERT INTO user_backup_codes (user_id, code_hash) VALUES ($1, $2)`, userID, h); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (s *pgxStore) Enable(ctx context.Context, userID string) error {
_, err := s.pool.Exec(ctx, `UPDATE users SET totp_enabled = true WHERE id = $1`, userID)
return err
}
func (s *pgxStore) Disable(ctx context.Context, userID string) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err = tx.Exec(ctx,
`UPDATE users SET totp_enabled = false, totp_secret = NULL WHERE id = $1`, userID); err != nil {
return err
}
if _, err = tx.Exec(ctx, `DELETE FROM user_backup_codes WHERE user_id = $1`, userID); err != nil {
return err
}
return tx.Commit(ctx)
}
func (s *pgxStore) ConsumeBackupCode(ctx context.Context, userID, code string) (bool, error) {
rows, err := s.pool.Query(ctx,
`SELECT id, code_hash FROM user_backup_codes WHERE user_id = $1 AND used_at IS NULL`, userID)
if err != nil {
return false, err
}
type bc struct{ id, hash string }
var list []bc
for rows.Next() {
var x bc
if rows.Scan(&x.id, &x.hash) == nil {
list = append(list, x)
}
}
rows.Close() // 先释放连接再执行更新
for _, x := range list {
if auth.CheckBackupCode(code, x.hash) {
_, _ = s.pool.Exec(ctx, `UPDATE user_backup_codes SET used_at = NOW() WHERE id = $1`, x.id)
return true, nil
}
}
return false, nil
}