Files
RawMark/internal/store/store.go
T

170 lines
5.4 KiB
Go

// Package store 保存应用自身的数据(设置、会话、API 令牌)。笔记内容不在这里,见 vault。
package store
import (
"database/sql"
"errors"
"rawmark/internal/i18n"
"time"
_ "modernc.org/sqlite"
)
type Store struct{ db *sql.DB }
const schema = `
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
token_hash TEXT PRIMARY KEY,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS resource_tickets (
token_hash TEXT PRIMARY KEY,
session_hash TEXT NOT NULL,
kind TEXT NOT NULL,
resource TEXT NOT NULL,
revision TEXT NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS resource_tickets_expiry ON resource_tickets(expires_at);
CREATE TABLE IF NOT EXISTS api_tokens (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
scope TEXT NOT NULL,
created_at INTEGER NOT NULL,
last_used_at INTEGER NOT NULL DEFAULT 0
);
`
func Open(path string) (*Store, error) {
db, err := sql.Open("sqlite", "file:"+path+"?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)")
if err != nil {
return nil, err
}
if _, err := db.Exec(schema); err != nil {
db.Close()
return nil, i18n.New("could_not_initialize_database_w_208f9e", err)
}
return &Store{db: db}, nil
}
func (s *Store) Close() error { return s.db.Close() }
// Setting 读取设置项;不存在时 ok 为 false
func (s *Store) Setting(key string) (value string, ok bool, err error) {
err = s.db.QueryRow(`SELECT value FROM settings WHERE key = ?`, key).Scan(&value)
if errors.Is(err, sql.ErrNoRows) {
return "", false, nil
}
return value, err == nil, err
}
func (s *Store) SetSetting(key, value string) error {
_, err := s.db.Exec(`INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, key, value)
return err
}
// SetSettingIfAbsent 仅在设置项不存在时写入,返回是否写入成功(用于首次初始化,防止并发重复设置)
func (s *Store) SetSettingIfAbsent(key, value string) (bool, error) {
res, err := s.db.Exec(`INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO NOTHING`, key, value)
if err != nil {
return false, err
}
n, err := res.RowsAffected()
return n == 1, err
}
func (s *Store) CreateSession(tokenHash string, expires time.Time) error {
_, err := s.db.Exec(`INSERT INTO sessions (token_hash, created_at, expires_at) VALUES (?, ?, ?)`,
tokenHash, time.Now().Unix(), expires.Unix())
return err
}
// SessionExpiry 只读取有效会话,不延长固定有效期。
func (s *Store) SessionExpiry(tokenHash string, now time.Time) (time.Time, bool, error) {
var expires int64
err := s.db.QueryRow(`SELECT expires_at FROM sessions WHERE token_hash = ? AND expires_at > ?`, tokenHash, now.Unix()).Scan(&expires)
if errors.Is(err, sql.ErrNoRows) {
return time.Time{}, false, nil
}
return time.Unix(expires, 0).UTC(), err == nil, err
}
func (s *Store) DeleteSession(tokenHash string) error {
_, err := s.db.Exec(`DELETE FROM sessions WHERE token_hash = ?`, tokenHash)
return err
}
// DeleteOtherSessions 删除除 keepHash 之外的所有会话(修改密码后让其他设备重新登录)
func (s *Store) DeleteOtherSessions(keepHash string) error {
_, err := s.db.Exec(`DELETE FROM sessions WHERE token_hash <> ?`, keepHash)
return err
}
func (s *Store) DeleteExpiredSessions() error {
_, err := s.db.Exec(`DELETE FROM sessions WHERE expires_at <= ?`, time.Now().Unix())
if err != nil {
return err
}
_, err = s.db.Exec(`DELETE FROM resource_tickets WHERE expires_at <= ? OR NOT EXISTS (SELECT 1 FROM sessions WHERE sessions.token_hash = resource_tickets.session_hash)`, time.Now().Unix())
return err
}
// ---- API 令牌 ----
// APIToken 为自动化调用的访问令牌,只保存哈希。Scope 为 read 或 write
type APIToken struct {
ID string `json:"id"`
Name string `json:"name"`
Hash string `json:"-"`
Scope string `json:"scope"`
CreatedAt int64 `json:"createdAt"`
LastUsedAt int64 `json:"lastUsedAt"`
}
func (s *Store) CreateAPIToken(t APIToken) error {
_, err := s.db.Exec(`INSERT INTO api_tokens (id, name, token_hash, scope, created_at) VALUES (?, ?, ?, ?, ?)`,
t.ID, t.Name, t.Hash, t.Scope, t.CreatedAt)
return err
}
func (s *Store) APITokens() ([]APIToken, error) {
rows, err := s.db.Query(`SELECT id, name, scope, created_at, last_used_at FROM api_tokens ORDER BY created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
out := []APIToken{}
for rows.Next() {
var t APIToken
if err := rows.Scan(&t.ID, &t.Name, &t.Scope, &t.CreatedAt, &t.LastUsedAt); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
// UseAPIToken 按哈希查找令牌并记录使用时间;不存在时 ok 为 false
func (s *Store) UseAPIToken(hash string, now int64) (scope string, ok bool, err error) {
err = s.db.QueryRow(`UPDATE api_tokens SET last_used_at = ? WHERE token_hash = ? RETURNING scope`, now, hash).Scan(&scope)
if errors.Is(err, sql.ErrNoRows) {
return "", false, nil
}
return scope, err == nil, err
}
func (s *Store) DeleteAPIToken(id string) (bool, error) {
res, err := s.db.Exec(`DELETE FROM api_tokens WHERE id = ?`, id)
if err != nil {
return false, err
}
n, err := res.RowsAffected()
return n == 1, err
}