2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

feat: 添加操作日志记录功能和前端日志查看页面 (#1227)

* Initial plan

* feat: 添加操作日志记录功能和前端日志查看页面

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: 修复 HttpLog 中 IP 字符串处理的类型安全问题

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: lint

* feat: 完善面板内日志记录,添加网站/数据库/项目/计划任务/备份CRUD日志

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* feat: 完善更多模块的日志记录(证书/SSH/Webhook/用户/设置/安全)

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: 改进cert_dns删除方法,在删除前获取实体信息用于日志记录

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: lint

* feat: 从session获取操作员ID并在前端显示用户名

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: lint

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
Copilot
2026-01-12 23:31:22 +08:00
committed by GitHub
parent 1012e5a246
commit f2e41a3364
64 changed files with 1305 additions and 248 deletions

View File

@@ -67,30 +67,30 @@ func initWeb() (*app.Web, error) {
appRepo := data.NewAppRepo(locale, config, db, logger, cacheRepo, taskRepo)
userTokenRepo := data.NewUserTokenRepo(locale, config, db)
middlewares := middleware.NewMiddlewares(config, manager, appRepo, userTokenRepo)
userRepo := data.NewUserRepo(locale, db)
userRepo := data.NewUserRepo(locale, db, logger)
userService := service.NewUserService(locale, config, manager, userRepo)
userTokenService := service.NewUserTokenService(locale, userTokenRepo)
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, databaseServerRepo)
databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo)
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, logger, databaseServerRepo)
databaseRepo := data.NewDatabaseRepo(locale, db, logger, databaseServerRepo, databaseUserRepo)
certRepo := data.NewCertRepo(locale, db, logger)
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
settingRepo := data.NewSettingRepo(locale, db, config, taskRepo)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
settingRepo := data.NewSettingRepo(locale, db, logger, config, taskRepo)
websiteRepo := data.NewWebsiteRepo(locale, db, logger, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
environmentRepo := data.NewEnvironmentRepo(locale, config, cacheRepo, taskRepo)
cronRepo := data.NewCronRepo(locale, db)
backupRepo := data.NewBackupRepo(locale, config, db, settingRepo, websiteRepo)
cronRepo := data.NewCronRepo(locale, db, logger)
backupRepo := data.NewBackupRepo(locale, config, db, logger, settingRepo, websiteRepo)
homeService := service.NewHomeService(locale, config, taskRepo, websiteRepo, appRepo, environmentRepo, settingRepo, cronRepo, backupRepo)
taskService := service.NewTaskService(taskRepo)
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
projectRepo := data.NewProjectRepo(locale, db)
projectRepo := data.NewProjectRepo(locale, db, logger)
projectService := service.NewProjectService(projectRepo)
databaseService := service.NewDatabaseService(databaseRepo)
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
backupService := service.NewBackupService(locale, backupRepo)
certService := service.NewCertService(locale, certRepo)
certDNSRepo := data.NewCertDNSRepo(db)
certDNSRepo := data.NewCertDNSRepo(db, logger)
certDNSService := service.NewCertDNSService(certDNSRepo)
certAccountService := service.NewCertAccountService(certAccountRepo)
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo)
@@ -98,10 +98,10 @@ func initWeb() (*app.Web, error) {
environmentPHPService := service.NewEnvironmentPHPService(locale, config, environmentRepo, taskRepo)
cronService := service.NewCronService(cronRepo)
processService := service.NewProcessService()
safeRepo := data.NewSafeRepo()
safeRepo := data.NewSafeRepo(logger)
safeService := service.NewSafeService(safeRepo)
firewallService := service.NewFirewallService()
sshRepo := data.NewSSHRepo(locale, db)
sshRepo := data.NewSSHRepo(locale, db, logger)
sshService := service.NewSSHService(sshRepo)
containerRepo := data.NewContainerRepo()
containerService := service.NewContainerService(containerRepo)
@@ -114,6 +114,8 @@ func initWeb() (*app.Web, error) {
containerVolumeRepo := data.NewContainerVolumeRepo()
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
fileService := service.NewFileService(locale, taskRepo)
logRepo := data.NewLogRepo(db)
logService := service.NewLogService(logRepo)
monitorRepo := data.NewMonitorRepo(db, settingRepo)
monitorService := service.NewMonitorService(settingRepo, monitorRepo)
settingService := service.NewSettingService(locale, db, settingRepo, certRepo, certAccountRepo)
@@ -123,7 +125,7 @@ func initWeb() (*app.Web, error) {
toolboxSSHService := service.NewToolboxSSHService(locale)
toolboxDiskService := service.NewToolboxDiskService(locale)
toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo)
webHookRepo := data.NewWebHookRepo(locale, db)
webHookRepo := data.NewWebHookRepo(locale, db, logger)
webHookService := service.NewWebHookService(webHookRepo)
codeserverApp := codeserver.NewApp()
dockerApp := docker.NewApp()
@@ -146,7 +148,7 @@ func initWeb() (*app.Web, error) {
s3fsApp := s3fs.NewApp(locale)
supervisorApp := supervisor.NewApp(locale)
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, loader)
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, loader)
wsService := service.NewWsService(locale, config, logger, sshRepo)
ws := route.NewWs(wsService)
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)

View File

@@ -59,15 +59,15 @@ func initCli() (*app.Cli, error) {
queue := bootstrap.NewQueue()
taskRepo := data.NewTaskRepo(locale, db, logger, queue)
appRepo := data.NewAppRepo(locale, config, db, logger, cacheRepo, taskRepo)
userRepo := data.NewUserRepo(locale, db)
settingRepo := data.NewSettingRepo(locale, db, config, taskRepo)
userRepo := data.NewUserRepo(locale, db, logger)
settingRepo := data.NewSettingRepo(locale, db, logger, config, taskRepo)
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, databaseServerRepo)
databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo)
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, logger, databaseServerRepo)
databaseRepo := data.NewDatabaseRepo(locale, db, logger, databaseServerRepo, databaseUserRepo)
certRepo := data.NewCertRepo(locale, db, logger)
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
backupRepo := data.NewBackupRepo(locale, config, db, settingRepo, websiteRepo)
websiteRepo := data.NewWebsiteRepo(locale, db, logger, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
backupRepo := data.NewBackupRepo(locale, config, db, logger, settingRepo, websiteRepo)
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo, certRepo, certAccountRepo)
cli := route.NewCli(locale, cliService)
command := bootstrap.NewCli(locale, cli)

View File

@@ -1,6 +1,10 @@
package biz
import "github.com/acepanel/panel/pkg/types"
import (
"context"
"github.com/acepanel/panel/pkg/types"
)
type BackupType string
@@ -15,9 +19,9 @@ const (
type BackupRepo interface {
List(typ BackupType) ([]*types.BackupFile, error)
Create(typ BackupType, target string, path ...string) error
Delete(typ BackupType, name string) error
Restore(typ BackupType, backup, target string) error
Create(ctx context.Context, typ BackupType, target string, path ...string) error
Delete(ctx context.Context, typ BackupType, name string) error
Restore(ctx context.Context, typ BackupType, backup, target string) error
ClearExpired(path, prefix string, save int) error
CutoffLog(path, target string) error
GetPath(typ BackupType) (string, error)

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
mholtacme "github.com/mholt/acmez/v3/acme"
@@ -35,10 +36,10 @@ type CertRepo interface {
List(page, limit uint) ([]*types.CertList, int64, error)
Get(id uint) (*Cert, error)
GetByWebsite(WebsiteID uint) (*Cert, error)
Upload(req *request.CertUpload) (*Cert, error)
Create(req *request.CertCreate) (*Cert, error)
Update(req *request.CertUpdate) error
Delete(id uint) error
Upload(ctx context.Context, req *request.CertUpload) (*Cert, error)
Create(ctx context.Context, req *request.CertCreate) (*Cert, error)
Update(ctx context.Context, req *request.CertUpdate) error
Delete(ctx context.Context, id uint) error
ObtainAuto(id uint) (*acme.Certificate, error)
ObtainManual(id uint) (*acme.Certificate, error)
ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error)

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/acepanel/panel/internal/http/request"
@@ -24,7 +25,7 @@ type CertAccountRepo interface {
List(page, limit uint) ([]*CertAccount, int64, error)
GetDefault(userID uint) (*CertAccount, error)
Get(id uint) (*CertAccount, error)
Create(req *request.CertAccountCreate) (*CertAccount, error)
Update(req *request.CertAccountUpdate) error
Delete(id uint) error
Create(ctx context.Context, req *request.CertAccountCreate) (*CertAccount, error)
Update(ctx context.Context, req *request.CertAccountUpdate) error
Delete(ctx context.Context, id uint) error
}

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/acepanel/panel/internal/http/request"
@@ -21,7 +22,7 @@ type CertDNS struct {
type CertDNSRepo interface {
List(page, limit uint) ([]*CertDNS, int64, error)
Get(id uint) (*CertDNS, error)
Create(req *request.CertDNSCreate) (*CertDNS, error)
Update(req *request.CertDNSUpdate) error
Delete(id uint) error
Create(ctx context.Context, req *request.CertDNSCreate) (*CertDNS, error)
Update(ctx context.Context, req *request.CertDNSUpdate) error
Delete(ctx context.Context, id uint) error
}

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/acepanel/panel/internal/http/request"
@@ -22,8 +23,8 @@ type CronRepo interface {
Count() (int64, error)
List(page, limit uint) ([]*Cron, int64, error)
Get(id uint) (*Cron, error)
Create(req *request.CronCreate) error
Update(req *request.CronUpdate) error
Delete(id uint) error
Create(ctx context.Context, req *request.CronCreate) error
Update(ctx context.Context, req *request.CronUpdate) error
Delete(ctx context.Context, id uint) error
Status(id uint, status bool) error
}

View File

@@ -1,6 +1,8 @@
package biz
import (
"context"
"github.com/acepanel/panel/internal/http/request"
)
@@ -25,7 +27,7 @@ type Database struct {
type DatabaseRepo interface {
List(page, limit uint) ([]*Database, int64, error)
Create(req *request.DatabaseCreate) error
Delete(serverID uint, name string) error
Create(ctx context.Context, req *request.DatabaseCreate) error
Delete(ctx context.Context, serverID uint, name string) error
Comment(req *request.DatabaseComment) error
}

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/libtnb/utils/crypt"
@@ -65,9 +66,9 @@ type DatabaseUserRepo interface {
Count() (int64, error)
List(page, limit uint) ([]*DatabaseUser, int64, error)
Get(id uint) (*DatabaseUser, error)
Create(req *request.DatabaseUserCreate) error
Create(ctx context.Context, req *request.DatabaseUserCreate) error
Update(req *request.DatabaseUserUpdate) error
UpdateRemark(req *request.DatabaseUserUpdateRemark) error
Delete(id uint) error
Delete(ctx context.Context, id uint) error
DeleteByNames(serverID uint, names []string) error
}

51
internal/biz/log.go Normal file
View File

@@ -0,0 +1,51 @@
package biz
import (
"time"
)
const (
LogTypeApp = "app"
LogTypeDB = "db"
LogTypeHTTP = "http"
)
// 操作日志类型常量
const (
OperationTypePanel = "panel"
OperationTypeWebsite = "website"
OperationTypeDatabase = "database"
OperationTypeDatabaseUser = "database_user"
OperationTypeDatabaseServer = "database_server"
OperationTypeProject = "project"
OperationTypeCert = "cert"
OperationTypeFile = "file"
OperationTypeApp = "app"
OperationTypeCron = "cron"
OperationTypeBackup = "backup"
OperationTypeContainer = "container"
OperationTypeFirewall = "firewall"
OperationTypeSafe = "safe"
OperationTypeSSH = "ssh"
OperationTypeSetting = "setting"
OperationTypeMonitor = "monitor"
OperationTypeWebhook = "webhook"
OperationTypeUser = "user"
)
// LogEntry 日志条目
type LogEntry struct {
Time time.Time `json:"time"`
Level string `json:"level"`
Msg string `json:"msg"`
Type string `json:"type,omitempty"`
OperatorID uint `json:"operator_id,omitempty"`
OperatorName string `json:"operator_name,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
}
// LogRepo 日志仓库接口
type LogRepo interface {
// List 获取日志列表
List(logType string, limit int) ([]LogEntry, error)
}

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/acepanel/panel/internal/http/request"
@@ -19,7 +20,7 @@ type Project struct {
type ProjectRepo interface {
List(typ types.ProjectType, page, limit uint) ([]*types.ProjectDetail, int64, error)
Get(id uint) (*types.ProjectDetail, error)
Create(req *request.ProjectCreate) (*types.ProjectDetail, error)
Update(req *request.ProjectUpdate) error
Delete(id uint) error
Create(ctx context.Context, req *request.ProjectCreate) (*types.ProjectDetail, error)
Update(ctx context.Context, req *request.ProjectUpdate) error
Delete(ctx context.Context, id uint) error
}

View File

@@ -1,8 +1,10 @@
package biz
import "context"
type SafeRepo interface {
GetSSH() (uint, bool, error)
UpdateSSH(port uint, status bool) error
UpdateSSH(ctx context.Context, port uint, status bool) error
GetPingStatus() (bool, error)
UpdatePingStatus(status bool) error
UpdatePingStatus(ctx context.Context, status bool) error
}

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/acepanel/panel/internal/http/request"
@@ -44,6 +45,6 @@ type SettingRepo interface {
SetSlice(key SettingKey, value []string) error
Delete(key SettingKey) error
GetPanel() (*request.SettingPanel, error)
UpdatePanel(req *request.SettingPanel) (bool, error)
UpdatePanel(ctx context.Context, req *request.SettingPanel) (bool, error)
UpdateCert(req *request.SettingCert) error
}

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/libtnb/utils/crypt"
@@ -61,7 +62,7 @@ func (r *SSH) AfterFind(tx *gorm.DB) error {
type SSHRepo interface {
List(page, limit uint) ([]*SSH, int64, error)
Get(id uint) (*SSH, error)
Create(req *request.SSHCreate) error
Update(req *request.SSHUpdate) error
Delete(id uint) error
Create(ctx context.Context, req *request.SSHCreate) error
Update(ctx context.Context, req *request.SSHUpdate) error
Delete(ctx context.Context, id uint) error
}

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"image"
"time"
@@ -23,11 +24,11 @@ type User struct {
type UserRepo interface {
List(page, limit uint) ([]*User, int64, error)
Get(id uint) (*User, error)
Create(username, password, email string) (*User, error)
UpdateUsername(id uint, username string) error
UpdatePassword(id uint, password string) error
UpdateEmail(id uint, email string) error
Delete(id uint) error
Create(ctx context.Context, username, password, email string) (*User, error)
UpdateUsername(ctx context.Context, id uint, username string) error
UpdatePassword(ctx context.Context, id uint, password string) error
UpdateEmail(ctx context.Context, id uint, email string) error
Delete(ctx context.Context, id uint) error
CheckPassword(username, password string) (*User, error)
IsTwoFA(username string) (bool, error)
GenerateTwoFA(id uint) (image.Image, string, string, error)

View File

@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/acepanel/panel/internal/http/request"
@@ -24,8 +25,8 @@ type WebHookRepo interface {
List(page, limit uint) ([]*WebHook, int64, error)
Get(id uint) (*WebHook, error)
GetByKey(key string) (*WebHook, error)
Create(req *request.WebHookCreate) (*WebHook, error)
Update(req *request.WebHookUpdate) error
Delete(id uint) error
Create(ctx context.Context, req *request.WebHookCreate) (*WebHook, error)
Update(ctx context.Context, req *request.WebHookUpdate) error
Delete(ctx context.Context, id uint) error
Call(key string) (string, error)
}

View File

@@ -40,9 +40,9 @@ type WebsiteRepo interface {
Get(id uint) (*types.WebsiteSetting, error)
GetByName(name string) (*types.WebsiteSetting, error)
List(typ string, page, limit uint) ([]*Website, int64, error)
Create(req *request.WebsiteCreate) (*Website, error)
Update(req *request.WebsiteUpdate) error
Delete(req *request.WebsiteDelete) error
Create(ctx context.Context, req *request.WebsiteCreate) (*Website, error)
Update(ctx context.Context, req *request.WebsiteUpdate) error
Delete(ctx context.Context, req *request.WebsiteDelete) error
ClearLog(id uint) error
UpdateRemark(id uint, remark string) error
ResetConfig(id uint) error

View File

@@ -217,7 +217,7 @@ func (r *appRepo) Install(channel, slug string) error {
// 下载回调
if err = r.api.AppCallback(slug); err != nil {
r.log.Warn("[App] download callback failed", slog.String("app", slug), slog.Any("err", err))
r.log.Warn("download callback failed", slog.String("type", biz.OperationTypeApp), slog.Uint64("operator_id", 0), slog.String("app", slug), slog.Any("err", err))
}
if app.IsCli {
@@ -332,7 +332,7 @@ func (r *appRepo) Update(slug string) error {
// 下载回调
if err = r.api.AppCallback(slug); err != nil {
r.log.Warn("[App] download callback failed", slog.String("app", slug), slog.Any("err", err))
r.log.Warn("download callback failed", slog.String("type", biz.OperationTypeApp), slog.Uint64("operator_id", 0), slog.String("app", slug), slog.Any("err", err))
}
if app.IsCli {

View File

@@ -1,8 +1,10 @@
package data
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
@@ -27,15 +29,17 @@ type backupRepo struct {
t *gotext.Locale
conf *config.Config
db *gorm.DB
log *slog.Logger
setting biz.SettingRepo
website biz.WebsiteRepo
}
func NewBackupRepo(t *gotext.Locale, conf *config.Config, db *gorm.DB, setting biz.SettingRepo, website biz.WebsiteRepo) biz.BackupRepo {
func NewBackupRepo(t *gotext.Locale, conf *config.Config, db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, website biz.WebsiteRepo) biz.BackupRepo {
return &backupRepo{
t: t,
conf: conf,
db: db,
log: log,
setting: setting,
website: website,
}
@@ -74,7 +78,7 @@ func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
// typ 备份类型
// target 目标名称
// path 可选备份保存路径
func (r *backupRepo) Create(typ biz.BackupType, target string, path ...string) error {
func (r *backupRepo) Create(ctx context.Context, typ biz.BackupType, target string, path ...string) error {
defPath, err := r.GetPath(typ)
if err != nil {
return err
@@ -83,37 +87,53 @@ func (r *backupRepo) Create(typ biz.BackupType, target string, path ...string) e
defPath = path[0]
}
var createErr error
switch typ {
case biz.BackupTypeWebsite:
return r.createWebsite(defPath, target)
createErr = r.createWebsite(defPath, target)
case biz.BackupTypeMySQL:
return r.createMySQL(defPath, target)
createErr = r.createMySQL(defPath, target)
case biz.BackupTypePostgres:
return r.createPostgres(defPath, target)
createErr = r.createPostgres(defPath, target)
case biz.BackupTypePanel:
return r.createPanel(defPath)
createErr = r.createPanel(defPath)
default:
return errors.New(r.t.Get("unknown backup type"))
}
return errors.New(r.t.Get("unknown backup type"))
if createErr != nil {
return createErr
}
// 记录日志
r.log.Info("backup created", slog.String("type", biz.OperationTypeBackup), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("backup_type", string(typ)), slog.String("target", target))
return nil
}
// Delete 删除备份
func (r *backupRepo) Delete(typ biz.BackupType, name string) error {
func (r *backupRepo) Delete(ctx context.Context, typ biz.BackupType, name string) error {
path, err := r.GetPath(typ)
if err != nil {
return err
}
file := filepath.Join(path, name)
return io.Remove(file)
if err = io.Remove(file); err != nil {
return err
}
// 记录日志
r.log.Info("backup deleted", slog.String("type", biz.OperationTypeBackup), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("backup_type", string(typ)), slog.String("name", name))
return nil
}
// Restore 恢复备份
// typ 备份类型
// backup 备份压缩包,可以是绝对路径或者相对路径
// target 目标名称
func (r *backupRepo) Restore(typ biz.BackupType, backup, target string) error {
func (r *backupRepo) Restore(ctx context.Context, typ biz.BackupType, backup, target string) error {
if !io.Exists(backup) {
path, err := r.GetPath(typ)
if err != nil {
@@ -122,16 +142,26 @@ func (r *backupRepo) Restore(typ biz.BackupType, backup, target string) error {
backup = filepath.Join(path, backup)
}
var restoreErr error
switch typ {
case biz.BackupTypeWebsite:
return r.restoreWebsite(backup, target)
restoreErr = r.restoreWebsite(backup, target)
case biz.BackupTypeMySQL:
return r.restoreMySQL(backup, target)
restoreErr = r.restoreMySQL(backup, target)
case biz.BackupTypePostgres:
return r.restorePostgres(backup, target)
restoreErr = r.restorePostgres(backup, target)
default:
return errors.New(r.t.Get("unknown backup type"))
}
return errors.New(r.t.Get("unknown backup type"))
if restoreErr != nil {
return restoreErr
}
// 记录日志
r.log.Info("backup restored", slog.String("type", biz.OperationTypeBackup), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("backup_type", string(typ)), slog.String("target", target))
return nil
}
// CutoffLog 切割日志
@@ -745,7 +775,7 @@ func (r *backupRepo) UpdatePanel(version, url, checksum string) error {
fmt.Println(r.t.Get("|-Backup panel data..."))
}
// 备份面板
if err := r.Create(biz.BackupTypePanel, ""); err != nil {
if err := r.Create(context.Background(), biz.BackupTypePanel, ""); err != nil {
return errors.New(r.t.Get("|-Backup panel data failed: %v", err))
}
if err := io.Compress(filepath.Join(app.Root, "panel/storage"), nil, "/tmp/panel-storage.zip"); err != nil {

View File

@@ -89,7 +89,7 @@ func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) {
return cert, err
}
func (r *certRepo) Upload(req *request.CertUpload) (*biz.Cert, error) {
func (r *certRepo) Upload(ctx context.Context, req *request.CertUpload) (*biz.Cert, error) {
info, err := pkgcert.ParseCert(req.Cert)
if err != nil {
return nil, errors.New(r.t.Get("failed to parse certificate: %v", err))
@@ -108,10 +108,13 @@ func (r *certRepo) Upload(req *request.CertUpload) (*biz.Cert, error) {
return nil, err
}
// 记录日志
r.log.Info("cert uploaded", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(cert.ID)))
return cert, nil
}
func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
func (r *certRepo) Create(ctx context.Context, req *request.CertCreate) (*biz.Cert, error) {
cert := &biz.Cert{
AccountID: req.AccountID,
WebsiteID: req.WebsiteID,
@@ -123,10 +126,14 @@ func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
if err := r.db.Create(cert).Error; err != nil {
return nil, err
}
// 记录日志
r.log.Info("cert created", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(cert.ID)), slog.String("cert_type", req.Type))
return cert, nil
}
func (r *certRepo) Update(req *request.CertUpdate) error {
func (r *certRepo) Update(ctx context.Context, req *request.CertUpdate) error {
info, err := pkgcert.ParseCert(req.Cert)
if err == nil && req.Type == "upload" {
req.Domains = info.DNSNames
@@ -135,7 +142,7 @@ func (r *certRepo) Update(req *request.CertUpdate) error {
return errors.New(r.t.Get("upload certificate cannot be set to auto renewal"))
}
return r.db.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{
if err = r.db.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{
ID: req.ID,
AccountID: req.AccountID,
WebsiteID: req.WebsiteID,
@@ -146,11 +153,25 @@ func (r *certRepo) Update(req *request.CertUpdate) error {
Script: req.Script,
Domains: req.Domains,
AutoRenewal: req.AutoRenewal,
}).Error
}).Error; err != nil {
return err
}
// 记录日志
r.log.Info("cert updated", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)))
return nil
}
func (r *certRepo) Delete(id uint) error {
return r.db.Model(&biz.Cert{}).Where("id = ?", id).Delete(&biz.Cert{}).Error
func (r *certRepo) Delete(ctx context.Context, id uint) error {
if err := r.db.Model(&biz.Cert{}).Where("id = ?", id).Delete(&biz.Cert{}).Error; err != nil {
return err
}
// 记录日志
r.log.Info("cert deleted", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)))
return nil
}
func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {

View File

@@ -56,7 +56,7 @@ func (r certAccountRepo) GetDefault(userID uint) (*biz.CertAccount, error) {
KeyType: string(acme.KeyEC256),
}
return r.Create(req)
return r.Create(context.Background(), req)
}
func (r certAccountRepo) Get(id uint) (*biz.CertAccount, error) {
@@ -65,7 +65,7 @@ func (r certAccountRepo) Get(id uint) (*biz.CertAccount, error) {
return account, err
}
func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccount, error) {
func (r certAccountRepo) Create(ctx context.Context, req *request.CertAccountCreate) (*biz.CertAccount, error) {
account := new(biz.CertAccount)
account.CA = req.CA
account.Email = req.Email
@@ -118,10 +118,13 @@ func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccoun
return nil, err
}
// 记录日志
r.log.Info("cert account created", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(account.ID)), slog.String("ca", req.CA), slog.String("email", req.Email))
return account, nil
}
func (r certAccountRepo) Update(req *request.CertAccountUpdate) error {
func (r certAccountRepo) Update(ctx context.Context, req *request.CertAccountUpdate) error {
account, err := r.Get(req.ID)
if err != nil {
return err
@@ -173,11 +176,25 @@ func (r certAccountRepo) Update(req *request.CertAccountUpdate) error {
}
account.PrivateKey = string(privateKey)
return r.db.Save(account).Error
if err = r.db.Save(account).Error; err != nil {
return err
}
// 记录日志
r.log.Info("cert account updated", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("ca", req.CA))
return nil
}
func (r certAccountRepo) Delete(id uint) error {
return r.db.Model(&biz.CertAccount{}).Where("id = ?", id).Delete(&biz.CertAccount{}).Error
func (r certAccountRepo) Delete(ctx context.Context, id uint) error {
if err := r.db.Model(&biz.CertAccount{}).Where("id = ?", id).Delete(&biz.CertAccount{}).Error; err != nil {
return err
}
// 记录日志
r.log.Info("cert account deleted", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)))
return nil
}
// getGoogleEAB 获取 Google EAB

View File

@@ -1,6 +1,9 @@
package data
import (
"context"
"log/slog"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/biz"
@@ -8,12 +11,14 @@ import (
)
type certDNSRepo struct {
db *gorm.DB
db *gorm.DB
log *slog.Logger
}
func NewCertDNSRepo(db *gorm.DB) biz.CertDNSRepo {
func NewCertDNSRepo(db *gorm.DB, log *slog.Logger) biz.CertDNSRepo {
return &certDNSRepo{
db: db,
db: db,
log: log,
}
}
@@ -30,7 +35,7 @@ func (r certDNSRepo) Get(id uint) (*biz.CertDNS, error) {
return certDNS, err
}
func (r certDNSRepo) Create(req *request.CertDNSCreate) (*biz.CertDNS, error) {
func (r certDNSRepo) Create(ctx context.Context, req *request.CertDNSCreate) (*biz.CertDNS, error) {
certDNS := &biz.CertDNS{
Name: req.Name,
Type: req.Type,
@@ -41,10 +46,13 @@ func (r certDNSRepo) Create(req *request.CertDNSCreate) (*biz.CertDNS, error) {
return nil, err
}
// 记录日志
r.log.Info("cert dns created", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(certDNS.ID)), slog.String("name", req.Name))
return certDNS, nil
}
func (r certDNSRepo) Update(req *request.CertDNSUpdate) error {
func (r certDNSRepo) Update(ctx context.Context, req *request.CertDNSUpdate) error {
cert, err := r.Get(req.ID)
if err != nil {
return err
@@ -54,9 +62,28 @@ func (r certDNSRepo) Update(req *request.CertDNSUpdate) error {
cert.Type = req.Type
cert.Data = req.Data
return r.db.Save(cert).Error
if err = r.db.Save(cert).Error; err != nil {
return err
}
// 记录日志
r.log.Info("cert dns updated", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", req.Name))
return nil
}
func (r certDNSRepo) Delete(id uint) error {
return r.db.Model(&biz.CertDNS{}).Where("id = ?", id).Delete(&biz.CertDNS{}).Error
func (r certDNSRepo) Delete(ctx context.Context, id uint) error {
certDNS, err := r.Get(id)
if err != nil {
return err
}
if err = r.db.Model(&biz.CertDNS{}).Where("id = ?", id).Delete(&biz.CertDNS{}).Error; err != nil {
return err
}
// 记录日志
r.log.Info("cert dns deleted", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", certDNS.Name))
return nil
}

View File

@@ -1,8 +1,10 @@
package data
import (
"context"
"errors"
"fmt"
"log/slog"
"path/filepath"
"github.com/leonelquinteros/gotext"
@@ -19,14 +21,16 @@ import (
)
type cronRepo struct {
t *gotext.Locale
db *gorm.DB
t *gotext.Locale
db *gorm.DB
log *slog.Logger
}
func NewCronRepo(t *gotext.Locale, db *gorm.DB) biz.CronRepo {
func NewCronRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.CronRepo {
return &cronRepo{
t: t,
db: db,
t: t,
db: db,
log: log,
}
}
@@ -55,7 +59,7 @@ func (r *cronRepo) Get(id uint) (*biz.Cron, error) {
return cron, nil
}
func (r *cronRepo) Create(req *request.CronCreate) error {
func (r *cronRepo) Create(ctx context.Context, req *request.CronCreate) error {
var script string
if req.Type == "backup" {
if req.BackupType == "website" {
@@ -111,10 +115,13 @@ acepanel cutoff clear -t website -f '%s' -s '%d' -p '%s'
return err
}
// 记录日志
r.log.Info("cron created", slog.String("type", biz.OperationTypeCron), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("name", req.Name), slog.String("cron_type", req.Type))
return nil
}
func (r *cronRepo) Update(req *request.CronUpdate) error {
func (r *cronRepo) Update(ctx context.Context, req *request.CronUpdate) error {
cron, err := r.Get(req.ID)
if err != nil {
return err
@@ -142,10 +149,13 @@ func (r *cronRepo) Update(req *request.CronUpdate) error {
}
}
// 记录日志
r.log.Info("cron updated", slog.String("type", biz.OperationTypeCron), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", cron.Name))
return nil
}
func (r *cronRepo) Delete(id uint) error {
func (r *cronRepo) Delete(ctx context.Context, id uint) error {
cron, err := r.Get(id)
if err != nil {
return err
@@ -158,7 +168,14 @@ func (r *cronRepo) Delete(id uint) error {
return err
}
return r.db.Delete(cron).Error
if err = r.db.Delete(cron).Error; err != nil {
return err
}
// 记录日志
r.log.Info("cron deleted", slog.String("type", biz.OperationTypeCron), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", cron.Name))
return nil
}
func (r *cronRepo) Status(id uint, status bool) error {

View File

@@ -20,6 +20,7 @@ var ProviderSet = wire.NewSet(
NewDatabaseServerRepo,
NewDatabaseUserRepo,
NewEnvironmentRepo,
NewLogRepo,
NewMonitorRepo,
NewProjectRepo,
NewSafeRepo,

View File

@@ -1,8 +1,10 @@
package data
import (
"context"
"errors"
"fmt"
"log/slog"
"slices"
"github.com/leonelquinteros/gotext"
@@ -16,14 +18,16 @@ import (
type databaseRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
server biz.DatabaseServerRepo
user biz.DatabaseUserRepo
}
func NewDatabaseRepo(t *gotext.Locale, db *gorm.DB, server biz.DatabaseServerRepo, user biz.DatabaseUserRepo) biz.DatabaseRepo {
func NewDatabaseRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, server biz.DatabaseServerRepo, user biz.DatabaseUserRepo) biz.DatabaseRepo {
return &databaseRepo{
t: t,
db: db,
log: log,
server: server,
user: user,
}
@@ -65,7 +69,7 @@ func (r *databaseRepo) List(page, limit uint) ([]*biz.Database, int64, error) {
return database[(page-1)*limit:], int64(len(database)), nil
}
func (r *databaseRepo) Create(req *request.DatabaseCreate) error {
func (r *databaseRepo) Create(ctx context.Context, req *request.DatabaseCreate) error {
server, err := r.server.Get(req.ServerID)
if err != nil {
return err
@@ -80,7 +84,7 @@ func (r *databaseRepo) Create(req *request.DatabaseCreate) error {
switch server.Type {
case biz.DatabaseTypeMysql:
if req.CreateUser {
if err = r.user.Create(&request.DatabaseUserCreate{
if err = r.user.Create(ctx, &request.DatabaseUserCreate{
ServerID: req.ServerID,
Username: req.Username,
Password: req.Password,
@@ -99,7 +103,7 @@ func (r *databaseRepo) Create(req *request.DatabaseCreate) error {
}
case biz.DatabaseTypePostgresql:
if req.CreateUser {
if err = r.user.Create(&request.DatabaseUserCreate{
if err = r.user.Create(ctx, &request.DatabaseUserCreate{
ServerID: req.ServerID,
Username: req.Username,
Password: req.Password,
@@ -121,10 +125,13 @@ func (r *databaseRepo) Create(req *request.DatabaseCreate) error {
}
}
// 记录日志
r.log.Info("database created", slog.String("type", biz.OperationTypeDatabase), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("name", req.Name), slog.Uint64("server_id", uint64(req.ServerID)))
return nil
}
func (r *databaseRepo) Delete(serverID uint, name string) error {
func (r *databaseRepo) Delete(ctx context.Context, serverID uint, name string) error {
server, err := r.server.Get(serverID)
if err != nil {
return err
@@ -136,7 +143,14 @@ func (r *databaseRepo) Delete(serverID uint, name string) error {
}
defer operator.Close()
return operator.DatabaseDrop(name)
if err = operator.DatabaseDrop(name); err != nil {
return err
}
// 记录日志
r.log.Info("database deleted", slog.String("type", biz.OperationTypeDatabase), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("name", name), slog.Uint64("server_id", uint64(serverID)))
return nil
}
func (r *databaseRepo) Comment(req *request.DatabaseComment) error {

View File

@@ -160,7 +160,7 @@ func (r *databaseServerRepo) Sync(id uint) error {
Remark: r.t.Get("sync from server %s", server.Name),
}
if err = r.db.Create(newUser).Error; err != nil {
r.log.Warn("[DatabaseServer] sync mysql database user failed", slog.Any("err", err))
r.log.Warn("sync mysql database user failed", slog.String("type", biz.OperationTypeDatabaseServer), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
}
}
@@ -179,7 +179,7 @@ func (r *databaseServerRepo) Sync(id uint) error {
Remark: r.t.Get("sync from server %s", server.Name),
}
if err = r.db.Create(newUser).Error; err != nil {
r.log.Warn("[DatabaseServer] sync postgresql database user failed", slog.Any("err", err))
r.log.Warn("sync postgresql database user failed", slog.String("type", biz.OperationTypeDatabaseServer), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
}
}

View File

@@ -1,7 +1,9 @@
package data
import (
"context"
"fmt"
"log/slog"
"slices"
"github.com/leonelquinteros/gotext"
@@ -15,13 +17,15 @@ import (
type databaseUserRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
server biz.DatabaseServerRepo
}
func NewDatabaseUserRepo(t *gotext.Locale, db *gorm.DB, server biz.DatabaseServerRepo) biz.DatabaseUserRepo {
func NewDatabaseUserRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, server biz.DatabaseServerRepo) biz.DatabaseUserRepo {
return &databaseUserRepo{
t: t,
db: db,
log: log,
server: server,
}
}
@@ -58,7 +62,7 @@ func (r *databaseUserRepo) Get(id uint) (*biz.DatabaseUser, error) {
return user, nil
}
func (r *databaseUserRepo) Create(req *request.DatabaseUserCreate) error {
func (r *databaseUserRepo) Create(ctx context.Context, req *request.DatabaseUserCreate) error {
server, err := r.server.Get(req.ServerID)
if err != nil {
return err
@@ -97,7 +101,14 @@ func (r *databaseUserRepo) Create(req *request.DatabaseUserCreate) error {
return err
}
return r.db.Save(user).Error
if err = r.db.Save(user).Error; err != nil {
return err
}
// 记录日志
r.log.Info("database user created", slog.String("type", biz.OperationTypeDatabaseUser), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("username", req.Username), slog.Uint64("server_id", uint64(req.ServerID)))
return nil
}
func (r *databaseUserRepo) Update(req *request.DatabaseUserUpdate) error {
@@ -151,7 +162,7 @@ func (r *databaseUserRepo) UpdateRemark(req *request.DatabaseUserUpdateRemark) e
return r.db.Save(user).Error
}
func (r *databaseUserRepo) Delete(id uint) error {
func (r *databaseUserRepo) Delete(ctx context.Context, id uint) error {
user, err := r.Get(id)
if err != nil {
return err
@@ -170,7 +181,14 @@ func (r *databaseUserRepo) Delete(id uint) error {
_ = operator.UserDrop(user.Username, user.Host)
return r.db.Where("id = ?", id).Delete(&biz.DatabaseUser{}).Error
if err = r.db.Where("id = ?", id).Delete(&biz.DatabaseUser{}).Error; err != nil {
return err
}
// 记录日志
r.log.Info("database user deleted", slog.String("type", biz.OperationTypeDatabaseUser), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("username", user.Username))
return nil
}
func (r *databaseUserRepo) DeleteByNames(serverID uint, names []string) error {

View File

@@ -1,9 +1,11 @@
package data
import (
"context"
"fmt"
"github.com/moby/moby/client"
"github.com/spf13/cast"
)
func getDockerClient(sock string) (*client.Client, error) {
@@ -14,3 +16,16 @@ func getDockerClient(sock string) (*client.Client, error) {
return apiClient, nil
}
// getOperatorID 从 context 中获取操作员ID
// 如果无法获取,返回 0表示系统操作
func getOperatorID(ctx context.Context) uint64 {
if ctx == nil {
return 0
}
userID := ctx.Value("user_id")
if userID == nil {
return 0
}
return cast.ToUint64(userID)
}

179
internal/data/log.go Normal file
View File

@@ -0,0 +1,179 @@
package data
import (
"bufio"
"encoding/json"
"os"
"path/filepath"
"strings"
"time"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/app"
"github.com/acepanel/panel/internal/biz"
)
type logRepo struct {
db *gorm.DB
}
func NewLogRepo(db *gorm.DB) biz.LogRepo {
return &logRepo{
db: db,
}
}
// List 获取日志列表
func (r *logRepo) List(logType string, limit int) ([]biz.LogEntry, error) {
var filename string
switch logType {
case biz.LogTypeApp:
filename = "app.log"
case biz.LogTypeDB:
filename = "db.log"
case biz.LogTypeHTTP:
filename = "http.log"
default:
filename = "app.log"
}
logPath := filepath.Join(app.Root, "panel/storage/logs", filename)
file, err := os.Open(logPath)
if err != nil {
if os.IsNotExist(err) {
return []biz.LogEntry{}, nil
}
return nil, err
}
defer func(file *os.File) { _ = file.Close() }(file)
// 读取所有行
var lines []string
scanner := bufio.NewScanner(file)
// 增加缓冲区大小以处理较长的日志行
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) != "" {
lines = append(lines, line)
}
}
if err = scanner.Err(); err != nil {
return nil, err
}
// 从末尾取指定数量的行
start := 0
if len(lines) > limit {
start = len(lines) - limit
}
lines = lines[start:]
// 倒序处理,最新的在前面
entries := make([]biz.LogEntry, 0, len(lines))
for i := len(lines) - 1; i >= 0; i-- {
entry, err := r.parseLine(lines[i], logType)
if err != nil {
continue
}
entries = append(entries, entry)
}
// 如果是app日志查询用户名
if logType == biz.LogTypeApp {
r.fillOperatorNames(entries)
}
return entries, nil
}
// fillOperatorNames 填充操作员用户名
func (r *logRepo) fillOperatorNames(entries []biz.LogEntry) {
// 收集所有用户ID
userIDs := make(map[uint]bool)
for _, entry := range entries {
if entry.OperatorID > 0 {
userIDs[entry.OperatorID] = true
}
}
if len(userIDs) == 0 {
return
}
// 批量查询用户名
ids := make([]uint, 0, len(userIDs))
for id := range userIDs {
ids = append(ids, id)
}
var users []biz.User
r.db.Select("id", "username").Where("id IN ?", ids).Find(&users)
// 构建ID到用户名的映射
userMap := make(map[uint]string)
for _, user := range users {
userMap[user.ID] = user.Username
}
// 填充用户名
for i := range entries {
if entries[i].OperatorID > 0 {
if username, ok := userMap[entries[i].OperatorID]; ok {
entries[i].OperatorName = username
}
}
}
}
// parseLine 解析日志行
func (r *logRepo) parseLine(line string, logType string) (biz.LogEntry, error) {
var rawEntry map[string]any
if err := json.Unmarshal([]byte(line), &rawEntry); err != nil {
return biz.LogEntry{}, err
}
entry := biz.LogEntry{
Extra: make(map[string]any),
}
// 解析通用字段
if t, ok := rawEntry["time"].(string); ok {
if parsed, err := time.Parse(time.RFC3339Nano, t); err == nil {
entry.Time = parsed
}
}
if level, ok := rawEntry["level"].(string); ok {
entry.Level = level
}
if msg, ok := rawEntry["msg"].(string); ok {
entry.Msg = msg
}
// 解析操作日志特有字段
if logType == biz.LogTypeApp {
if t, ok := rawEntry["type"].(string); ok {
entry.Type = t
}
if opID, ok := rawEntry["operator_id"].(float64); ok {
entry.OperatorID = uint(opID)
}
}
// 其他字段放入Extra
excludeKeys := map[string]bool{
"time": true, "level": true, "msg": true, "type": true, "operator_id": true,
}
for k, v := range rawEntry {
if !excludeKeys[k] {
entry.Extra[k] = v
}
}
return entry, nil
}

View File

@@ -1,9 +1,11 @@
package data
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strconv"
@@ -21,14 +23,16 @@ import (
)
type projectRepo struct {
t *gotext.Locale
db *gorm.DB
t *gotext.Locale
db *gorm.DB
log *slog.Logger
}
func NewProjectRepo(t *gotext.Locale, db *gorm.DB) biz.ProjectRepo {
func NewProjectRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.ProjectRepo {
return &projectRepo{
t: t,
db: db,
t: t,
db: db,
log: log,
}
}
@@ -73,7 +77,7 @@ func (r *projectRepo) Get(id uint) (*types.ProjectDetail, error) {
return r.parseProjectDetail(project)
}
func (r *projectRepo) Create(req *request.ProjectCreate) (*types.ProjectDetail, error) {
func (r *projectRepo) Create(ctx context.Context, req *request.ProjectCreate) (*types.ProjectDetail, error) {
// 检查项目名是否已存在
var count int64
if err := r.db.Model(&biz.Project{}).Where("name = ?", req.Name).Count(&count).Error; err != nil {
@@ -106,10 +110,13 @@ func (r *projectRepo) Create(req *request.ProjectCreate) (*types.ProjectDetail,
return nil, err
}
// 记录日志
r.log.Info("project created", slog.String("type", biz.OperationTypeProject), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("name", req.Name), slog.String("project_type", string(req.Type)))
return r.parseProjectDetail(project)
}
func (r *projectRepo) Update(req *request.ProjectUpdate) error {
func (r *projectRepo) Update(ctx context.Context, req *request.ProjectUpdate) error {
project := new(biz.Project)
if err := r.db.First(project, req.ID).Error; err != nil {
return err
@@ -130,11 +137,14 @@ func (r *projectRepo) Update(req *request.ProjectUpdate) error {
return err
}
// 记录日志
r.log.Info("project updated", slog.String("type", biz.OperationTypeProject), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", project.Name))
// 更新 systemd unit 文件
return r.updateUnitFile(project.Name, req)
}
func (r *projectRepo) Delete(id uint) error {
func (r *projectRepo) Delete(ctx context.Context, id uint) error {
project := new(biz.Project)
if err := r.db.First(project, id).Error; err != nil {
return err
@@ -146,7 +156,14 @@ func (r *projectRepo) Delete(id uint) error {
return fmt.Errorf("%s: %w", r.t.Get("failed to delete systemd config"), err)
}
return r.db.Delete(project).Error
if err := r.db.Delete(project).Error; err != nil {
return err
}
// 记录日志
r.log.Info("project deleted", slog.String("type", biz.OperationTypeProject), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", project.Name))
return nil
}
// unitFilePath 返回 systemd unit 文件路径

View File

@@ -1,7 +1,9 @@
package data
import (
"context"
"fmt"
"log/slog"
"strings"
"github.com/spf13/cast"
@@ -15,9 +17,10 @@ import (
type safeRepo struct {
ssh string
log *slog.Logger
}
func NewSafeRepo() biz.SafeRepo {
func NewSafeRepo(log *slog.Logger) biz.SafeRepo {
var ssh string
if os.IsRHEL() {
ssh = "sshd"
@@ -26,6 +29,7 @@ func NewSafeRepo() biz.SafeRepo {
}
return &safeRepo{
ssh: ssh,
log: log,
}
}
@@ -43,7 +47,7 @@ func (r *safeRepo) GetSSH() (uint, bool, error) {
return cast.ToUint(out), running, nil
}
func (r *safeRepo) UpdateSSH(port uint, status bool) error {
func (r *safeRepo) UpdateSSH(ctx context.Context, port uint, status bool) error {
oldPort, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
if err != nil {
return err
@@ -53,10 +57,19 @@ func (r *safeRepo) UpdateSSH(port uint, status bool) error {
_, _ = shell.Execf("sed -i 's/Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port)
if !status {
return systemctl.Stop(r.ssh)
if err = systemctl.Stop(r.ssh); err != nil {
return err
}
} else {
if err = systemctl.Restart(r.ssh); err != nil {
return err
}
}
return systemctl.Restart(r.ssh)
// 记录日志
r.log.Info("ssh settings updated", slog.String("type", biz.OperationTypeSafe), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("port", uint64(port)), slog.Bool("status", status))
return nil
}
func (r *safeRepo) GetPingStatus() (bool, error) {
@@ -72,7 +85,7 @@ func (r *safeRepo) GetPingStatus() (bool, error) {
return false, nil
}
func (r *safeRepo) UpdatePingStatus(status bool) error {
func (r *safeRepo) UpdatePingStatus(ctx context.Context, status bool) error {
fw, err := firewall.NewFirewall().Status()
if err != nil {
return err
@@ -95,5 +108,8 @@ func (r *safeRepo) UpdatePingStatus(status bool) error {
return err
}
// 记录日志
r.log.Info("ping status updated", slog.String("type", biz.OperationTypeSafe), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Bool("status", status))
return nil
}

View File

@@ -1,8 +1,10 @@
package data
import (
"context"
"encoding/json"
"errors"
"log/slog"
"path/filepath"
"sync"
@@ -25,14 +27,16 @@ type settingRepo struct {
t *gotext.Locale
cache sync.Map
db *gorm.DB
log *slog.Logger
conf *config.Config
task biz.TaskRepo
}
func NewSettingRepo(t *gotext.Locale, db *gorm.DB, conf *config.Config, task biz.TaskRepo) biz.SettingRepo {
func NewSettingRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, conf *config.Config, task biz.TaskRepo) biz.SettingRepo {
return &settingRepo{
t: t,
db: db,
log: log,
conf: conf,
task: task,
}
@@ -268,7 +272,7 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
}, nil
}
func (r *settingRepo) UpdatePanel(req *request.SettingPanel) (bool, error) {
func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel) (bool, error) {
if err := r.Set(biz.SettingKeyName, req.Name); err != nil {
return false, err
}
@@ -371,6 +375,9 @@ func (r *settingRepo) UpdatePanel(req *request.SettingPanel) (bool, error) {
return false, err
}
// 记录日志
r.log.Info("panel settings updated", slog.String("type", biz.OperationTypeSetting), slog.Uint64("operator_id", getOperatorID(ctx)))
return restartFlag, nil
}

View File

@@ -1,8 +1,10 @@
package data
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/leonelquinteros/gotext"
cryptossh "golang.org/x/crypto/ssh"
@@ -14,14 +16,16 @@ import (
)
type sshRepo struct {
t *gotext.Locale
db *gorm.DB
t *gotext.Locale
db *gorm.DB
log *slog.Logger
}
func NewSSHRepo(t *gotext.Locale, db *gorm.DB) biz.SSHRepo {
func NewSSHRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.SSHRepo {
return &sshRepo{
t: t,
db: db,
t: t,
db: db,
log: log,
}
}
@@ -41,7 +45,7 @@ func (r *sshRepo) Get(id uint) (*biz.SSH, error) {
return ssh, nil
}
func (r *sshRepo) Create(req *request.SSHCreate) error {
func (r *sshRepo) Create(ctx context.Context, req *request.SSHCreate) error {
conf := pkgssh.ClientConfig{
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
@@ -64,10 +68,17 @@ func (r *sshRepo) Create(req *request.SSHCreate) error {
Remark: req.Remark,
}
return r.db.Create(ssh).Error
if err = r.db.Create(ssh).Error; err != nil {
return err
}
// 记录日志
r.log.Info("ssh created", slog.String("type", biz.OperationTypeSSH), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("name", req.Name), slog.String("host", req.Host))
return nil
}
func (r *sshRepo) Update(req *request.SSHUpdate) error {
func (r *sshRepo) Update(ctx context.Context, req *request.SSHUpdate) error {
conf := pkgssh.ClientConfig{
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
@@ -91,9 +102,28 @@ func (r *sshRepo) Update(req *request.SSHUpdate) error {
Remark: req.Remark,
}
return r.db.Model(ssh).Where("id = ?", req.ID).Select("*").Updates(ssh).Error
if err = r.db.Model(ssh).Where("id = ?", req.ID).Select("*").Updates(ssh).Error; err != nil {
return err
}
// 记录日志
r.log.Info("ssh updated", slog.String("type", biz.OperationTypeSSH), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", req.Name))
return nil
}
func (r *sshRepo) Delete(id uint) error {
return r.db.Delete(&biz.SSH{}, id).Error
func (r *sshRepo) Delete(ctx context.Context, id uint) error {
ssh, err := r.Get(id)
if err != nil {
return err
}
if err = r.db.Delete(&biz.SSH{}, id).Error; err != nil {
return err
}
// 记录日志
r.log.Info("ssh deleted", slog.String("type", biz.OperationTypeSSH), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", ssh.Name))
return nil
}

View File

@@ -1,8 +1,10 @@
package data
import (
"context"
"errors"
"image"
"log/slog"
"github.com/leonelquinteros/gotext"
"github.com/libtnb/utils/hash"
@@ -17,13 +19,15 @@ import (
type userRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
hasher hash.Hasher
}
func NewUserRepo(t *gotext.Locale, db *gorm.DB) biz.UserRepo {
func NewUserRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.UserRepo {
return &userRepo{
t: t,
db: db,
log: log,
hasher: hash.NewArgon2id(),
}
}
@@ -44,7 +48,7 @@ func (r *userRepo) Get(id uint) (*biz.User, error) {
return user, nil
}
func (r *userRepo) Create(username, password, email string) (*biz.User, error) {
func (r *userRepo) Create(ctx context.Context, username, password, email string) (*biz.User, error) {
value, err := r.hasher.Make(password)
if err != nil {
return nil, err
@@ -59,20 +63,30 @@ func (r *userRepo) Create(username, password, email string) (*biz.User, error) {
return nil, err
}
// 记录日志
r.log.Info("user created", slog.String("type", biz.OperationTypeUser), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(user.ID)), slog.String("username", username))
return user, nil
}
func (r *userRepo) UpdateUsername(id uint, username string) error {
func (r *userRepo) UpdateUsername(ctx context.Context, id uint, username string) error {
user, err := r.Get(id)
if err != nil {
return err
}
user.Username = username
return r.db.Save(user).Error
if err = r.db.Save(user).Error; err != nil {
return err
}
// 记录日志
r.log.Info("user username updated", slog.String("type", biz.OperationTypeUser), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("username", username))
return nil
}
func (r *userRepo) UpdatePassword(id uint, password string) error {
func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string) error {
value, err := r.hasher.Make(password)
if err != nil {
return err
@@ -84,20 +98,34 @@ func (r *userRepo) UpdatePassword(id uint, password string) error {
}
user.Password = value
return r.db.Save(user).Error
if err = r.db.Save(user).Error; err != nil {
return err
}
// 记录日志
r.log.Info("user password updated", slog.String("type", biz.OperationTypeUser), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)))
return nil
}
func (r *userRepo) UpdateEmail(id uint, email string) error {
func (r *userRepo) UpdateEmail(ctx context.Context, id uint, email string) error {
user, err := r.Get(id)
if err != nil {
return err
}
user.Email = email
return r.db.Save(user).Error
if err = r.db.Save(user).Error; err != nil {
return err
}
// 记录日志
r.log.Info("user email updated", slog.String("type", biz.OperationTypeUser), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("email", email))
return nil
}
func (r *userRepo) Delete(id uint) error {
func (r *userRepo) Delete(ctx context.Context, id uint) error {
var count int64
if err := r.db.Model(&biz.User{}).Count(&count).Error; err != nil {
return err
@@ -111,12 +139,20 @@ func (r *userRepo) Delete(id uint) error {
return err
}
return r.db.Transaction(func(tx *gorm.DB) error {
username := user.Username
if err := r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&user).Association("Tokens").Delete(); err != nil {
return err
}
return tx.Delete(&user).Error
})
}); err != nil {
return err
}
// 记录日志
r.log.Info("user deleted", slog.String("type", biz.OperationTypeUser), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("username", username))
return nil
}
func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) {

View File

@@ -1,8 +1,10 @@
package data
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
@@ -19,14 +21,16 @@ import (
)
type webhookRepo struct {
t *gotext.Locale
db *gorm.DB
t *gotext.Locale
db *gorm.DB
log *slog.Logger
}
func NewWebHookRepo(t *gotext.Locale, db *gorm.DB) biz.WebHookRepo {
func NewWebHookRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.WebHookRepo {
return &webhookRepo{
t: t,
db: db,
t: t,
db: db,
log: log,
}
}
@@ -53,7 +57,7 @@ func (r *webhookRepo) GetByKey(key string) (*biz.WebHook, error) {
return webhook, nil
}
func (r *webhookRepo) Create(req *request.WebHookCreate) (*biz.WebHook, error) {
func (r *webhookRepo) Create(ctx context.Context, req *request.WebHookCreate) (*biz.WebHook, error) {
if err := os.MkdirAll(r.webhookDir(), 0755); err != nil {
return nil, errors.New(r.t.Get("failed to create webhook directory: %v", err))
}
@@ -78,10 +82,13 @@ func (r *webhookRepo) Create(req *request.WebHookCreate) (*biz.WebHook, error) {
return nil, err
}
// 记录日志
r.log.Info("webhook created", slog.String("type", biz.OperationTypeWebhook), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("name", req.Name))
return webhook, nil
}
func (r *webhookRepo) Update(req *request.WebHookUpdate) error {
func (r *webhookRepo) Update(ctx context.Context, req *request.WebHookUpdate) error {
webhook, err := r.Get(req.ID)
if err != nil {
return err
@@ -92,16 +99,23 @@ func (r *webhookRepo) Update(req *request.WebHookUpdate) error {
return errors.New(r.t.Get("failed to write webhook script: %v", err))
}
return r.db.Model(&biz.WebHook{}).Where("id = ?", req.ID).Updates(map[string]any{
if err = r.db.Model(&biz.WebHook{}).Where("id = ?", req.ID).Updates(map[string]any{
"name": req.Name,
"script": req.Script,
"raw": req.Raw,
"user": req.User,
"status": req.Status,
}).Error
}).Error; err != nil {
return err
}
// 记录日志
r.log.Info("webhook updated", slog.String("type", biz.OperationTypeWebhook), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", req.Name))
return nil
}
func (r *webhookRepo) Delete(id uint) error {
func (r *webhookRepo) Delete(ctx context.Context, id uint) error {
webhook, err := r.Get(id)
if err != nil {
return err
@@ -110,7 +124,14 @@ func (r *webhookRepo) Delete(id uint) error {
scriptFile := r.scriptPath(webhook.Key)
_ = os.Remove(scriptFile)
return r.db.Delete(&biz.WebHook{}, id).Error
if err = r.db.Delete(&biz.WebHook{}, id).Error; err != nil {
return err
}
// 记录日志
r.log.Info("webhook deleted", slog.String("type", biz.OperationTypeWebhook), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(id)), slog.String("name", webhook.Name))
return nil
}
func (r *webhookRepo) Call(key string) (string, error) {

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
@@ -35,6 +36,7 @@ import (
type websiteRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
cache biz.CacheRepo
database biz.DatabaseRepo
databaseServer biz.DatabaseServerRepo
@@ -44,10 +46,11 @@ type websiteRepo struct {
setting biz.SettingRepo
}
func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, database biz.DatabaseRepo, databaseServer biz.DatabaseServerRepo, databaseUser biz.DatabaseUserRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo, setting biz.SettingRepo) biz.WebsiteRepo {
func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, cache biz.CacheRepo, database biz.DatabaseRepo, databaseServer biz.DatabaseServerRepo, databaseUser biz.DatabaseUserRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo, setting biz.SettingRepo) biz.WebsiteRepo {
return &websiteRepo{
t: t,
db: db,
log: log,
cache: cache,
database: database,
databaseServer: databaseServer,
@@ -232,7 +235,7 @@ func (r *websiteRepo) List(typ string, page, limit uint) ([]*biz.Website, int64,
return websites, total, nil
}
func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (*biz.Website, error) {
w := &biz.Website{
Name: req.Name,
Type: biz.WebsiteType(req.Type),
@@ -425,6 +428,9 @@ location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
return nil, err
}
// 记录日志
r.log.Info("website created", slog.String("type", biz.OperationTypeWebsite), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("name", req.Name), slog.String("website_type", req.Type), slog.String("path", req.Path))
// 重载 Web 服务器
if err = r.reloadWebServer(); err != nil {
return nil, err
@@ -437,7 +443,7 @@ location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
if err != nil {
return nil, errors.New(r.t.Get("can't find %s database server, please add it first", name))
}
if err = r.database.Create(&request.DatabaseCreate{
if err = r.database.Create(ctx, &request.DatabaseCreate{
ServerID: server.ID,
Name: req.DBName,
CreateUser: true,
@@ -453,7 +459,7 @@ location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
return w, nil
}
func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) error {
website := new(biz.Website)
if err := r.db.Where("id", req.ID).First(website).Error; err != nil {
return err
@@ -585,10 +591,13 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
return err
}
// 记录日志
r.log.Info("website updated", slog.String("type", biz.OperationTypeWebsite), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", website.Name))
return r.reloadWebServer()
}
func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
func (r *websiteRepo) Delete(ctx context.Context, req *request.WebsiteDelete) error {
website := new(biz.Website)
if err := r.db.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil {
return err
@@ -605,11 +614,11 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
if req.DB {
if mysql, err := r.databaseServer.GetByName("local_mysql"); err == nil {
_ = r.databaseUser.DeleteByNames(mysql.ID, []string{website.Name})
_ = r.database.Delete(mysql.ID, website.Name)
_ = r.database.Delete(ctx, mysql.ID, website.Name)
}
if postgres, err := r.databaseServer.GetByName("local_postgresql"); err == nil {
_ = r.databaseUser.DeleteByNames(postgres.ID, []string{website.Name})
_ = r.database.Delete(postgres.ID, website.Name)
_ = r.database.Delete(ctx, postgres.ID, website.Name)
}
}
@@ -617,6 +626,9 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
return err
}
// 记录日志
r.log.Info("website deleted", slog.String("type", biz.OperationTypeWebsite), slog.Uint64("operator_id", getOperatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("name", website.Name))
return r.reloadWebServer()
}
@@ -791,7 +803,7 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
newCert, err := r.cert.GetByWebsite(website.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
newCert, err = r.cert.Create(&request.CertCreate{
newCert, err = r.cert.Create(ctx, &request.CertCreate{
Type: string(acme.KeyEC256),
Domains: website.Domains,
AutoRenewal: true,

View File

@@ -0,0 +1,7 @@
package request
// LogList 日志列表请求
type LogList struct {
Type string `json:"type" form:"type" query:"type" validate:"required|in:app,db,http"`
Limit int `json:"limit" form:"limit" query:"limit" validate:"min:1|max:1000"`
}

View File

@@ -44,7 +44,7 @@ func (r *CertRenew) Run() {
var certs []biz.Cert
if err := r.db.Preload("Website").Preload("Account").Preload("DNS").Find(&certs).Error; err != nil {
r.log.Warn("[CertRenew] failed to get certs", slog.Any("err", err))
r.log.Warn("failed to get certs", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
@@ -58,7 +58,7 @@ func (r *CertRenew) Run() {
if cert.RenewalInfo.NeedsRefresh() {
renewInfo, err := r.certRepo.RefreshRenewalInfo(cert.ID)
if err != nil {
r.log.Warn("[CertRenew] failed to refresh renewal info", slog.Any("err", err))
r.log.Warn("failed to refresh renewal info", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
continue
}
cert.RenewalInfo = renewInfo
@@ -67,7 +67,7 @@ func (r *CertRenew) Run() {
// 到达建议时间,续签证书
if time.Now().After(cert.RenewalInfo.SelectedTime) {
if _, err := r.certRepo.Renew(cert.ID); err != nil {
r.log.Warn("[CertRenew] failed to renew cert", slog.Any("err", err))
r.log.Warn("failed to renew cert", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
}
}
@@ -76,7 +76,7 @@ func (r *CertRenew) Run() {
if r.conf.HTTP.ACME {
decode, err := pkgcert.ParseCert(filepath.Join(app.Root, "panel/storage/cert.pem"))
if err != nil {
r.log.Warn("[CertRenew] failed to parse panel cert", slog.Any("err", err))
r.log.Warn("failed to parse panel cert", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
// 结束时间大于 2 天不续签
@@ -86,28 +86,28 @@ func (r *CertRenew) Run() {
ip, err := r.settingRepo.Get(biz.SettingKeyPublicIPs)
if err != nil {
r.log.Warn("[CertRenew] failed to get panel IP", slog.Any("err", err))
r.log.Warn("failed to get panel IP", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
var ips []string
if err = json.Unmarshal([]byte(ip), &ips); err != nil || len(ips) == 0 {
r.log.Warn("[CertRenew] panel public IPs not set", slog.Any("err", err))
r.log.Warn("panel public IPs not set", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
var user biz.User
if err = r.db.First(&user).Error; err != nil {
r.log.Warn("[CertRenew] failed to get a panel user", slog.Any("err", err))
r.log.Warn("failed to get a panel user", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
account, err := r.certAccountRepo.GetDefault(user.ID)
if err != nil {
r.log.Warn("[CertRenew] failed to get panel ACME account", slog.Any("err", err))
r.log.Warn("failed to get panel ACME account", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
crt, key, err := r.certRepo.ObtainPanel(account, ips)
if err != nil {
r.log.Warn("[CertRenew] failed to obtain ACME cert", slog.Any("err", err))
r.log.Warn("failed to obtain ACME cert", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
@@ -115,11 +115,11 @@ func (r *CertRenew) Run() {
Cert: string(crt),
Key: string(key),
}); err != nil {
r.log.Warn("[CertRenew] failed to update panel cert", slog.Any("err", err))
r.log.Warn("failed to update panel cert", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
r.log.Info("[CertRenew] panel cert renewed successfully")
r.log.Info("panel cert renewed successfully", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0))
tools.RestartPanel()
}

View File

@@ -48,7 +48,7 @@ func (r *Monitoring) Run() {
}
if err = r.db.Create(&biz.Monitor{Info: info}).Error; err != nil {
r.log.Warn("[Monitor] failed to create monitor record", slog.Any("err", err))
r.log.Warn("failed to create monitor record", slog.String("type", biz.OperationTypeMonitor), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
@@ -62,7 +62,7 @@ func (r *Monitoring) Run() {
return
}
if err = r.db.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format(time.DateTime)).Delete(&biz.Monitor{}).Error; err != nil {
r.log.Warn("[Monitor] failed to delete monitor record", slog.Any("err", err))
r.log.Warn("failed to delete monitor record", slog.String("type", biz.OperationTypeMonitor), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
}

View File

@@ -1,6 +1,7 @@
package job
import (
"context"
"fmt"
"log/slog"
"math/rand/v2"
@@ -49,29 +50,29 @@ func (r *PanelTask) Run() {
// 优化数据库
if err := r.db.Exec("VACUUM").Error; err != nil {
app.Status = app.StatusFailed
r.log.Warn("[PanelTask] failed to vacuum database", slog.Any("err", err))
r.log.Warn("failed to vacuum database", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
if err := r.db.Exec("PRAGMA journal_mode=WAL;").Error; err != nil {
app.Status = app.StatusFailed
r.log.Warn("[PanelTask] failed to set database journal_mode to WAL", slog.Any("err", err))
r.log.Warn("failed to set database journal_mode to WAL", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
if err := r.db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
app.Status = app.StatusFailed
r.log.Warn("[PanelTask] failed to wal checkpoint database", slog.Any("err", err))
r.log.Warn("failed to wal checkpoint database", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
return
}
// 备份面板
if err := r.backupRepo.Create(biz.BackupTypePanel, ""); err != nil {
r.log.Warn("[PanelTask] failed to backup panel", slog.Any("err", err))
if err := r.backupRepo.Create(context.Background(), biz.BackupTypePanel, ""); err != nil {
r.log.Warn("failed to backup panel", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
// 清理备份
if path, err := r.backupRepo.GetPath("panel"); err == nil {
if err = r.backupRepo.ClearExpired(path, "panel_", 10); err != nil {
r.log.Warn("[PanelTask] failed to clear backup", slog.Any("err", err))
r.log.Warn("failed to clear backup", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
}
@@ -96,7 +97,7 @@ func (r *PanelTask) Run() {
func (r *PanelTask) updateCategories() {
time.AfterFunc(time.Duration(rand.IntN(300))*time.Second, func() {
if err := r.cacheRepo.UpdateCategories(); err != nil {
r.log.Warn("[PanelTask] failed to update categories cache", slog.Any("err", err))
r.log.Warn("failed to update categories cache", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
})
}
@@ -105,7 +106,7 @@ func (r *PanelTask) updateCategories() {
func (r *PanelTask) updateApps() {
time.AfterFunc(time.Duration(rand.IntN(300))*time.Second, func() {
if err := r.cacheRepo.UpdateApps(); err != nil {
r.log.Warn("[PanelTask] failed to update apps cache", slog.Any("err", err))
r.log.Warn("failed to update apps cache", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
})
}
@@ -114,7 +115,7 @@ func (r *PanelTask) updateApps() {
func (r *PanelTask) updateRewrites() {
time.AfterFunc(time.Duration(rand.IntN(300))*time.Second, func() {
if err := r.cacheRepo.UpdateRewrites(); err != nil {
r.log.Warn("[PanelTask] failed to update rewrites cache", slog.Any("err", err))
r.log.Warn("failed to update rewrites cache", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
}
})
}
@@ -148,7 +149,7 @@ func (r *PanelTask) updatePanel() {
url := fmt.Sprintf("https://%s%s", r.conf.App.DownloadEndpoint, download.URL)
checksum := fmt.Sprintf("https://%s%s", r.conf.App.DownloadEndpoint, download.Checksum)
if err = r.backupRepo.UpdatePanel(panel.Version, url, checksum); err != nil {
r.log.Warn("[PanelTask] failed to update panel", slog.Any("err", err))
r.log.Warn("failed to update panel", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
_ = r.backupRepo.FixPanel()
}
}

View File

@@ -44,6 +44,7 @@ type Http struct {
containerImage *service.ContainerImageService
containerVolume *service.ContainerVolumeService
file *service.FileService
log *service.LogService
monitor *service.MonitorService
setting *service.SettingService
systemctl *service.SystemctlService
@@ -85,6 +86,7 @@ func NewHttp(
containerImage *service.ContainerImageService,
containerVolume *service.ContainerVolumeService,
file *service.FileService,
log *service.LogService,
monitor *service.MonitorService,
setting *service.SettingService,
systemctl *service.SystemctlService,
@@ -125,6 +127,7 @@ func NewHttp(
containerImage: containerImage,
containerVolume: containerVolume,
file: file,
log: log,
monitor: monitor,
setting: setting,
systemctl: systemctl,
@@ -428,6 +431,10 @@ func (route *Http) Register(r *chi.Mux) {
r.Get("/list", route.file.List)
})
r.Route("/log", func(r chi.Router) {
r.Get("/list", route.log.List)
})
r.Route("/monitor", func(r chi.Router) {
r.Get("/setting", route.monitor.GetSetting)
r.Post("/setting", route.monitor.UpdateSetting)

View File

@@ -50,7 +50,7 @@ func (s *BackupService) Create(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.backupRepo.Create(biz.BackupType(req.Type), req.Target, req.Path); err != nil {
if err = s.backupRepo.Create(r.Context(), biz.BackupType(req.Type), req.Target, req.Path); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -111,7 +111,7 @@ func (s *BackupService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.backupRepo.Delete(biz.BackupType(req.Type), req.File); err != nil {
if err = s.backupRepo.Delete(r.Context(), biz.BackupType(req.Type), req.File); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -126,7 +126,7 @@ func (s *BackupService) Restore(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.backupRepo.Restore(biz.BackupType(req.Type), req.File, req.Target); err != nil {
if err = s.backupRepo.Restore(r.Context(), biz.BackupType(req.Type), req.File, req.Target); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -143,7 +143,7 @@ func (s *CertService) Upload(w http.ResponseWriter, r *http.Request) {
return
}
cert, err := s.certRepo.Upload(req)
cert, err := s.certRepo.Upload(r.Context(), req)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -159,7 +159,7 @@ func (s *CertService) Create(w http.ResponseWriter, r *http.Request) {
return
}
cert, err := s.certRepo.Create(req)
cert, err := s.certRepo.Create(r.Context(), req)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -175,7 +175,7 @@ func (s *CertService) Update(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.certRepo.Update(req); err != nil {
if err = s.certRepo.Update(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -206,7 +206,7 @@ func (s *CertService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
err = s.certRepo.Delete(req.ID)
err = s.certRepo.Delete(r.Context(), req.ID)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return

View File

@@ -45,7 +45,7 @@ func (s *CertAccountService) Create(w http.ResponseWriter, r *http.Request) {
return
}
account, err := s.certAccountRepo.Create(req)
account, err := s.certAccountRepo.Create(r.Context(), req)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -61,7 +61,7 @@ func (s *CertAccountService) Update(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.certAccountRepo.Update(req); err != nil {
if err = s.certAccountRepo.Update(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -92,7 +92,7 @@ func (s *CertAccountService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.certAccountRepo.Delete(req.ID); err != nil {
if err = s.certAccountRepo.Delete(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -45,7 +45,7 @@ func (s *CertDNSService) Create(w http.ResponseWriter, r *http.Request) {
return
}
certDNS, err := s.certDNSRepo.Create(req)
certDNS, err := s.certDNSRepo.Create(r.Context(), req)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -61,7 +61,7 @@ func (s *CertDNSService) Update(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.certDNSRepo.Update(req); err != nil {
if err = s.certDNSRepo.Update(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -92,7 +92,7 @@ func (s *CertDNSService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.certDNSRepo.Delete(req.ID); err != nil {
if err = s.certDNSRepo.Delete(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -542,7 +542,7 @@ func (s *CliService) WebsiteCreate(ctx context.Context, cmd *cli.Command) error
DB: false,
}
website, err := s.websiteRepo.Create(req)
website, err := s.websiteRepo.Create(ctx, req)
if err != nil {
return err
}
@@ -560,7 +560,7 @@ func (s *CliService) WebsiteRemove(ctx context.Context, cmd *cli.Command) error
ID: website.ID,
}
if err = s.websiteRepo.Delete(req); err != nil {
if err = s.websiteRepo.Delete(ctx, req); err != nil {
return err
}
@@ -579,7 +579,7 @@ func (s *CliService) WebsiteDelete(ctx context.Context, cmd *cli.Command) error
DB: true,
}
if err = s.websiteRepo.Delete(req); err != nil {
if err = s.websiteRepo.Delete(ctx, req); err != nil {
return err
}
@@ -631,7 +631,7 @@ func (s *CliService) BackupWebsite(ctx context.Context, cmd *cli.Command) error
fmt.Println(s.hr)
fmt.Println(s.t.Get("|-Backup type: website"))
fmt.Println(s.t.Get("|-Backup target: %s", cmd.String("name")))
if err := s.backupRepo.Create(biz.BackupTypeWebsite, cmd.String("name"), cmd.String("path")); err != nil {
if err := s.backupRepo.Create(ctx, biz.BackupTypeWebsite, cmd.String("name"), cmd.String("path")); err != nil {
return errors.New(s.t.Get("Backup failed: %v", err))
}
fmt.Println(s.hr)
@@ -647,7 +647,7 @@ func (s *CliService) BackupDatabase(ctx context.Context, cmd *cli.Command) error
fmt.Println(s.t.Get("|-Backup type: database"))
fmt.Println(s.t.Get("|-Database: %s", cmd.String("type")))
fmt.Println(s.t.Get("|-Backup target: %s", cmd.String("name")))
if err := s.backupRepo.Create(biz.BackupType(cmd.String("type")), cmd.String("name"), cmd.String("path")); err != nil {
if err := s.backupRepo.Create(ctx, biz.BackupType(cmd.String("type")), cmd.String("name"), cmd.String("path")); err != nil {
return errors.New(s.t.Get("Backup failed: %v", err))
}
fmt.Println(s.hr)
@@ -661,7 +661,7 @@ func (s *CliService) BackupPanel(ctx context.Context, cmd *cli.Command) error {
fmt.Println(s.t.Get("★ Start backup [%s]", time.Now().Format(time.DateTime)))
fmt.Println(s.hr)
fmt.Println(s.t.Get("|-Backup type: panel"))
if err := s.backupRepo.Create(biz.BackupTypePanel, "", cmd.String("path")); err != nil {
if err := s.backupRepo.Create(ctx, biz.BackupTypePanel, "", cmd.String("path")); err != nil {
return errors.New(s.t.Get("Backup failed: %v", err))
}
fmt.Println(s.hr)
@@ -948,7 +948,7 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
return errors.New(s.t.Get("Initialization failed: %v", err))
}
_, err = s.userRepo.Create("admin", value, str.Random(8)+"@yourdomain.com")
_, err = s.userRepo.Create(ctx, "admin", value, str.Random(8)+"@yourdomain.com")
if err != nil {
return errors.New(s.t.Get("Initialization failed: %v", err))
}

View File

@@ -45,7 +45,7 @@ func (s *CronService) Create(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.cronRepo.Create(req); err != nil {
if err = s.cronRepo.Create(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -60,7 +60,7 @@ func (s *CronService) Update(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.cronRepo.Update(req); err != nil {
if err = s.cronRepo.Update(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -91,7 +91,7 @@ func (s *CronService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.cronRepo.Delete(req.ID); err != nil {
if err = s.cronRepo.Delete(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -45,7 +45,7 @@ func (s *DatabaseService) Create(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.databaseRepo.Create(req); err != nil {
if err = s.databaseRepo.Create(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -60,7 +60,7 @@ func (s *DatabaseService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.databaseRepo.Delete(req.ServerID, req.Name); err != nil {
if err = s.databaseRepo.Delete(r.Context(), req.ServerID, req.Name); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -45,7 +45,7 @@ func (s *DatabaseUserService) Create(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.databaseUserRepo.Create(req); err != nil {
if err = s.databaseUserRepo.Create(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -106,7 +106,7 @@ func (s *DatabaseUserService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.databaseUserRepo.Delete(req.ID); err != nil {
if err = s.databaseUserRepo.Delete(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

40
internal/service/log.go Normal file
View File

@@ -0,0 +1,40 @@
package service
import (
"net/http"
"github.com/acepanel/panel/internal/biz"
"github.com/acepanel/panel/internal/http/request"
)
type LogService struct {
logRepo biz.LogRepo
}
func NewLogService(logRepo biz.LogRepo) *LogService {
return &LogService{
logRepo: logRepo,
}
}
// List 获取日志列表
func (s *LogService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.LogList](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
// 默认限制
if req.Limit == 0 {
req.Limit = 100
}
entries, err := s.logRepo.List(req.Type, req.Limit)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, entries)
}

View File

@@ -63,7 +63,7 @@ func (s *ProjectService) Create(w http.ResponseWriter, r *http.Request) {
return
}
project, err := s.projectRepo.Create(req)
project, err := s.projectRepo.Create(r.Context(), req)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -79,7 +79,7 @@ func (s *ProjectService) Update(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.projectRepo.Update(req); err != nil {
if err = s.projectRepo.Update(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -94,7 +94,7 @@ func (s *ProjectService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.projectRepo.Delete(req.ID); err != nil {
if err = s.projectRepo.Delete(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -38,7 +38,7 @@ func (s *SafeService) UpdateSSH(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.safeRepo.UpdateSSH(req.Port, req.Status); err != nil {
if err = s.safeRepo.UpdateSSH(r.Context(), req.Port, req.Status); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -63,7 +63,7 @@ func (s *SafeService) UpdatePingStatus(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.safeRepo.UpdatePingStatus(req.Status); err != nil {
if err = s.safeRepo.UpdatePingStatus(r.Context(), req.Status); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -24,6 +24,7 @@ var ProviderSet = wire.NewSet(
NewFileService,
NewFirewallService,
NewHomeService,
NewLogService,
NewMonitorService,
NewProcessService,
NewProjectService,

View File

@@ -49,7 +49,7 @@ func (s *SettingService) Update(w http.ResponseWriter, r *http.Request) {
}
restart := false
if restart, err = s.settingRepo.UpdatePanel(req); err != nil {
if restart, err = s.settingRepo.UpdatePanel(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -45,7 +45,7 @@ func (s *SSHService) Create(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.sshRepo.Create(req); err != nil {
if err = s.sshRepo.Create(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -60,7 +60,7 @@ func (s *SSHService) Update(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.sshRepo.Update(req); err != nil {
if err = s.sshRepo.Update(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -91,7 +91,7 @@ func (s *SSHService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.sshRepo.Delete(req.ID); err != nil {
if err = s.sshRepo.Delete(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -265,7 +265,7 @@ func (s *UserService) Create(w http.ResponseWriter, r *http.Request) {
return
}
user, err := s.userRepo.Create(req.Username, req.Password, req.Email)
user, err := s.userRepo.Create(r.Context(), req.Username, req.Password, req.Email)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -281,7 +281,7 @@ func (s *UserService) UpdateUsername(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.userRepo.UpdateUsername(req.ID, req.Username); err != nil {
if err = s.userRepo.UpdateUsername(r.Context(), req.ID, req.Username); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -296,7 +296,7 @@ func (s *UserService) UpdatePassword(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.userRepo.UpdatePassword(req.ID, req.Password); err != nil {
if err = s.userRepo.UpdatePassword(r.Context(), req.ID, req.Password); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -311,7 +311,7 @@ func (s *UserService) UpdateEmail(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.userRepo.UpdateEmail(req.ID, req.Email); err != nil {
if err = s.userRepo.UpdateEmail(r.Context(), req.ID, req.Email); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -367,7 +367,7 @@ func (s *UserService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.userRepo.Delete(req.ID); err != nil {
if err = s.userRepo.Delete(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -61,7 +61,7 @@ func (s *WebHookService) Create(w http.ResponseWriter, r *http.Request) {
return
}
webhook, err := s.webhookRepo.Create(req)
webhook, err := s.webhookRepo.Create(r.Context(), req)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
@@ -77,7 +77,7 @@ func (s *WebHookService) Update(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.webhookRepo.Update(req); err != nil {
if err = s.webhookRepo.Update(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -92,7 +92,7 @@ func (s *WebHookService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.webhookRepo.Delete(req.ID); err != nil {
if err = s.webhookRepo.Delete(r.Context(), req.ID); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -113,7 +113,7 @@ func (s *WebsiteService) Create(w http.ResponseWriter, r *http.Request) {
req.Path = filepath.Join(req.Path, req.Name, "public")
}
if _, err = s.websiteRepo.Create(req); err != nil {
if _, err = s.websiteRepo.Create(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -144,7 +144,7 @@ func (s *WebsiteService) Update(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.websiteRepo.Update(req); err != nil {
if err = s.websiteRepo.Update(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -159,7 +159,7 @@ func (s *WebsiteService) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if err = s.websiteRepo.Delete(req); err != nil {
if err = s.websiteRepo.Delete(r.Context(), req); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}

View File

@@ -0,0 +1,7 @@
import { http } from '@/utils'
export default {
// 获取日志列表
list: (type: 'app' | 'db' | 'http', limit: number = 100): any =>
http.Get('/log/list', { params: { type, limit } })
}

View File

@@ -0,0 +1,121 @@
<script setup lang="ts">
defineOptions({
name: 'database-log'
})
import { NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import log from '@/api/panel/log'
const { $gettext } = useGettext()
// 日志条目类型定义
interface LogEntry {
time: string
level: string
msg: string
extra?: Record<string, any>
}
// 数据加载
const limit = ref(200)
const { loading, data, send: refresh } = useRequest(
() => log.list('db', limit.value),
{ initialData: [] }
)
// 表格列配置
const columns = [
{
title: $gettext('Time'),
key: 'time',
width: 180,
render: (row: LogEntry) => {
const date = new Date(row.time)
return date.toLocaleString()
}
},
{
title: $gettext('Level'),
key: 'level',
width: 80,
render: (row: LogEntry) => {
const typeMap: Record<string, 'success' | 'warning' | 'error' | 'info'> = {
INFO: 'success',
WARN: 'warning',
ERROR: 'error',
DEBUG: 'info'
}
return h(NTag, { type: typeMap[row.level] || 'default', size: 'small' }, () => row.level)
}
},
{
title: $gettext('Query'),
key: 'query',
ellipsis: {
tooltip: true
},
render: (row: LogEntry) => {
return row.extra?.query || row.msg || '-'
}
},
{
title: $gettext('Duration'),
key: 'duration',
width: 120,
render: (row: LogEntry) => {
if (row.extra?.duration) {
// 纳秒转毫秒
const ms = Number(row.extra.duration) / 1000000
return `${ms.toFixed(2)} ms`
}
return '-'
}
},
{
title: $gettext('Rows'),
key: 'rows',
width: 80,
render: (row: LogEntry) => {
return row.extra?.rows !== undefined ? row.extra.rows : '-'
}
}
]
// 刷新
const handleRefresh = () => {
refresh()
}
</script>
<template>
<div class="flex flex-col h-full">
<div class="mb-4 flex gap-4 items-center">
<span>{{ $gettext('Show entries') }}:</span>
<n-select
v-model:value="limit"
:options="[
{ label: '100', value: 100 },
{ label: '200', value: 200 },
{ label: '500', value: 500 },
{ label: '1000', value: 1000 }
]"
class="w-100px"
@update:value="handleRefresh"
/>
<n-button type="primary" @click="handleRefresh">
{{ $gettext('Refresh') }}
</n-button>
</div>
<n-data-table
:columns="columns"
:data="data"
:loading="loading"
:bordered="false"
:max-height="600"
:scroll-x="800"
virtual-scroll
/>
</div>
</template>

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
defineOptions({
name: 'http-log'
})
import { NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import log from '@/api/panel/log'
const { $gettext } = useGettext()
// 日志条目类型定义
interface LogEntry {
time: string
level: string
msg: string
extra?: Record<string, any>
}
// 数据加载
const limit = ref(200)
const { loading, data, send: refresh } = useRequest(
() => log.list('http', limit.value),
{ initialData: [] }
)
// 获取状态码颜色
const getStatusType = (code: number): 'success' | 'warning' | 'error' | 'info' => {
if (code >= 200 && code < 300) return 'success'
if (code >= 300 && code < 400) return 'info'
if (code >= 400 && code < 500) return 'warning'
return 'error'
}
// 表格列配置
const columns = [
{
title: $gettext('Time'),
key: 'time',
width: 180,
render: (row: LogEntry) => {
const date = new Date(row.time)
return date.toLocaleString()
}
},
{
title: $gettext('Method'),
key: 'method',
width: 80,
render: (row: LogEntry) => {
const method = row.extra?.['http.request.method'] || '-'
const colorMap: Record<string, string> = {
GET: '#52c41a',
POST: '#1890ff',
PUT: '#faad14',
DELETE: '#ff4d4f',
PATCH: '#722ed1'
}
return h('span', { style: { color: colorMap[method] || 'inherit', fontWeight: 'bold' } }, method)
}
},
{
title: $gettext('Path'),
key: 'path',
ellipsis: {
tooltip: true
},
render: (row: LogEntry) => {
return row.extra?.['url.path'] || '-'
}
},
{
title: $gettext('Status'),
key: 'status',
width: 80,
render: (row: LogEntry) => {
const status = row.extra?.['http.response.status_code']
if (status) {
return h(NTag, { type: getStatusType(status), size: 'small' }, () => status)
}
return '-'
}
},
{
title: $gettext('Duration'),
key: 'duration',
width: 120,
render: (row: LogEntry) => {
const duration = row.extra?.['event.duration']
if (duration) {
// 纳秒转毫秒
const ms = Number(duration) / 1000000
return `${ms.toFixed(2)} ms`
}
return '-'
}
},
{
title: $gettext('Client IP'),
key: 'client_ip',
width: 150,
render: (row: LogEntry) => {
const ip = row.extra?.['client.ip'] || '-'
// 移除端口号
return typeof ip === 'string' && ip.includes(':') ? ip.split(':')[0] : ip
}
}
]
// 刷新
const handleRefresh = () => {
refresh()
}
</script>
<template>
<div class="flex flex-col h-full">
<div class="mb-4 flex gap-4 items-center">
<span>{{ $gettext('Show entries') }}:</span>
<n-select
v-model:value="limit"
:options="[
{ label: '100', value: 100 },
{ label: '200', value: 200 },
{ label: '500', value: 500 },
{ label: '1000', value: 1000 }
]"
class="w-100px"
@update:value="handleRefresh"
/>
<n-button type="primary" @click="handleRefresh">
{{ $gettext('Refresh') }}
</n-button>
</div>
<n-data-table
:columns="columns"
:data="data"
:loading="loading"
:bordered="false"
:max-height="600"
:scroll-x="800"
virtual-scroll
/>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
defineOptions({
name: 'log-index'
})
import { useGettext } from 'vue3-gettext'
import DatabaseLog from './DatabaseLog.vue'
import HttpLog from './HttpLog.vue'
import OperationLog from './OperationLog.vue'
const { $gettext } = useGettext()
// 当前激活的标签
const activeTab = ref('operation')
</script>
<template>
<common-page show-header show-footer>
<n-tabs v-model:value="activeTab" type="line" animated>
<n-tab-pane name="operation" :tab="$gettext('Operation Log')">
<OperationLog />
</n-tab-pane>
<n-tab-pane name="database" :tab="$gettext('Database Log')">
<DatabaseLog />
</n-tab-pane>
<n-tab-pane name="http" :tab="$gettext('HTTP Log')">
<HttpLog />
</n-tab-pane>
</n-tabs>
</common-page>
</template>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
defineOptions({
name: 'operation-log'
})
import { NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import log from '@/api/panel/log'
const { $gettext } = useGettext()
// 日志条目类型定义
interface LogEntry {
time: string
level: string
msg: string
type?: string
operator_id?: number
operator_name?: string
extra?: Record<string, any>
}
// 数据加载
const limit = ref(200)
const { loading, data, send: refresh } = useRequest(
() => log.list('app', limit.value),
{ initialData: [] }
)
// 表格列配置
const columns = [
{
title: $gettext('Time'),
key: 'time',
width: 180,
render: (row: LogEntry) => {
const date = new Date(row.time)
return date.toLocaleString()
}
},
{
title: $gettext('Level'),
key: 'level',
width: 80,
render: (row: LogEntry) => {
const typeMap: Record<string, 'success' | 'warning' | 'error' | 'info'> = {
INFO: 'success',
WARN: 'warning',
ERROR: 'error',
DEBUG: 'info'
}
return h(NTag, { type: typeMap[row.level] || 'default', size: 'small' }, () => row.level)
}
},
{
title: $gettext('Type'),
key: 'type',
width: 120,
render: (row: LogEntry) => {
return row.type || '-'
}
},
{
title: $gettext('Operator'),
key: 'operator_name',
width: 120,
render: (row: LogEntry) => {
if (row.operator_id === 0 || row.operator_id === undefined) {
return $gettext('System')
}
return row.operator_name || `#${row.operator_id}`
}
},
{
title: $gettext('Message'),
key: 'msg',
ellipsis: {
tooltip: true
}
}
]
// 刷新
const handleRefresh = () => {
refresh()
}
</script>
<template>
<div class="flex flex-col h-full">
<div class="mb-4 flex gap-4 items-center">
<span>{{ $gettext('Show entries') }}:</span>
<n-select
v-model:value="limit"
:options="[
{ label: '100', value: 100 },
{ label: '200', value: 200 },
{ label: '500', value: 500 },
{ label: '1000', value: 1000 }
]"
class="w-100px"
@update:value="handleRefresh"
/>
<n-button type="primary" @click="handleRefresh">
{{ $gettext('Refresh') }}
</n-button>
</div>
<n-data-table
:columns="columns"
:data="data"
:loading="loading"
:bordered="false"
:max-height="600"
:scroll-x="800"
virtual-scroll
/>
</div>
</template>

View File

@@ -0,0 +1,25 @@
import type { RouteType } from '~/types/router'
const Layout = () => import('@/layout/IndexView.vue')
export default {
name: 'log',
path: '/log',
component: Layout,
meta: {
order: 35
},
children: [
{
name: 'log-index',
path: '',
component: () => import('./IndexView.vue'),
meta: {
title: 'Logs',
icon: 'mdi:file-document-outline',
role: ['admin'],
requireAuth: true
}
}
]
} as RouteType