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 }) }