Update application UI and functionality

This commit is contained in:
2026-07-26 16:20:36 +08:00
parent b9aff58f32
commit 97ea6fb7aa
48 changed files with 2790 additions and 628 deletions
@@ -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")