Update application UI and functionality
This commit is contained in:
@@ -46,7 +46,7 @@ func (s *Store) VerifyAdminPassword(ctx context.Context, username, password stri
|
||||
if username == "" {
|
||||
username = "admin"
|
||||
}
|
||||
user, ok, err := s.verifyAdminPasswordOn(s.localDB, s.localDialect, username, password)
|
||||
user, ok, err := s.verifyAdminPasswordOnContext(ctx, s.localDB, s.localDialect, username, password)
|
||||
if err == nil && (ok || user.Username != "") {
|
||||
return user, ok, nil
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (s *Store) VerifyAdminPassword(ctx context.Context, username, password stri
|
||||
remote, remoteDialect := s.remoteDB, s.remoteDialect
|
||||
s.mu.RUnlock()
|
||||
if remote != nil && remote != s.localDB {
|
||||
user, ok, err := s.verifyAdminPasswordOn(remote, remoteDialect, username, password)
|
||||
user, ok, err := s.verifyAdminPasswordOnContext(ctx, remote, remoteDialect, username, password)
|
||||
if err != nil {
|
||||
s.markFailover(err)
|
||||
}
|
||||
@@ -67,12 +67,16 @@ func (s *Store) VerifyAdminPassword(ctx context.Context, username, password stri
|
||||
}
|
||||
|
||||
func (s *Store) verifyAdminPasswordOn(conn *sql.DB, d dialect, username, password string) (AdminUser, bool, error) {
|
||||
return s.verifyAdminPasswordOnContext(context.Background(), conn, d, username, password)
|
||||
}
|
||||
|
||||
func (s *Store) verifyAdminPasswordOnContext(ctx context.Context, conn *sql.DB, d dialect, username, password string) (AdminUser, bool, error) {
|
||||
if conn == nil {
|
||||
return AdminUser{}, false, errors.New("database is not available")
|
||||
}
|
||||
var row adminRow
|
||||
var changed int
|
||||
err := conn.QueryRow(d.rebind(`SELECT id, username, password_hash, password_changed, created_at, updated_at FROM admin_users WHERE username = ?`), username).
|
||||
err := conn.QueryRowContext(ctx, d.rebind(`SELECT id, username, password_hash, password_changed, created_at, updated_at FROM admin_users WHERE username = ?`), username).
|
||||
Scan(&row.ID, &row.Username, &row.PasswordHash, &changed, &row.CreatedAt, &row.UpdatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return AdminUser{}, false, nil
|
||||
@@ -104,12 +108,12 @@ func (s *Store) ChangeAdminPasswordWithWarning(ctx context.Context, username, cu
|
||||
return "", err
|
||||
}
|
||||
username = firstNonEmpty(strings.TrimSpace(username), "admin")
|
||||
_, ok, err := s.verifyAdminPasswordOn(s.localDB, s.localDialect, username, current)
|
||||
_, ok, err := s.verifyAdminPasswordOnContext(ctx, s.localDB, s.localDialect, username, current)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok {
|
||||
remoteOK, remoteErr := s.verifyRemoteAdminPassword(username, current)
|
||||
remoteOK, remoteErr := s.verifyRemoteAdminPassword(ctx, username, current)
|
||||
if remoteErr != nil {
|
||||
s.markFailover(remoteErr)
|
||||
}
|
||||
@@ -150,14 +154,14 @@ func validateAdminPasswordChange(current, next string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) verifyRemoteAdminPassword(username, password string) (bool, error) {
|
||||
func (s *Store) verifyRemoteAdminPassword(ctx context.Context, username, password string) (bool, error) {
|
||||
s.mu.RLock()
|
||||
remote, remoteDialect := s.remoteDB, s.remoteDialect
|
||||
s.mu.RUnlock()
|
||||
if remote == nil || remote == s.localDB {
|
||||
return false, nil
|
||||
}
|
||||
_, ok, err := s.verifyAdminPasswordOn(remote, remoteDialect, username, password)
|
||||
_, ok, err := s.verifyAdminPasswordOnContext(ctx, remote, remoteDialect, username, password)
|
||||
return ok, err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -170,10 +171,18 @@ func (s *Store) RecentSourceCalls(limit int) ([]map[string]any, error) {
|
||||
}
|
||||
|
||||
func (s *Store) InsertAudit(log AuditLog) error {
|
||||
return s.InsertAuditContext(context.Background(), log)
|
||||
}
|
||||
|
||||
func (s *Store) InsertAuditContext(ctx context.Context, log AuditLog) error {
|
||||
if log.CreatedAt == "" {
|
||||
log.CreatedAt = Now()
|
||||
}
|
||||
_, err := s.exec(`INSERT INTO audit_logs (actor, type, target, message, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
conn, d := s.active()
|
||||
if conn == nil {
|
||||
return fmt.Errorf("database is not available")
|
||||
}
|
||||
_, err := conn.ExecContext(ctx, d.rebind(`INSERT INTO audit_logs (actor, type, target, message, ip, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`),
|
||||
sanitize(log.Actor), sanitize(log.Type), sanitize(log.Target), sanitize(log.Message), sanitize(log.IP), sanitize(log.UserAgent), log.CreatedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ymhut-box/server/unified-management/internal/config"
|
||||
)
|
||||
@@ -109,6 +111,45 @@ func TestVerifyAdminPasswordUsesLocalSQLiteWhenRemoteIsUnavailable(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyAdminPasswordHonorsContextDeadlineWhenSQLiteIsBusy(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
store, err := Open(&config.Config{
|
||||
StorageDir: root,
|
||||
Database: config.DatabaseConfig{
|
||||
Provider: "sqlite",
|
||||
SQLitePath: filepath.Join(root, "busy-login.sqlite"),
|
||||
FailoverEnabled: true,
|
||||
HealthIntervalSec: 3600,
|
||||
MaxOpenConns: 1,
|
||||
MaxIdleConns: 1,
|
||||
ConnMaxLifetimeSeconds: 60,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
if err := store.EnsureDefaultAdmin(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
conn, err := store.localDB.Conn(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
started := time.Now()
|
||||
_, ok, err := store.VerifyAdminPassword(ctx, "admin", "admin")
|
||||
if !errors.Is(err, context.DeadlineExceeded) || ok {
|
||||
t.Fatalf("busy login returned ok=%v err=%v, want deadline exceeded", ok, err)
|
||||
}
|
||||
if elapsed := time.Since(started); elapsed > time.Second {
|
||||
t.Fatalf("busy login ignored context deadline for %s", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenRecordsCurrentSchemaVersion(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, "unified.sqlite")
|
||||
|
||||
Reference in New Issue
Block a user