Details (original message): Add dual-tier storage (embedded SQLite + optional PostgreSQL), tenant owner scoping, API key auth, versioned dual-dialect migrations, integrity hardening, and batched event persistence
147 lines
4.2 KiB
Go
147 lines
4.2 KiB
Go
package store
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"database/sql"
|
||
"encoding/hex"
|
||
"errors"
|
||
"time"
|
||
)
|
||
|
||
// ---- 认证与租户(阶段 1:API key 认证,user 即租户 owner)----
|
||
// key 明文仅创建时返回一次;库中只存 SHA-256 摘要(数据可靠性标准:不落明文凭证)。
|
||
|
||
// User 是租户账户(owner 全局唯一;id 供后续扩展用户目录)。
|
||
type User struct {
|
||
ID string `json:"id"`
|
||
Owner string `json:"owner"`
|
||
Name string `json:"name"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
}
|
||
|
||
// APIKeyInfo 是密钥的展示视图(不含摘要)。
|
||
type APIKeyInfo struct {
|
||
Name string `json:"name"`
|
||
Owner string `json:"owner"`
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||
Revoked bool `json:"revoked"`
|
||
}
|
||
|
||
func hashKey(plain string) string {
|
||
sum := sha256.Sum256([]byte(plain))
|
||
return hex.EncodeToString(sum[:])
|
||
}
|
||
|
||
// EnsureUser 幂等创建租户账户(owner 唯一)。
|
||
func (s *Store) EnsureUser(owner, name string) error {
|
||
_, err := s.db.Exec(s.q(`INSERT INTO users(id,owner,name,created_at) VALUES(?,?,?,?)
|
||
ON CONFLICT(owner) DO NOTHING`), newID("usr"), owner, name, nowISO())
|
||
return err
|
||
}
|
||
|
||
func (s *Store) GetUser(owner string) (User, error) {
|
||
var u User
|
||
var ca string
|
||
err := s.db.QueryRow(s.q(`SELECT id,owner,name,created_at FROM users WHERE owner=?`), owner).Scan(&u.ID, &u.Owner, &u.Name, &ca)
|
||
if errors.Is(err, sql.ErrNoRows) {
|
||
return u, ErrNotFound
|
||
}
|
||
if err != nil {
|
||
return u, err
|
||
}
|
||
u.CreatedAt, _ = parseTime(ca)
|
||
return u, nil
|
||
}
|
||
|
||
func (s *Store) ListUsers() ([]User, error) {
|
||
rows, err := s.db.Query(s.q(`SELECT id,owner,name,created_at FROM users ORDER BY created_at`))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
var out []User
|
||
for rows.Next() {
|
||
var u User
|
||
var ca string
|
||
if err := rows.Scan(&u.ID, &u.Owner, &u.Name, &ca); err != nil {
|
||
return nil, err
|
||
}
|
||
u.CreatedAt, _ = parseTime(ca)
|
||
out = append(out, u)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
// CreateAPIKey 创建密钥:明文 `ask_<hex32>` 仅此一次返回。
|
||
func (s *Store) CreateAPIKey(owner, name string) (string, error) {
|
||
if err := s.EnsureUser(owner, ""); err != nil {
|
||
return "", err
|
||
}
|
||
raw := make([]byte, 16)
|
||
if _, err := rand.Read(raw); err != nil {
|
||
return "", err
|
||
}
|
||
plain := "ask_" + hex.EncodeToString(raw)
|
||
if name == "" {
|
||
name = "key-" + nowISO()[:10]
|
||
}
|
||
_, err := s.db.Exec(s.q(`INSERT INTO api_keys(key_hash,owner,name,created_at,revoked) VALUES(?,?,?,?,0)`),
|
||
hashKey(plain), owner, name, nowISO())
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return plain, nil
|
||
}
|
||
|
||
// RevokeAPIKeyByName 撤销指定名称的未撤销密钥(返回是否命中)。
|
||
func (s *Store) RevokeAPIKeyByName(owner, name string) (bool, error) {
|
||
res, err := s.db.Exec(s.q(`UPDATE api_keys SET revoked=1 WHERE owner=? AND name=? AND revoked=0`), owner, name)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
n, _ := res.RowsAffected()
|
||
return n > 0, nil
|
||
}
|
||
|
||
func (s *Store) ListAPIKeys(owner string) ([]APIKeyInfo, error) {
|
||
rows, err := s.db.Query(s.q(`SELECT name,owner,created_at,last_used_at,revoked FROM api_keys WHERE owner=? ORDER BY created_at`), owner)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
var out []APIKeyInfo
|
||
for rows.Next() {
|
||
var k APIKeyInfo
|
||
var ca string
|
||
var last sql.NullString
|
||
if err := rows.Scan(&k.Name, &k.Owner, &ca, &last, &k.Revoked); err != nil {
|
||
return nil, err
|
||
}
|
||
k.CreatedAt, _ = parseTime(ca)
|
||
if last.Valid && last.String != "" {
|
||
t, _ := parseTime(last.String)
|
||
k.LastUsedAt = &t
|
||
}
|
||
out = append(out, k)
|
||
}
|
||
return out, rows.Err()
|
||
}
|
||
|
||
// AuthOwner 校验 API key:命中返回 owner;未命中/已撤销返回 false。
|
||
// last_used_at 尽力刷新(失败不影响认证结果)。
|
||
func (s *Store) AuthOwner(plainKey string) (string, bool) {
|
||
if plainKey == "" {
|
||
return "", false
|
||
}
|
||
var owner string
|
||
var revoked int
|
||
err := s.db.QueryRow(s.q(`SELECT owner,revoked FROM api_keys WHERE key_hash=?`), hashKey(plainKey)).Scan(&owner, &revoked)
|
||
if err != nil || revoked == 1 {
|
||
return "", false
|
||
}
|
||
s.db.Exec(s.q(`UPDATE api_keys SET last_used_at=? WHERE key_hash=?`), nowISO(), hashKey(plainKey))
|
||
return owner, true
|
||
}
|