更新了update门户站点界面和部分功能
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user