feat: 实现管理鉴权、API 令牌与操作日志
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user