更新了update门户站点界面和部分功能

This commit is contained in:
QWQLwToo
2026-06-26 14:30:09 +08:00
parent 57f4d94d0a
commit cd2fd435a2
20 changed files with 1128 additions and 168 deletions
+35 -6
View File
@@ -855,24 +855,53 @@ func (s *Store) IsDefaultAdminPassword(ctx context.Context) (bool, error) {
}
func (s *Store) ChangeAdminPassword(ctx context.Context, username, current, next string) error {
_, err := s.ChangeAdminPasswordWithWarning(ctx, username, current, next)
return err
}
func (s *Store) ChangeAdminPasswordWithWarning(ctx context.Context, username, current, next string) (string, error) {
if strings.TrimSpace(next) == "" {
return errors.New("new password is required")
return "", errors.New("new password is required")
}
_, ok, err := s.VerifyAdminPassword(ctx, username, current)
if err != nil {
return err
return "", err
}
if !ok {
return errors.New("current password is invalid")
return "", errors.New("current password is invalid")
}
result, err := s.exec(`UPDATE admin_users SET password_hash = ?, password_changed = 1, updated_at = ? WHERE username = ?`, passwordHash(next), Now(), username)
username = firstNonEmpty(strings.TrimSpace(username), "admin")
hash := passwordHash(next)
now := Now()
if err := s.changeAdminPasswordOn(s.localDB, s.localDialect, username, hash, now, true); err != nil {
return "", err
}
conn, d := s.active()
if conn != nil && conn != s.localDB {
if err := s.changeAdminPasswordOn(conn, d, username, hash, now, false); err != nil {
s.markFailover(err)
return "远端 MySQL 同步失败,密码已持久化到本地 SQLite", nil
}
}
return "", nil
}
func (s *Store) changeAdminPasswordOn(conn *sql.DB, d dialect, username, hash, updatedAt string, insertIfMissing bool) error {
if conn == nil {
return errors.New("database is not available")
}
result, err := conn.Exec(d.rebind(`UPDATE admin_users SET password_hash = ?, password_changed = 1, updated_at = ? WHERE username = ?`), hash, updatedAt, username)
if err != nil {
return err
}
if rows, _ := result.RowsAffected(); rows == 0 {
if rows, _ := result.RowsAffected(); rows > 0 {
return nil
}
if !insertIfMissing {
return errors.New("admin user not found")
}
return nil
_, err = conn.Exec(d.rebind(`INSERT INTO admin_users (username, password_hash, password_changed, created_at, updated_at) VALUES (?, ?, 1, ?, ?)`), username, hash, updatedAt, updatedAt)
return err
}
func (s *Store) InsertFeedback(item Feedback) error {
@@ -368,10 +368,11 @@ func sha256File(path string) string {
}
func safePackageName(name string) (string, error) {
name = strings.TrimSpace(filepath.Base(name))
if name == "" || name == "." || name == ".." || strings.ContainsAny(name, `/\`) {
original := strings.TrimSpace(name)
if original == "" || original == "." || original == ".." || strings.ContainsAny(original, `/\`) {
return "", errors.New("invalid filename")
}
name = filepath.Base(original)
lower := strings.ToLower(name)
for _, suffix := range []string{".exe", ".msix", ".appinstaller", ".msi", ".zip", ".7z"} {
if strings.HasSuffix(lower, suffix) {
@@ -49,8 +49,9 @@ func TestSaveUploadedPackageWritesFileAndUpdatesManifest(t *testing.T) {
DownloadsDir: filepath.Join(dir, "data", "update", "public", "downloads"),
BaseURL: "https://update.ymhut.cn",
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"),
Provider: "sqlite",
SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"),
HealthIntervalSec: 30,
},
}
store, err := db.Open(cfg)
@@ -88,8 +89,9 @@ func TestSaveUploadedPackageRejectsUnsafeName(t *testing.T) {
UpdatePublicDir: filepath.Join(dir, "data", "update", "public"),
DownloadsDir: filepath.Join(dir, "data", "update", "public", "downloads"),
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"),
Provider: "sqlite",
SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"),
HealthIntervalSec: 30,
},
}
store, err := db.Open(cfg)
@@ -29,9 +29,9 @@ type legacyMedia struct {
}
type legacyCategory struct {
ID string `json:"id"`
Name string `json:"name"`
Enabled *bool `json:"enabled"`
ID string `json:"id"`
Name string `json:"name"`
Enabled *bool `json:"enabled"`
Subcategories []legacySubcategory `json:"subcategories"`
}
@@ -43,7 +43,7 @@ type legacySubcategory struct {
ThumbnailURL string `json:"thumbnail_url"`
RefreshInterval int `json:"refresh_interval"`
SupportedFormats []string `json:"supported_formats"`
Downloadable bool `json:"downloadable"`
Downloadable bool `json:"downloadable"`
}
func NewService(cfg *config.Config, store *db.Store) *Service {
@@ -150,19 +150,19 @@ func (s *Service) Catalog(includeHidden bool) (map[string]any, error) {
var formats []string
_ = json.Unmarshal([]byte(item.SupportedFormats), &formats)
sub := map[string]any{
"id": item.SourceID,
"name": item.Name,
"description": item.Description,
"api_url": item.APIURL,
"urlTemplate": firstNonEmpty(item.URLTemplate, item.APIURL),
"thumbnail_url": item.ThumbnailURL,
"method": item.Method,
"proxy_mode": item.ProxyMode,
"proxyMode": item.ProxyMode,
"refresh_interval": item.CheckIntervalSec,
"cacheSeconds": item.CacheSeconds,
"supported_formats": formats,
"downloadable": true,
"id": item.SourceID,
"name": item.Name,
"description": item.Description,
"api_url": item.APIURL,
"urlTemplate": firstNonEmpty(item.URLTemplate, item.APIURL),
"thumbnail_url": item.ThumbnailURL,
"method": item.Method,
"proxy_mode": item.ProxyMode,
"proxyMode": item.ProxyMode,
"refresh_interval": item.CheckIntervalSec,
"cacheSeconds": item.CacheSeconds,
"supported_formats": formats,
"downloadable": true,
"health": map[string]any{
"status": item.LastStatus,
"latency_ms": item.LastLatencyMS,
@@ -68,8 +68,9 @@ func testStore(t *testing.T) (*config.Config, *db.Store) {
DownloadsDir: filepath.Join(dir, "data", "update", "public", "downloads"),
BaseURL: "https://update.ymhut.cn",
Database: config.DatabaseConfig{
Provider: "sqlite",
SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"),
Provider: "sqlite",
SQLitePath: filepath.Join(dir, "storage", "unified.sqlite"),
HealthIntervalSec: 30,
},
}
store, err := db.Open(cfg)
@@ -294,7 +294,7 @@ func (s *Service) importOldWebhooks(oldDB *sql.DB, result *Result) {
if err := rows.Scan(&id, &name, &event, &status, &attempts, &response, &message, &payload, &createdAt, &finishedAt); err != nil {
continue
}
_ = s.store.InsertAudit(db.AuditLog{Actor: "legacy", Type: "webhook." + status, Target: name, Message: "旧反馈 Webhook 记录:" + strings.TrimSpace(event+" "+message), CreatedAt: firstNonEmpty(createdAt, finishedAt, db.Now())})
_ = s.store.InsertAudit(db.AuditLog{Actor: "legacy", Type: "webhook." + status, Target: name, Message: "旧反馈 Webhook 记录:" + strings.TrimSpace(event+" "+message), CreatedAt: firstNonEmpty(createdAt, finishedAt, db.Now())})
result.Stats["importedRows"]++
}
}
@@ -195,12 +195,17 @@ func (r *router) handleChangePassword(w http.ResponseWriter, req *http.Request)
writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err)
return
}
if err := r.store.ChangeAdminPassword(req.Context(), "admin", body.CurrentPassword, body.NewPassword); err != nil {
warning, err := r.store.ChangeAdminPasswordWithWarning(req.Context(), "admin", body.CurrentPassword, body.NewPassword)
if err != nil {
writeError(w, http.StatusBadRequest, "PASSWORD_CHANGE_FAILED", err)
return
}
_ = r.store.InsertAudit(db.AuditLog{Actor: "admin", Type: "auth.password_changed", Target: "admin", Message: "后台密码已修改", IP: req.RemoteAddr, UserAgent: req.UserAgent()})
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
payload := map[string]any{"ok": true, "isDefaultPassword": false}
if warning != "" {
payload["warning"] = warning
}
writeJSON(w, http.StatusOK, payload)
}
func (r *router) handleClientBootstrap(w http.ResponseWriter, req *http.Request) {