feat(server): add Go backend with vault, auth, events and REST API
- vault: path validation (no traversal, hidden segments or symlink escape), atomic writes with If-Match/If-None-Match revisions, moves that carry the note's .assets folder and rewrite its image links, trash with restore, content-addressed asset uploads - auth: single-user Argon2id password, sliding sessions in SQLite, login rate limiting, first-run setup gated by a one-time code written to the data directory - watch: fsnotify plus periodic rescans diffed against a snapshot, so both RawMark and external edits are broadcast as tree/changed/moved/ deleted events over WebSocket - api: stdlib router, SameSite=Strict cookies plus Origin checks, sandboxed raw file serving, embedded SPA with gzip and CSP
This commit is contained in:
1 parent
5746f2e9ae
commit
c4d2bd4636
15 files changed
+2196
No files matched your search
@@ -0,0 +1,95 @@
|
||||
// Package store 保存应用自身的数据(设置、会话)。笔记内容不在这里,见 vault。
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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
|
||||
);
|
||||
`
|
||||
|
||||
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, fmt.Errorf("初始化数据库失败: %w", 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
|
||||
}
|
||||
|
||||
// SessionValid 校验会话未过期,并顺延有效期(滑动过期)
|
||||
func (s *Store) SessionValid(tokenHash string, extendTo time.Time) (bool, error) {
|
||||
res, err := s.db.Exec(`UPDATE sessions SET expires_at = ? WHERE token_hash = ? AND expires_at > ?`,
|
||||
extendTo.Unix(), tokenHash, time.Now().Unix())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
return n == 1, err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSession(tokenHash string) error {
|
||||
_, err := s.db.Exec(`DELETE FROM sessions WHERE token_hash = ?`, tokenHash)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteAllSessions() error {
|
||||
_, err := s.db.Exec(`DELETE FROM sessions`)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteExpiredSessions() error {
|
||||
_, err := s.db.Exec(`DELETE FROM sessions WHERE expires_at <= ?`, time.Now().Unix())
|
||||
return err
|
||||
}
|
||||
Reference in new issue
Block a user