Files
NixMsg/internal/admin/store.go
T

250 lines
6.5 KiB
Go

package admin
import (
"context"
"database/sql"
"encoding/hex"
"time"
"git.asio.asia/nixevol/NixMsg/internal/auth"
"git.asio.asia/nixevol/NixMsg/internal/store"
)
type apiTokenRow struct {
ID string
Name string
Enabled bool
CreatedAt time.Time
LastUsedAt *time.Time
}
func (h *Handler) getAdminPasswordHash(ctx context.Context) (string, error) {
var v string
err := h.db.Read.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, settingAdminHash).Scan(&v)
if err != nil {
return "", err
}
return v, nil
}
func (h *Handler) setAdminPasswordHash(ctx context.Context, phc string) error {
now := time.Now().UnixMilli()
return h.db.Queue.Do(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(
`INSERT INTO settings(key, value, updated_at) VALUES(?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
settingAdminHash, phc, now,
)
return err
})
}
func (h *Handler) createSession(ctx context.Context, hashHex string, ttl time.Duration) error {
now := time.Now()
return h.db.Queue.Do(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(
`INSERT INTO admin_sessions(token_hash, created_at, expires_at) VALUES(?, ?, ?)`,
hashHex, now.UnixMilli(), now.Add(ttl).UnixMilli(),
)
return err
})
}
func (h *Handler) sessionValid(ctx context.Context, hashHex string) (bool, error) {
var expires int64
err := h.db.Read.QueryRowContext(ctx,
`SELECT expires_at FROM admin_sessions WHERE token_hash = ?`, hashHex,
).Scan(&expires)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return time.Now().UnixMilli() < expires, nil
}
func (h *Handler) deleteSession(ctx context.Context, hashHex string) error {
return h.db.Queue.Do(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(`DELETE FROM admin_sessions WHERE token_hash = ?`, hashHex)
return err
})
}
func (h *Handler) deleteOtherSessions(ctx context.Context, keepHashHex string) error {
return h.db.Queue.Do(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(`DELETE FROM admin_sessions WHERE token_hash != ?`, keepHashHex)
return err
})
}
func (h *Handler) lookupAPIToken(ctx context.Context, hash []byte) (apiTokenRow, error) {
hashHex := hex.EncodeToString(hash)
var (
id, name string
enabled int
created, last sql.NullInt64
)
err := h.db.Read.QueryRowContext(ctx,
`SELECT id, name, enabled, created_at, last_used_at FROM api_tokens WHERE token_hash = ?`,
hashHex,
).Scan(&id, &name, &enabled, &created, &last)
if err != nil {
return apiTokenRow{}, err
}
row := apiTokenRow{
ID: id,
Name: name,
Enabled: enabled != 0,
}
if created.Valid {
row.CreatedAt = time.UnixMilli(created.Int64)
}
if last.Valid {
t := time.UnixMilli(last.Int64)
row.LastUsedAt = &t
}
return row, nil
}
func (h *Handler) listAPITokens(ctx context.Context) ([]apiTokenRow, error) {
rows, err := h.db.Read.QueryContext(ctx,
`SELECT id, name, enabled, created_at, last_used_at FROM api_tokens ORDER BY created_at ASC`,
)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
var out []apiTokenRow
for rows.Next() {
var (
id, name string
enabled int
created, last sql.NullInt64
)
if err := rows.Scan(&id, &name, &enabled, &created, &last); err != nil {
return nil, err
}
row := apiTokenRow{ID: id, Name: name, Enabled: enabled != 0}
if created.Valid {
row.CreatedAt = time.UnixMilli(created.Int64)
}
if last.Valid {
t := time.UnixMilli(last.Int64)
row.LastUsedAt = &t
}
out = append(out, row)
}
return out, rows.Err()
}
func (h *Handler) insertAPIToken(ctx context.Context, id, name string, hash []byte) (time.Time, error) {
now := time.Now()
hashHex := hex.EncodeToString(hash)
err := h.db.Queue.Do(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(
`INSERT INTO api_tokens(id, name, token_hash, enabled, created_at, last_used_at) VALUES(?, ?, ?, 1, ?, NULL)`,
id, name, hashHex, now.UnixMilli(),
)
return err
})
return now, err
}
func (h *Handler) getAPITokenByID(ctx context.Context, id string) (apiTokenRow, error) {
var (
name string
enabled int
created, last sql.NullInt64
)
err := h.db.Read.QueryRowContext(ctx,
`SELECT name, enabled, created_at, last_used_at FROM api_tokens WHERE id = ?`, id,
).Scan(&name, &enabled, &created, &last)
if err != nil {
return apiTokenRow{}, err
}
row := apiTokenRow{ID: id, Name: name, Enabled: enabled != 0}
if created.Valid {
row.CreatedAt = time.UnixMilli(created.Int64)
}
if last.Valid {
t := time.UnixMilli(last.Int64)
row.LastUsedAt = &t
}
return row, nil
}
func (h *Handler) updateAPIToken(ctx context.Context, id string, name *string, enabled *bool) error {
return h.db.Queue.Do(ctx, func(tx *sql.Tx) error {
var (
curName string
curEn int
)
if err := tx.QueryRow(`SELECT name, enabled FROM api_tokens WHERE id = ?`, id).Scan(&curName, &curEn); err != nil {
return err
}
newName := curName
newEn := curEn
if name != nil {
newName = *name
}
if enabled != nil {
if *enabled {
newEn = 1
} else {
newEn = 0
}
}
_, err := tx.Exec(`UPDATE api_tokens SET name = ?, enabled = ? WHERE id = ?`, newName, newEn, id)
return err
})
}
func (h *Handler) deleteAPIToken(ctx context.Context, id string) error {
return h.db.Queue.Do(ctx, func(tx *sql.Tx) error {
res, err := tx.Exec(`DELETE FROM api_tokens WHERE id = ?`, id)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return sql.ErrNoRows
}
return nil
})
}
func (h *Handler) touchLastUsed(ctx context.Context, id string) {
now := time.Now()
h.lastUsedMu.Lock()
prev, ok := h.lastUsed[id]
if ok && now.Sub(prev) < lastUsedMinGap {
h.lastUsedMu.Unlock()
return
}
h.lastUsed[id] = now
h.lastUsedMu.Unlock()
_ = h.db.Queue.Do(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(`UPDATE api_tokens SET last_used_at = ? WHERE id = ?`, now.UnixMilli(), id)
return err
})
}
// SeedAdminPassword 写入管理员密码哈希(测试与接线辅助);走 HashPool。
func SeedAdminPassword(ctx context.Context, db *store.DB, hash auth.HashPool, password string) error {
phc, err := hash.Hash(ctx, auth.PasswordAdmin, password)
if err != nil {
return err
}
now := time.Now().UnixMilli()
return db.Queue.Do(ctx, func(tx *sql.Tx) error {
_, err := tx.Exec(
`INSERT INTO settings(key, value, updated_at) VALUES(?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
settingAdminHash, phc, now,
)
return err
})
}