Files
subtrack/internal/web/handlers_misc.go
T
chun_qiu f047662129
build / build-and-test (push) Failing after 1m24s
首次推送
2026-10-18 19:44:38 +08:00

300 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package web
import (
"encoding/csv"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"time"
"daili/internal/domain"
"daili/internal/store"
)
type logsData struct {
List []store.OpLog
Pager Pager
Q string
Total int
}
func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
pageNo := s.queryInt(r, "page", 1)
if pageNo < 1 {
pageNo = 1
}
list, total, err := s.db.ListLogs(q, pageSize, (pageNo-1)*pageSize)
if err != nil {
http.Error(w, "读取日志失败", http.StatusInternalServerError)
return
}
pager := newPager(r, pageNo, pageSize, total)
if pager.Page != pageNo {
list, total, err = s.db.ListLogs(q, pageSize, (pager.Page-1)*pageSize)
if err != nil {
http.Error(w, "读取日志失败", http.StatusInternalServerError)
return
}
}
p := s.newPage(r, "操作日志", "logs")
p.D = &logsData{List: list, Pager: pager, Q: q, Total: total}
s.render(w, r, "logs", p)
}
type settingsData struct {
WarnDays int
ScreenRefresh int
Users []store.User
Backups []string
DBPath string
BackupDir string
BackupKeep int
BackupEnabled bool
ConfigPath string
ScreenPublic bool
Addrs []string
Port string
Uptime string
LogCount int
Version string
CustomerCount int
PackageCount int
}
func humanDuration(d time.Duration) string {
secs := int(d.Seconds())
days := secs / 86400
hours := (secs % 86400) / 3600
mins := (secs % 3600) / 60
switch {
case days > 0:
return fmt.Sprintf("%d 天 %d 小时", days, hours)
case hours > 0:
return fmt.Sprintf("%d 小时 %d 分钟", hours, mins)
default:
return fmt.Sprintf("%d 分钟", mins)
}
}
func LocalAddrs() []string {
out := []string{}
ifaces, err := net.Interfaces()
if err != nil {
return out
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, a := range addrs {
var ip net.IP
switch v := a.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil {
continue
}
ip4 := ip.To4()
if ip4 == nil || ip4.IsLoopback() {
continue
}
ipStr := ip4.String()
if strings.HasPrefix(ipStr, "169.254.") {
continue
}
out = append(out, ipStr)
}
}
return out
}
func (s *Server) handleSettings(w http.ResponseWriter, r *http.Request) {
users, _ := s.db.ListUsers()
backups, _ := s.db.ListBackups()
custN, _ := s.db.CustomerCount()
pkgs, _ := s.db.ListPackages(true)
logN, _ := s.db.LogCount()
port := s.cfg.Addr
if strings.HasPrefix(port, ":") {
port = port[1:]
}
data := &settingsData{
WarnDays: s.warnDays(),
ScreenRefresh: s.screenRefresh(),
Users: users,
Backups: backups,
DBPath: s.db.Path,
BackupDir: s.cfg.BackupDir,
BackupKeep: s.cfg.BackupKeep,
BackupEnabled: s.cfg.BackupEnabled,
ConfigPath: s.cfg.ConfigPath,
ScreenPublic: s.cfg.ScreenPublic,
Addrs: LocalAddrs(),
Port: port,
Uptime: humanDuration(time.Since(s.started)),
LogCount: logN,
Version: s.cfg.Version,
CustomerCount: custN,
PackageCount: len(pkgs),
}
p := s.newPage(r, "设置", "settings")
p.D = data
s.render(w, r, "settings", p)
}
func (s *Server) handleSettingsSave(w http.ResponseWriter, r *http.Request) {
warn := s.formInt(r, "warn_days", 3)
if warn < 0 {
warn = 0
}
if warn > 60 {
warn = 60
}
refresh := s.formInt(r, "screen_refresh", 30)
if refresh < 5 {
refresh = 5
}
if refresh > 600 {
refresh = 600
}
_ = s.db.SetSetting("warn_days", strconv.Itoa(warn))
_ = s.db.SetSetting("screen_refresh", strconv.Itoa(refresh))
s.logOp(r, "修改设置", "settings", 0,
fmt.Sprintf("预警 %d 天、大屏刷新 %d 秒", warn, refresh))
s.redirect(w, r, "/settings", "设置已保存")
}
func (s *Server) handlePasswordChange(w http.ResponseWriter, r *http.Request) {
u := s.currentUser(r)
old := r.PostFormValue("old_password")
pw := r.PostFormValue("password")
pw2 := r.PostFormValue("password2")
if u == nil {
http.Redirect(w, r, "/login", http.StatusFound)
return
}
if _, err := s.db.Authenticate(u.Username, old); err != nil {
s.redirect(w, r, "/settings", "原密码不正确")
return
}
if len(pw) < 6 {
s.redirect(w, r, "/settings", "新密码至少 6 位")
return
}
if pw != pw2 {
s.redirect(w, r, "/settings", "两次输入的新密码不一致")
return
}
if err := s.db.UpdatePassword(u.ID, pw); err != nil {
s.redirect(w, r, "/settings", "修改失败:"+err.Error())
return
}
s.logOp(r, "修改密码", "user", u.ID, "管理员 "+u.Username+" 修改了自己的密码")
s.redirect(w, r, "/settings", "密码已更新")
}
func (s *Server) handleUserCreate(w http.ResponseWriter, r *http.Request) {
username := strings.TrimSpace(r.PostFormValue("username"))
display := strings.TrimSpace(r.PostFormValue("display_name"))
pw := r.PostFormValue("password")
switch {
case len(username) < 3:
s.redirect(w, r, "/settings", "用户名至少 3 个字符")
return
case len(pw) < 6:
s.redirect(w, r, "/settings", "密码至少 6 位")
return
}
if display == "" {
display = username
}
u, err := s.db.CreateUser(username, display, pw)
if err != nil {
if err == store.ErrDuplicate {
s.redirect(w, r, "/settings", "用户名已存在")
return
}
s.redirect(w, r, "/settings", "创建失败:"+err.Error())
return
}
s.logOp(r, "新增管理员", "user", u.ID, "新增管理员 "+username)
s.redirect(w, r, "/settings", "管理员 "+username+" 已创建")
}
func (s *Server) handleUserToggle(w http.ResponseWriter, r *http.Request) {
id := pathID(r)
me := s.currentUser(r)
if me != nil && me.ID == id {
s.redirect(w, r, "/settings", "不能停用自己的账户")
return
}
target, err := s.db.UserByID(id)
if err != nil {
http.NotFound(w, r)
return
}
if err := s.db.SetUserActive(id, !target.IsActive); err != nil {
s.redirect(w, r, "/settings", "操作失败:"+err.Error())
return
}
action := "停用账户"
if !target.IsActive {
action = "启用账户"
}
s.logOp(r, action, "user", id, action+" "+target.Username)
s.redirect(w, r, "/settings", action+"成功")
}
func (s *Server) handleBackupNow(w http.ResponseWriter, r *http.Request) {
path, err := s.db.BackupNow(s.cfg.BackupKeep)
if err != nil {
s.redirect(w, r, "/settings", "备份失败:"+err.Error())
return
}
s.logOp(r, "备份数据", "backup", 0, "手动备份 "+path)
s.redirect(w, r, "/settings", "已备份:"+path)
}
func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
kind := strings.TrimSuffix(r.PathValue("kind"), ".csv")
var set *store.ExportRowSet
var err error
switch kind {
case "customers":
set, err = s.db.ExportCustomers()
case "subscriptions":
set, err = s.db.ExportSubscriptions(domain.Today(), s.warnDays())
case "logs":
set, err = s.db.ExportLogs()
default:
http.NotFound(w, r)
return
}
if err != nil {
http.Error(w, "导出失败:"+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition",
fmt.Sprintf("attachment; filename=\"daili-%s-%s.csv\"", kind, time.Now().Format("20060102")))
_, _ = w.Write([]byte{0xEF, 0xBB, 0xBF})
cw := csv.NewWriter(w)
_ = cw.Write(set.Header)
for _, row := range set.Rows {
_ = cw.Write(row)
}
cw.Flush()
s.logOp(r, "导出数据", "export", 0, "导出 "+kind+"("+strconv.Itoa(len(set.Rows))+" 行)")
}