250 lines
6.5 KiB
Go
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
|
|
})
|
|
}
|