chore: opt settings code
This commit is contained in:
@@ -12,6 +12,7 @@ baihu
|
|||||||
data/
|
data/
|
||||||
# logs/
|
# logs/
|
||||||
# scripts/
|
# scripts/
|
||||||
|
configs/config.ini
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
+6
-5
@@ -50,7 +50,7 @@ RUN sed -i 's@deb.debian.org@mirrors.tuna.tsinghua.edu.cn@g' /etc/apt/sources.li
|
|||||||
&& echo "${TZ}" > /etc/timezone \
|
&& echo "${TZ}" > /etc/timezone \
|
||||||
&& ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
|
&& ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \
|
||||||
&& apt update \
|
&& apt update \
|
||||||
&& apt install -y tzdata git gcc curl wget vim ca-certificates procps htop \
|
&& apt install -y tzdata git gcc curl wget vim ca-certificates procps htop e2fsprogs \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& curl -sSL https://gh-proxy.com/https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh -o /tmp/miniforge.sh \
|
&& curl -sSL https://gh-proxy.com/https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh -o /tmp/miniforge.sh \
|
||||||
&& bash /tmp/miniforge.sh -b -p ${CONDA_DIR} \
|
&& bash /tmp/miniforge.sh -b -p ${CONDA_DIR} \
|
||||||
@@ -68,11 +68,12 @@ COPY --from=backend-builder /app/baihu .
|
|||||||
# Copy config files
|
# Copy config files
|
||||||
COPY --from=backend-builder /app/configs ./configs
|
COPY --from=backend-builder /app/configs ./configs
|
||||||
|
|
||||||
# Create directories
|
# Copy entrypoint script
|
||||||
RUN mkdir -p ./data ./logs ./scripts
|
COPY docker-entrypoint.sh .
|
||||||
|
RUN chmod +x docker-entrypoint.sh
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8052
|
EXPOSE 8052
|
||||||
|
|
||||||
# Run
|
# Run with entrypoint
|
||||||
CMD ["./baihu"]
|
CMD ["./docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -162,31 +162,32 @@ make build-all
|
|||||||
│ ├── ql.db # SQLite 数据库
|
│ ├── ql.db # SQLite 数据库
|
||||||
│ └── scripts/ # 脚本文件存储
|
│ └── scripts/ # 脚本文件存储
|
||||||
└── configs/
|
└── configs/
|
||||||
└── config.json # 配置文件(自动创建)
|
└── config.ini # 配置文件(自动创建)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚙️ 配置说明
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
配置文件路径:`configs/config.json`
|
配置文件路径:`configs/config.ini`
|
||||||
|
|
||||||
```json
|
```ini
|
||||||
{
|
[server]
|
||||||
"server": {
|
port = 8052
|
||||||
"port": 8052,
|
host = 0.0.0.0
|
||||||
"host": "0.0.0.0"
|
|
||||||
},
|
[database]
|
||||||
"database": {
|
type = sqlite
|
||||||
"type": "sqlite",
|
host = localhost
|
||||||
"path": "./data/ql.db",
|
port = 3306
|
||||||
"table_prefix": "baihu_"
|
user = root
|
||||||
},
|
password =
|
||||||
"task": {
|
dbname = ql_panel
|
||||||
"default_timeout": 3600,
|
table_prefix = baihu_
|
||||||
"log_retention_days": 30
|
|
||||||
}
|
[task]
|
||||||
}
|
default_timeout = 3600
|
||||||
|
log_retention_days = 30
|
||||||
```
|
```
|
||||||
|
|
||||||
### 配置项说明
|
### 配置项说明
|
||||||
@@ -196,7 +197,6 @@ make build-all
|
|||||||
| `server.port` | 服务端口 | 8052 |
|
| `server.port` | 服务端口 | 8052 |
|
||||||
| `server.host` | 监听地址 | 0.0.0.0 |
|
| `server.host` | 监听地址 | 0.0.0.0 |
|
||||||
| `database.type` | 数据库类型 | sqlite |
|
| `database.type` | 数据库类型 | sqlite |
|
||||||
| `database.path` | 数据库路径 | ./data/ql.db |
|
|
||||||
| `database.table_prefix` | 表前缀 | baihu_ |
|
| `database.table_prefix` | 表前缀 | baihu_ |
|
||||||
| `task.default_timeout` | 任务默认超时(秒) | 3600 |
|
| `task.default_timeout` | 任务默认超时(秒) | 3600 |
|
||||||
| `task.log_retention_days` | 日志保留天数 | 30 |
|
| `task.log_retention_days` | 日志保留天数 | 30 |
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[server]
|
||||||
|
port = 8052
|
||||||
|
host = 0.0.0.0
|
||||||
|
|
||||||
|
[database]
|
||||||
|
type = sqlite
|
||||||
|
host = localhost
|
||||||
|
port = 3306
|
||||||
|
user = root
|
||||||
|
password =
|
||||||
|
dbname = ql_panel
|
||||||
|
table_prefix = baihu_
|
||||||
|
|
||||||
|
[task]
|
||||||
|
default_timeout = 3600
|
||||||
|
log_retention_days = 30
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"server": {
|
|
||||||
"port": 8052,
|
|
||||||
"host": "0.0.0.0"
|
|
||||||
},
|
|
||||||
"database": {
|
|
||||||
"type": "sqlite",
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 3306,
|
|
||||||
"user": "root",
|
|
||||||
"password": "",
|
|
||||||
"dbname": "ql_panel",
|
|
||||||
"path": "./data/ql.db",
|
|
||||||
"table_prefix": "baihu_"
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"password_salt": "ql_panel_salt"
|
|
||||||
},
|
|
||||||
"task": {
|
|
||||||
"default_timeout": 3600,
|
|
||||||
"log_retention_days": 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
mkdir -p /app/data /app/data/scripts /app/configs
|
||||||
|
|
||||||
|
# 如果数据库文件存在,设置不可变属性防止误删
|
||||||
|
if [ -f /app/data/ql.db ]; then
|
||||||
|
chattr +i /app/data/ql.db 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 如果 configs 目录存在,设置不可变属性
|
||||||
|
if [ -d /app/configs ]; then
|
||||||
|
chattr +i /app/configs 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 如果 scripts 目录存在,设置不可变属性
|
||||||
|
if [ -d /app/data/scripts ]; then
|
||||||
|
chattr +i /app/data/scripts 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
exec ./baihu
|
||||||
@@ -64,6 +64,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -62,17 +62,11 @@ func (a *App) initDatabase() {
|
|||||||
if err := database.Migrate(); err != nil {
|
if err := database.Migrate(); err != nil {
|
||||||
logger.Fatalf("Failed to migrate database: %v", err)
|
logger.Fatalf("Failed to migrate database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化默认设置
|
|
||||||
settingsService := services.NewSettingsService()
|
|
||||||
if err := settingsService.InitSettings(); err != nil {
|
|
||||||
logger.Warnf("Failed to init settings: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRouter() {
|
func (a *App) initRouter() {
|
||||||
ctrls, settingsService := router.RegisterControllers()
|
ctrls := router.RegisterControllers()
|
||||||
a.Router = router.Setup(ctrls, settingsService)
|
a.Router = router.Setup(ctrls)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Run() {
|
func (a *App) Run() {
|
||||||
|
|||||||
Vendored
+94
@@ -0,0 +1,94 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"baihu/internal/constant"
|
||||||
|
"baihu/internal/database"
|
||||||
|
"baihu/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// siteCache 站点设置内存缓存
|
||||||
|
var (
|
||||||
|
siteCache = make(map[string]string)
|
||||||
|
siteCacheMu sync.RWMutex
|
||||||
|
siteCacheInit bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadSiteCache 从数据库加载站点设置到缓存
|
||||||
|
func LoadSiteCache() {
|
||||||
|
siteCacheMu.Lock()
|
||||||
|
defer siteCacheMu.Unlock()
|
||||||
|
|
||||||
|
// 先填充默认值
|
||||||
|
if defaults, ok := constant.DefaultSettings[constant.SectionSite]; ok {
|
||||||
|
for k, v := range defaults {
|
||||||
|
siteCache[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据库加载覆盖
|
||||||
|
var settings []models.Setting
|
||||||
|
database.DB.Where("section = ?", constant.SectionSite).Find(&settings)
|
||||||
|
for _, setting := range settings {
|
||||||
|
siteCache[setting.Key] = setting.Value
|
||||||
|
}
|
||||||
|
siteCacheInit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureSiteCache 确保站点缓存已初始化
|
||||||
|
func ensureSiteCache() {
|
||||||
|
siteCacheMu.RLock()
|
||||||
|
init := siteCacheInit
|
||||||
|
siteCacheMu.RUnlock()
|
||||||
|
|
||||||
|
if !init {
|
||||||
|
LoadSiteCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSiteCache 从缓存获取站点设置
|
||||||
|
func GetSiteCache(key string) string {
|
||||||
|
ensureSiteCache()
|
||||||
|
|
||||||
|
siteCacheMu.RLock()
|
||||||
|
defer siteCacheMu.RUnlock()
|
||||||
|
|
||||||
|
if val, ok := siteCache[key]; ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
if def, ok := constant.DefaultSettings[constant.SectionSite][key]; ok {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSiteCache 更新缓存中的站点设置
|
||||||
|
func SetSiteCache(key, value string) {
|
||||||
|
siteCacheMu.Lock()
|
||||||
|
siteCache[key] = value
|
||||||
|
siteCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSiteCacheAll 获取整个站点设置缓存
|
||||||
|
func GetSiteCacheAll() map[string]string {
|
||||||
|
ensureSiteCache()
|
||||||
|
|
||||||
|
siteCacheMu.RLock()
|
||||||
|
defer siteCacheMu.RUnlock()
|
||||||
|
|
||||||
|
result := make(map[string]string)
|
||||||
|
for k, v := range siteCache {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSiteCacheBatch 批量更新缓存
|
||||||
|
func SetSiteCacheBatch(values map[string]string) {
|
||||||
|
siteCacheMu.Lock()
|
||||||
|
for k, v := range values {
|
||||||
|
siteCache[k] = v
|
||||||
|
}
|
||||||
|
siteCacheMu.Unlock()
|
||||||
|
}
|
||||||
@@ -3,11 +3,14 @@ package constant
|
|||||||
const (
|
const (
|
||||||
|
|
||||||
// ConfigPath 配置文件路径
|
// ConfigPath 配置文件路径
|
||||||
ConfigPath = "configs/config.json"
|
ConfigPath = "configs/config.ini"
|
||||||
|
|
||||||
// DataDir 数据目录
|
// DataDir 数据目录
|
||||||
DataDir = "./data"
|
DataDir = "./data"
|
||||||
|
|
||||||
|
// DefaultDBPath 默认数据库路径
|
||||||
|
DefaultDBPath = "./data/ql.db"
|
||||||
|
|
||||||
// WebDistDir 前端构建目录
|
// WebDistDir 前端构建目录
|
||||||
WebDistDir = "./web/dist"
|
WebDistDir = "./web/dist"
|
||||||
|
|
||||||
@@ -17,15 +20,9 @@ const (
|
|||||||
// AdminRole 管理员角色
|
// AdminRole 管理员角色
|
||||||
AdminRole = "admin"
|
AdminRole = "admin"
|
||||||
|
|
||||||
// DefaultTablePrefix 默认表前缀
|
|
||||||
DefaultTablePrefix = "baihu_"
|
|
||||||
|
|
||||||
// ScriptsWorkDir 脚本工作目录
|
// ScriptsWorkDir 脚本工作目录
|
||||||
ScriptsWorkDir = "./data/scripts"
|
ScriptsWorkDir = "./data/scripts"
|
||||||
|
|
||||||
// DefaultPageSize 默认分页大小
|
|
||||||
DefaultPageSize = 10
|
|
||||||
|
|
||||||
// CookieName Cookie 名称
|
// CookieName Cookie 名称
|
||||||
CookieName = "BHToken"
|
CookieName = "BHToken"
|
||||||
|
|
||||||
@@ -44,12 +41,14 @@ const (
|
|||||||
KeyCookieDays = "cookie_days"
|
KeyCookieDays = "cookie_days"
|
||||||
|
|
||||||
// System Settings Key 常量
|
// System Settings Key 常量
|
||||||
KeyJWTSecret = "jwt_secret"
|
|
||||||
KeyInitialized = "initialized"
|
KeyInitialized = "initialized"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TablePrefix 表前缀,可在运行时设置
|
// TablePrefix 表前缀,从配置文件读取
|
||||||
var TablePrefix = DefaultTablePrefix
|
var TablePrefix string
|
||||||
|
|
||||||
|
// Secret JWT和密码salt密钥,从配置文件读取
|
||||||
|
var Secret string
|
||||||
|
|
||||||
// DefaultIcon 默认站点图标
|
// DefaultIcon 默认站点图标
|
||||||
var DefaultIcon = `<svg t="1766107903919" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1942" width="200" height="200"><path d="M884.992 273.05984c4.10624 0 5.0688-2.36544 2.10944-5.25312 0 0-64.28672-65.55648-111.7696-75.55072-47.48288-9.984-45.47584-59.37152-80.56832-75.02848-72.0896-32.16384-158.6176-34.3552-158.6176-34.3552s-91.37152-4.92544-138.752-9.89184c-19.46624-2.03776-54.46656-9.58464-54.46656-9.58464-4.0448-0.84992-10.63936-1.19808-14.66368-0.44032 0 0-30.63808-0.07168-44.30848 43.35616-8.8576 28.11904 1.792 104.79616 1.792 104.79616 1.46432 12.27776-3.42016 30.21824-10.72128 40.20224 0 0-36.77184 46.03904-58.9312 100.34176-22.15936 54.30272 118.15936 145.05984 208.98816 205.27104C507.82208 613.89824 502.03648 743.424 502.03648 743.424s-74.19904-96.75776-194.00704-156.9792C188.2112 526.22336 150.30272 442.23488 150.30272 442.23488c-2.89792-5.45792-5.94944-4.94592-6.71744 1.21856 0 0-15.1552 91.61728 16.25088 147.8144 70.92224 126.88384 141.74208 112.88576 197.03808 183.27552s54.272 164.9152 54.272 164.9152-97.62816-141.29152-235.66336-205.55776c-91.53536-61.27616-74.7008-125.91104-85.49376-101.85728-10.79296 24.05376 26.73664 192.41984 65.40288 222.2592 80.57856 62.18752 94.16704 101.98016 94.16704 101.98016h175.53408S544.512 814.85824 572.928 725.73952c44.41088-139.30496 40.20224-191.26272 40.20224-191.26272 0.08192-8.22272 6.81984-14.19264 14.98112-13.29152 0 0 46.45888 4.64896 66.64192 9.68704 23.53152 5.86752 55.35744 26.20416 55.35744 26.20416 3.49184 2.14016 7.39328 0.68608 8.69376-3.1744l34.4576-102.77888c1.30048-3.8912-0.63488-5.51936-4.352-3.75808 0 0-45.37344 25.09824-88.17664 12.1856-20.15232-6.08256-59.60704-14.82752-74.69056-32.6656-16.95744-20.03968-15.59552-71.3728 26.66496-79.21664 48.0256-8.9088 33.13664 15.14496 65.91488 24.64768 27.42272 7.95648 22.29248-1.69984 26.69568 5.21216 1.67936 2.63168 0.38912 32.65536 0.38912 32.65536-0.21504 6.144 3.39968 7.84384 8.0384 3.79904l41.89184-36.46464c17.37728-11.24352 30.86336-0.57344 48.24064-11.81696 14.73536-9.53344 29.58336-43.66336 29.58336-43.66336 4.48512-9.24672 0.60416-20.41856-8.63232-24.92416l-42.58816-20.80768c-3.69664-1.80224-3.38944-3.26656 0.74752-3.26656h62.0032zM422.54336 123.87328s-49.88928 50.7392-74.5472 50.66752c-24.65792-0.07168-33.24928-56.12544-3.92192-56.12544 18.00192 0 76.32896 0.21504 76.32896 0.21504 4.13696 0.02048 5.0688 2.36544 2.14016 5.24288z m123.09504 249.64096s-3.31776-25.53856-33.16736-40.05888c-29.8496-14.52032-52.5312-41.13408-59.648-68.95616-12.1856-54.8864 48.29184-104.192 48.29184-104.192s-30.1056 73.6768 3.5328 106.60864c54.272 53.12512 40.99072 106.5984 40.99072 106.5984z m155.56608-164.46464c-7.94624 10.32192-9.60512 37.92896-59.2384 20.736-49.63328-17.2032-71.00416-54.5792-71.00416-54.5792-2.27328-3.40992-0.79872-6.49216 3.328-6.8096 0 0 43.55072-5.03808 80.06656 6.44096 24.91392 7.82336 54.79424 23.88992 46.848 34.21184z" fill="#272636" p-id="1943"></path><path d="M366.30528 259.26656c1.05472-1.76128 1.51552-1.57696 1.19808 0.43008 0 0-7.55712 19.0464 22.8864 87.63392 27.0848 61.02016 87.49056 68.7104 118.66112 98.23232 55.808 52.82816 53.52448 123.45344 53.52448 123.45344s-25.82528-49.85856-85.98528-78.83776-92.6208-50.52416-127.7952-85.77024c-45.02528-45.12768 17.5104-145.14176 17.5104-145.14176zM500.0704 961.44384h134.49216s95.8464-138.07616 46.68416-291.25632c-16.19968-50.46272-43.45856-80.00512-43.45856-80.00512-5.18144-6.38976-8.89856-4.88448-8.448 3.34848 0 0 9.15456 99.80928-19.44576 194.00704-28.60032 94.208-109.824 173.90592-109.824 173.90592zM681.61536 956.8768h105.24672s22.38464-73.5232 16.61952-130.21184c-9.30816-91.57632-48.31232-132.72064-48.31232-132.72064-3.80928-4.77184-6.0928-3.67616-5.2224 2.38592 0 0 16.75264 89.1904-14.4896 150.1184-30.9248 60.30336-53.84192 110.42816-53.84192 110.42816zM869.30432 811.35616c-2.88768-2.9184-4.80256-1.95584-4.38272 2.10944 0 0 6.79936 44.99456-7.76192 81.22368-10.12736 25.1904-28.91776 58.60352-28.91776 58.60352h107.17184s5.34528-50.31936-19.89632-83.97824c-11.56096-23.99232-46.21312-57.9584-46.21312-57.9584z" fill="#272636" p-id="1944"></path></svg>`
|
var DefaultIcon = `<svg t="1766107903919" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1942" width="200" height="200"><path d="M884.992 273.05984c4.10624 0 5.0688-2.36544 2.10944-5.25312 0 0-64.28672-65.55648-111.7696-75.55072-47.48288-9.984-45.47584-59.37152-80.56832-75.02848-72.0896-32.16384-158.6176-34.3552-158.6176-34.3552s-91.37152-4.92544-138.752-9.89184c-19.46624-2.03776-54.46656-9.58464-54.46656-9.58464-4.0448-0.84992-10.63936-1.19808-14.66368-0.44032 0 0-30.63808-0.07168-44.30848 43.35616-8.8576 28.11904 1.792 104.79616 1.792 104.79616 1.46432 12.27776-3.42016 30.21824-10.72128 40.20224 0 0-36.77184 46.03904-58.9312 100.34176-22.15936 54.30272 118.15936 145.05984 208.98816 205.27104C507.82208 613.89824 502.03648 743.424 502.03648 743.424s-74.19904-96.75776-194.00704-156.9792C188.2112 526.22336 150.30272 442.23488 150.30272 442.23488c-2.89792-5.45792-5.94944-4.94592-6.71744 1.21856 0 0-15.1552 91.61728 16.25088 147.8144 70.92224 126.88384 141.74208 112.88576 197.03808 183.27552s54.272 164.9152 54.272 164.9152-97.62816-141.29152-235.66336-205.55776c-91.53536-61.27616-74.7008-125.91104-85.49376-101.85728-10.79296 24.05376 26.73664 192.41984 65.40288 222.2592 80.57856 62.18752 94.16704 101.98016 94.16704 101.98016h175.53408S544.512 814.85824 572.928 725.73952c44.41088-139.30496 40.20224-191.26272 40.20224-191.26272 0.08192-8.22272 6.81984-14.19264 14.98112-13.29152 0 0 46.45888 4.64896 66.64192 9.68704 23.53152 5.86752 55.35744 26.20416 55.35744 26.20416 3.49184 2.14016 7.39328 0.68608 8.69376-3.1744l34.4576-102.77888c1.30048-3.8912-0.63488-5.51936-4.352-3.75808 0 0-45.37344 25.09824-88.17664 12.1856-20.15232-6.08256-59.60704-14.82752-74.69056-32.6656-16.95744-20.03968-15.59552-71.3728 26.66496-79.21664 48.0256-8.9088 33.13664 15.14496 65.91488 24.64768 27.42272 7.95648 22.29248-1.69984 26.69568 5.21216 1.67936 2.63168 0.38912 32.65536 0.38912 32.65536-0.21504 6.144 3.39968 7.84384 8.0384 3.79904l41.89184-36.46464c17.37728-11.24352 30.86336-0.57344 48.24064-11.81696 14.73536-9.53344 29.58336-43.66336 29.58336-43.66336 4.48512-9.24672 0.60416-20.41856-8.63232-24.92416l-42.58816-20.80768c-3.69664-1.80224-3.38944-3.26656 0.74752-3.26656h62.0032zM422.54336 123.87328s-49.88928 50.7392-74.5472 50.66752c-24.65792-0.07168-33.24928-56.12544-3.92192-56.12544 18.00192 0 76.32896 0.21504 76.32896 0.21504 4.13696 0.02048 5.0688 2.36544 2.14016 5.24288z m123.09504 249.64096s-3.31776-25.53856-33.16736-40.05888c-29.8496-14.52032-52.5312-41.13408-59.648-68.95616-12.1856-54.8864 48.29184-104.192 48.29184-104.192s-30.1056 73.6768 3.5328 106.60864c54.272 53.12512 40.99072 106.5984 40.99072 106.5984z m155.56608-164.46464c-7.94624 10.32192-9.60512 37.92896-59.2384 20.736-49.63328-17.2032-71.00416-54.5792-71.00416-54.5792-2.27328-3.40992-0.79872-6.49216 3.328-6.8096 0 0 43.55072-5.03808 80.06656 6.44096 24.91392 7.82336 54.79424 23.88992 46.848 34.21184z" fill="#272636" p-id="1943"></path><path d="M366.30528 259.26656c1.05472-1.76128 1.51552-1.57696 1.19808 0.43008 0 0-7.55712 19.0464 22.8864 87.63392 27.0848 61.02016 87.49056 68.7104 118.66112 98.23232 55.808 52.82816 53.52448 123.45344 53.52448 123.45344s-25.82528-49.85856-85.98528-78.83776-92.6208-50.52416-127.7952-85.77024c-45.02528-45.12768 17.5104-145.14176 17.5104-145.14176zM500.0704 961.44384h134.49216s95.8464-138.07616 46.68416-291.25632c-16.19968-50.46272-43.45856-80.00512-43.45856-80.00512-5.18144-6.38976-8.89856-4.88448-8.448 3.34848 0 0 9.15456 99.80928-19.44576 194.00704-28.60032 94.208-109.824 173.90592-109.824 173.90592zM681.61536 956.8768h105.24672s22.38464-73.5232 16.61952-130.21184c-9.30816-91.57632-48.31232-132.72064-48.31232-132.72064-3.80928-4.77184-6.0928-3.67616-5.2224 2.38592 0 0 16.75264 89.1904-14.4896 150.1184-30.9248 60.30336-53.84192 110.42816-53.84192 110.42816zM869.30432 811.35616c-2.88768-2.9184-4.80256-1.95584-4.38272 2.10944 0 0 6.79936 44.99456-7.76192 81.22368-10.12736 25.1904-28.91776 58.60352-28.91776 58.60352h107.17184s5.34528-50.31936-19.89632-83.97824c-11.56096-23.99232-46.21312-57.9584-46.21312-57.9584z" fill="#272636" p-id="1944"></path></svg>`
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ import (
|
|||||||
type AuthController struct {
|
type AuthController struct {
|
||||||
userService *services.UserService
|
userService *services.UserService
|
||||||
settingsService *services.SettingsService
|
settingsService *services.SettingsService
|
||||||
|
loginLogService *services.LoginLogService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthController(userService *services.UserService, settingsService *services.SettingsService) *AuthController {
|
func NewAuthController(userService *services.UserService, settingsService *services.SettingsService, loginLogService *services.LoginLogService) *AuthController {
|
||||||
return &AuthController{userService: userService, settingsService: settingsService}
|
return &AuthController{
|
||||||
|
userService: userService,
|
||||||
|
settingsService: settingsService,
|
||||||
|
loginLogService: loginLogService,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AuthController) Login(c *gin.Context) {
|
func (ac *AuthController) Login(c *gin.Context) {
|
||||||
@@ -26,6 +31,9 @@ func (ac *AuthController) Login(c *gin.Context) {
|
|||||||
Password string `json:"password" binding:"required"`
|
Password string `json:"password" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ip := c.ClientIP()
|
||||||
|
userAgent := c.GetHeader("User-Agent")
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
utils.BadRequest(c, err.Error())
|
utils.BadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
@@ -33,6 +41,8 @@ func (ac *AuthController) Login(c *gin.Context) {
|
|||||||
|
|
||||||
user := ac.userService.GetUserByUsername(req.Username)
|
user := ac.userService.GetUserByUsername(req.Username)
|
||||||
if user == nil || !ac.userService.ValidatePassword(user, req.Password) {
|
if user == nil || !ac.userService.ValidatePassword(user, req.Password) {
|
||||||
|
// 记录登录失败日志
|
||||||
|
ac.loginLogService.Create(req.Username, ip, userAgent, "failed", "用户名或密码错误")
|
||||||
utils.Unauthorized(c, "用户名或密码错误")
|
utils.Unauthorized(c, "用户名或密码错误")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -45,16 +55,10 @@ func (ac *AuthController) Login(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 JWT Secret
|
|
||||||
jwtSecret := ac.settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret)
|
|
||||||
if jwtSecret == "" {
|
|
||||||
utils.ServerError(c, "系统配置错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成 token
|
// 生成 token
|
||||||
token, err := utils.GenerateToken(user.ID, user.Username, expireDays, jwtSecret)
|
token, err := utils.GenerateToken(user.ID, user.Username, expireDays, constant.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
ac.loginLogService.Create(req.Username, ip, userAgent, "failed", "Token生成失败")
|
||||||
utils.ServerError(c, "登录失败")
|
utils.ServerError(c, "登录失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -62,6 +66,9 @@ func (ac *AuthController) Login(c *gin.Context) {
|
|||||||
// 设置 Cookie
|
// 设置 Cookie
|
||||||
middleware.SetAuthCookie(c, token, expireDays)
|
middleware.SetAuthCookie(c, token, expireDays)
|
||||||
|
|
||||||
|
// 记录登录成功日志
|
||||||
|
ac.loginLogService.Create(req.Username, ip, userAgent, "success", "登录成功")
|
||||||
|
|
||||||
utils.Success(c, gin.H{
|
utils.Success(c, gin.H{
|
||||||
"user": user.Username,
|
"user": user.Username,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"baihu/internal/constant"
|
"baihu/internal/constant"
|
||||||
"baihu/internal/database"
|
"baihu/internal/database"
|
||||||
"baihu/internal/models"
|
"baihu/internal/models"
|
||||||
@@ -17,12 +19,14 @@ import (
|
|||||||
type SettingsController struct {
|
type SettingsController struct {
|
||||||
userService *services.UserService
|
userService *services.UserService
|
||||||
settingsService *services.SettingsService
|
settingsService *services.SettingsService
|
||||||
|
loginLogService *services.LoginLogService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSettingsController(userService *services.UserService) *SettingsController {
|
func NewSettingsController(userService *services.UserService, loginLogService *services.LoginLogService) *SettingsController {
|
||||||
return &SettingsController{
|
return &SettingsController{
|
||||||
userService: userService,
|
userService: userService,
|
||||||
settingsService: services.NewSettingsService(),
|
settingsService: services.NewSettingsService(),
|
||||||
|
loginLogService: loginLogService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,3 +190,31 @@ func formatDuration(d time.Duration) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%d秒", seconds)
|
return fmt.Sprintf("%d秒", seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GetLoginLogs 获取登录日志
|
||||||
|
func (sc *SettingsController) GetLoginLogs(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
|
||||||
|
username := c.Query("username")
|
||||||
|
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize < 1 || pageSize > 100 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, total, err := sc.loginLogService.List(page, pageSize, username)
|
||||||
|
if err != nil {
|
||||||
|
utils.ServerError(c, "获取登录日志失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Success(c, gin.H{
|
||||||
|
"data": logs,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ func Migrate() error {
|
|||||||
&models.Script{},
|
&models.Script{},
|
||||||
&models.EnvironmentVariable{},
|
&models.EnvironmentVariable{},
|
||||||
&models.Setting{},
|
&models.Setting{},
|
||||||
|
&models.LoginLog{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"baihu/internal/constant"
|
"baihu/internal/constant"
|
||||||
"baihu/internal/services"
|
|
||||||
"baihu/internal/utils"
|
"baihu/internal/utils"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthRequired 认证中间件
|
// AuthRequired 认证中间件
|
||||||
func AuthRequired(settingsService *services.SettingsService) gin.HandlerFunc {
|
func AuthRequired() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
token, err := c.Cookie(constant.CookieName)
|
token, err := c.Cookie(constant.CookieName)
|
||||||
if err != nil || token == "" {
|
if err != nil || token == "" {
|
||||||
@@ -18,16 +17,8 @@ func AuthRequired(settingsService *services.SettingsService) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 JWT Secret
|
|
||||||
jwtSecret := settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret)
|
|
||||||
if jwtSecret == "" {
|
|
||||||
utils.Unauthorized(c, "系统配置错误")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证 token
|
// 验证 token
|
||||||
userID, username, err := utils.ParseToken(token, jwtSecret)
|
userID, username, err := utils.ParseToken(token, constant.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Unauthorized(c, "登录已过期,请重新登录")
|
utils.Unauthorized(c, "登录已过期,请重新登录")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"baihu/internal/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginLog 登录日志
|
||||||
|
type LoginLog struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
Username string `json:"username" gorm:"size:100;index;not null"`
|
||||||
|
IP string `json:"ip" gorm:"size:50"`
|
||||||
|
UserAgent string `json:"user_agent" gorm:"size:500"`
|
||||||
|
Status string `json:"status" gorm:"size:20"` // success, failed
|
||||||
|
Message string `json:"message" gorm:"size:255"`
|
||||||
|
CreatedAt LocalTime `json:"created_at" gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LoginLog) TableName() string {
|
||||||
|
return constant.TablePrefix + "login_logs"
|
||||||
|
}
|
||||||
@@ -8,9 +8,10 @@ import (
|
|||||||
|
|
||||||
var cronService *services.CronService
|
var cronService *services.CronService
|
||||||
|
|
||||||
func RegisterControllers() (*Controllers, *services.SettingsService) {
|
func RegisterControllers() *Controllers {
|
||||||
// Initialize services
|
// Initialize services
|
||||||
settingsService := services.NewSettingsService()
|
settingsService := services.NewSettingsService()
|
||||||
|
loginLogService := services.NewLoginLogService()
|
||||||
|
|
||||||
// 执行系统初始化(返回 userService)
|
// 执行系统初始化(返回 userService)
|
||||||
initService := services.NewInitService(settingsService)
|
initService := services.NewInitService(settingsService)
|
||||||
@@ -28,7 +29,7 @@ func RegisterControllers() (*Controllers, *services.SettingsService) {
|
|||||||
// Initialize and return controllers
|
// Initialize and return controllers
|
||||||
return &Controllers{
|
return &Controllers{
|
||||||
Task: controllers.NewTaskController(taskService, cronService),
|
Task: controllers.NewTaskController(taskService, cronService),
|
||||||
Auth: controllers.NewAuthController(userService, settingsService),
|
Auth: controllers.NewAuthController(userService, settingsService, loginLogService),
|
||||||
Env: controllers.NewEnvController(envService),
|
Env: controllers.NewEnvController(envService),
|
||||||
Script: controllers.NewScriptController(scriptService),
|
Script: controllers.NewScriptController(scriptService),
|
||||||
Executor: controllers.NewExecutorController(executorService),
|
Executor: controllers.NewExecutorController(executorService),
|
||||||
@@ -36,8 +37,8 @@ func RegisterControllers() (*Controllers, *services.SettingsService) {
|
|||||||
Dashboard: controllers.NewDashboardController(cronService, executorService),
|
Dashboard: controllers.NewDashboardController(cronService, executorService),
|
||||||
Log: controllers.NewLogController(),
|
Log: controllers.NewLogController(),
|
||||||
Terminal: controllers.NewTerminalController(),
|
Terminal: controllers.NewTerminalController(),
|
||||||
Settings: controllers.NewSettingsController(userService),
|
Settings: controllers.NewSettingsController(userService, loginLogService),
|
||||||
}, settingsService
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopCron stops the cron service gracefully
|
// StopCron stops the cron service gracefully
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"baihu/internal/controllers"
|
"baihu/internal/controllers"
|
||||||
"baihu/internal/middleware"
|
"baihu/internal/middleware"
|
||||||
"baihu/internal/services"
|
|
||||||
"baihu/internal/static"
|
"baihu/internal/static"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -41,7 +40,7 @@ func cacheControl(value string) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Setup(c *Controllers, settingsService *services.SettingsService) *gin.Engine {
|
func Setup(c *Controllers) *gin.Engine {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(middleware.GinLogger(), middleware.GinRecovery())
|
router.Use(middleware.GinLogger(), middleware.GinRecovery())
|
||||||
@@ -95,7 +94,7 @@ func Setup(c *Controllers, settingsService *services.SettingsService) *gin.Engin
|
|||||||
|
|
||||||
// 需要认证的路由
|
// 需要认证的路由
|
||||||
authorized := api.Group("")
|
authorized := api.Group("")
|
||||||
authorized.Use(middleware.AuthRequired(settingsService))
|
authorized.Use(middleware.AuthRequired())
|
||||||
{
|
{
|
||||||
// 获取当前用户
|
// 获取当前用户
|
||||||
authorized.GET("/auth/me", c.Auth.GetCurrentUser)
|
authorized.GET("/auth/me", c.Auth.GetCurrentUser)
|
||||||
@@ -174,6 +173,7 @@ func Setup(c *Controllers, settingsService *services.SettingsService) *gin.Engin
|
|||||||
settings.GET("/site", c.Settings.GetSiteSettings)
|
settings.GET("/site", c.Settings.GetSiteSettings)
|
||||||
settings.PUT("/site", c.Settings.UpdateSiteSettings)
|
settings.PUT("/site", c.Settings.UpdateSiteSettings)
|
||||||
settings.GET("/about", c.Settings.GetAbout)
|
settings.GET("/about", c.Settings.GetAbout)
|
||||||
|
settings.GET("/login-logs", c.Settings.GetLoginLogs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,62 +2,66 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"baihu/internal/constant"
|
"baihu/internal/constant"
|
||||||
"encoding/json"
|
|
||||||
"os"
|
"gopkg.in/ini.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `json:"port"`
|
Port int `ini:"port"`
|
||||||
Host string `json:"host"`
|
Host string `ini:"host"`
|
||||||
SiteName string `json:"site_name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Type string `json:"type"`
|
Type string `ini:"type"`
|
||||||
Host string `json:"host"`
|
Host string `ini:"host"`
|
||||||
Port int `json:"port"`
|
Port int `ini:"port"`
|
||||||
User string `json:"user"`
|
User string `ini:"user"`
|
||||||
Password string `json:"password"`
|
Password string `ini:"password"`
|
||||||
DBName string `json:"dbname"`
|
DBName string `ini:"dbname"`
|
||||||
Path string `json:"path"`
|
Path string `ini:"path"`
|
||||||
TablePrefix string `json:"table_prefix"`
|
TablePrefix string `ini:"table_prefix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SecurityConfig struct {
|
type SecurityConfig struct {
|
||||||
JWTSecret string `json:"jwt_secret"`
|
Secret string `ini:"secret"`
|
||||||
PasswordSalt string `json:"password_salt"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskConfig struct {
|
type TaskConfig struct {
|
||||||
DefaultTimeout int `json:"default_timeout"`
|
DefaultTimeout int `ini:"default_timeout"`
|
||||||
LogRetentionDays int `json:"log_retention_days"`
|
LogRetentionDays int `ini:"log_retention_days"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Server ServerConfig `json:"server"`
|
Server ServerConfig `ini:"server"`
|
||||||
Database DatabaseConfig `json:"database"`
|
Database DatabaseConfig `ini:"database"`
|
||||||
Security SecurityConfig `json:"security"`
|
Security SecurityConfig `ini:"security"`
|
||||||
Task TaskConfig `json:"task"`
|
Task TaskConfig `ini:"task"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var Config *AppConfig
|
var Config *AppConfig
|
||||||
|
|
||||||
func LoadConfig(path string) (*AppConfig, error) {
|
func LoadConfig(path string) (*AppConfig, error) {
|
||||||
data, err := os.ReadFile(path)
|
cfg, err := ini.Load(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
Config = &AppConfig{}
|
Config = &AppConfig{}
|
||||||
if err := json.Unmarshal(data, Config); err != nil {
|
if err := cfg.MapTo(Config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置表前缀到 constant 包
|
// 设置默认数据库路径
|
||||||
if Config.Database.TablePrefix != "" {
|
if Config.Database.Path == "" {
|
||||||
constant.TablePrefix = Config.Database.TablePrefix
|
Config.Database.Path = constant.DefaultDBPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置表前缀到 constant 包
|
||||||
|
constant.TablePrefix = Config.Database.TablePrefix
|
||||||
|
|
||||||
|
// 设置 Secret 到 constant 包
|
||||||
|
constant.Secret = Config.Security.Secret
|
||||||
|
|
||||||
return Config, nil
|
return Config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
|
|
||||||
"baihu/internal/constant"
|
|
||||||
"baihu/internal/logger"
|
"baihu/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,11 +23,9 @@ func (s *InitService) Initialize() *UserService {
|
|||||||
logger.Warnf("初始化设置失败: %v", err)
|
logger.Warnf("初始化设置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 JWT Secret(也用作密码 salt,必须在创建 UserService 之前)
|
// 创建 UserService
|
||||||
s.initJWTSecret()
|
userService := NewUserService()
|
||||||
|
|
||||||
// 创建 UserService(依赖 settingsService 获取 salt)
|
|
||||||
userService := NewUserService(s.settingsService)
|
|
||||||
// 创建管理员账号
|
// 创建管理员账号
|
||||||
s.initializeAdmin(userService)
|
s.initializeAdmin(userService)
|
||||||
|
|
||||||
@@ -49,30 +43,3 @@ func (s *InitService) initializeAdmin(userService *UserService) {
|
|||||||
userService.CreateUser("admin", "123456", "admin@local", "admin")
|
userService.CreateUser("admin", "123456", "admin@local", "admin")
|
||||||
logger.Info("管理员账号创建成功: admin / 123456")
|
logger.Info("管理员账号创建成功: admin / 123456")
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInitialized 检查是否已初始化
|
|
||||||
func (s *InitService) IsInitialized() bool {
|
|
||||||
return s.settingsService.Get(constant.SectionSystem, constant.KeyInitialized) == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
// initJWTSecret 初始化 JWT Secret,如果不存在则生成随机值
|
|
||||||
func (s *InitService) initJWTSecret() {
|
|
||||||
existing := s.settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret)
|
|
||||||
if existing != "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成 32 字节随机密钥
|
|
||||||
bytes := make([]byte, 32)
|
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
|
||||||
logger.Warnf("生成 JWT Secret 失败: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
secret := hex.EncodeToString(bytes)
|
|
||||||
if err := s.settingsService.Set(constant.SectionSystem, constant.KeyJWTSecret, secret); err != nil {
|
|
||||||
logger.Warnf("保存 JWT Secret 失败: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Info("JWT Secret 已生成")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"baihu/internal/database"
|
||||||
|
"baihu/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginLogService struct{}
|
||||||
|
|
||||||
|
func NewLoginLogService() *LoginLogService {
|
||||||
|
return &LoginLogService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 创建登录日志
|
||||||
|
func (s *LoginLogService) Create(username, ip, userAgent, status, message string) error {
|
||||||
|
log := &models.LoginLog{
|
||||||
|
Username: username,
|
||||||
|
IP: ip,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
Status: status,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
return database.DB.Create(log).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 获取登录日志列表
|
||||||
|
func (s *LoginLogService) List(page, pageSize int, username string) ([]models.LoginLog, int64, error) {
|
||||||
|
var logs []models.LoginLog
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := database.DB.Model(&models.LoginLog{})
|
||||||
|
if username != "" {
|
||||||
|
query = query.Where("username LIKE ?", "%"+username+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanOldLogs 清理指定天数前的日志
|
||||||
|
func (s *LoginLogService) CleanOldLogs(days int) (int64, error) {
|
||||||
|
result := database.DB.Exec("DELETE FROM "+models.LoginLog{}.TableName()+" WHERE created_at < datetime('now', ?)", "-"+string(rune(days))+" days")
|
||||||
|
return result.RowsAffected, result.Error
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"baihu/internal/cache"
|
||||||
"baihu/internal/constant"
|
"baihu/internal/constant"
|
||||||
"baihu/internal/database"
|
"baihu/internal/database"
|
||||||
"baihu/internal/models"
|
"baihu/internal/models"
|
||||||
@@ -17,26 +18,23 @@ func (s *SettingsService) InitSettings() error {
|
|||||||
for section, keys := range constant.DefaultSettings {
|
for section, keys := range constant.DefaultSettings {
|
||||||
for key, value := range keys {
|
for key, value := range keys {
|
||||||
var count int64
|
var count int64
|
||||||
database.DB.Model(&models.Setting{}).
|
database.DB.Model(&models.Setting{}).Where("section = ? AND key = ?", section, key).Count(&count)
|
||||||
Where("section = ? AND key = ?", section, key).
|
|
||||||
Count(&count)
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
setting := &models.Setting{
|
if err := database.DB.Create(&models.Setting{Section: section, Key: key, Value: value}).Error; err != nil {
|
||||||
Section: section,
|
|
||||||
Key: key,
|
|
||||||
Value: value,
|
|
||||||
}
|
|
||||||
if err := database.DB.Create(setting).Error; err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cache.LoadSiteCache()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 获取单个设置
|
// Get 获取单个设置
|
||||||
func (s *SettingsService) Get(section, key string) string {
|
func (s *SettingsService) Get(section, key string) string {
|
||||||
|
if section == constant.SectionSite {
|
||||||
|
return cache.GetSiteCache(key)
|
||||||
|
}
|
||||||
var setting models.Setting
|
var setting models.Setting
|
||||||
if err := database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error; err != nil {
|
if err := database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error; err != nil {
|
||||||
if def, ok := constant.DefaultSettings[section][key]; ok {
|
if def, ok := constant.DefaultSettings[section][key]; ok {
|
||||||
@@ -50,29 +48,27 @@ func (s *SettingsService) Get(section, key string) string {
|
|||||||
// Set 设置单个值
|
// Set 设置单个值
|
||||||
func (s *SettingsService) Set(section, key, value string) error {
|
func (s *SettingsService) Set(section, key, value string) error {
|
||||||
var setting models.Setting
|
var setting models.Setting
|
||||||
result := database.DB.Where("section = ? AND key = ?", section, key).First(&setting)
|
if database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error != nil {
|
||||||
if result.Error != nil {
|
return database.DB.Create(&models.Setting{Section: section, Key: key, Value: value}).Error
|
||||||
setting = models.Setting{Section: section, Key: key, Value: value}
|
|
||||||
return database.DB.Create(&setting).Error
|
|
||||||
}
|
}
|
||||||
return database.DB.Model(&setting).Update("value", value).Error
|
return database.DB.Model(&setting).Update("value", value).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSection 获取整个 section 的设置
|
// GetSection 获取整个 section 的设置
|
||||||
func (s *SettingsService) GetSection(section string) map[string]string {
|
func (s *SettingsService) GetSection(section string) map[string]string {
|
||||||
var settings []models.Setting
|
if section == constant.SectionSite {
|
||||||
database.DB.Where("section = ?", section).Find(&settings)
|
return cache.GetSiteCacheAll()
|
||||||
|
}
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
// 先填充默认值
|
|
||||||
if defaults, ok := constant.DefaultSettings[section]; ok {
|
if defaults, ok := constant.DefaultSettings[section]; ok {
|
||||||
for k, v := range defaults {
|
for k, v := range defaults {
|
||||||
result[k] = v
|
result[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 覆盖数据库值
|
var settings []models.Setting
|
||||||
for _, s := range settings {
|
database.DB.Where("section = ?", section).Find(&settings)
|
||||||
result[s.Key] = s.Value
|
for _, setting := range settings {
|
||||||
|
result[setting.Key] = setting.Value
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -84,5 +80,8 @@ func (s *SettingsService) SetSection(section string, values map[string]string) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if section == constant.SectionSite {
|
||||||
|
cache.SetSiteCacheBatch(values)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,14 @@ import (
|
|||||||
"baihu/internal/models"
|
"baihu/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct{}
|
||||||
settingsService *SettingsService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserService(settingsService *SettingsService) *UserService {
|
func NewUserService() *UserService {
|
||||||
return &UserService{settingsService: settingsService}
|
return &UserService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (us *UserService) hashPassword(password string) string {
|
func (us *UserService) hashPassword(password string) string {
|
||||||
// 使用 JWT Secret 作为密码 salt
|
hash := sha256.Sum256([]byte(password + constant.Secret))
|
||||||
salt := us.settingsService.Get(constant.SectionSystem, constant.KeyJWTSecret)
|
|
||||||
hash := sha256.Sum256([]byte(password + salt))
|
|
||||||
return hex.EncodeToString(hash[:])
|
return hex.EncodeToString(hash[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"baihu/internal/cache"
|
||||||
"baihu/internal/constant"
|
"baihu/internal/constant"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -14,16 +15,27 @@ type Pagination struct {
|
|||||||
PageSize int
|
PageSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDefaultPageSize 从缓存获取默认分页大小
|
||||||
|
func getDefaultPageSize() int {
|
||||||
|
pageSizeStr := cache.GetSiteCache(constant.KeyPageSize)
|
||||||
|
pageSize, err := strconv.Atoi(pageSizeStr)
|
||||||
|
if err != nil || pageSize < 1 {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
return pageSize
|
||||||
|
}
|
||||||
|
|
||||||
// ParsePagination 从请求中解析分页参数
|
// ParsePagination 从请求中解析分页参数
|
||||||
func ParsePagination(c *gin.Context) Pagination {
|
func ParsePagination(c *gin.Context) Pagination {
|
||||||
|
defaultPageSize := getDefaultPageSize()
|
||||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", strconv.Itoa(constant.DefaultPageSize)))
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", strconv.Itoa(defaultPageSize)))
|
||||||
|
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
if pageSize < 1 || pageSize > 100 {
|
if pageSize < 1 || pageSize > 100 {
|
||||||
pageSize = constant.DefaultPageSize
|
pageSize = defaultPageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pagination{Page: page, PageSize: pageSize}
|
return Pagination{Page: page, PageSize: pageSize}
|
||||||
|
|||||||
+26
-1
@@ -113,7 +113,14 @@ export const api = {
|
|||||||
getPublicSite: () => request<{ title: string; subtitle: string; icon: string }>('/settings/public'),
|
getPublicSite: () => request<{ title: string; subtitle: string; icon: string }>('/settings/public'),
|
||||||
updateSite: (data: SiteSettings) =>
|
updateSite: (data: SiteSettings) =>
|
||||||
request('/settings/site', { method: 'PUT', body: JSON.stringify(data) }),
|
request('/settings/site', { method: 'PUT', body: JSON.stringify(data) }),
|
||||||
getAbout: () => request<AboutInfo>('/settings/about')
|
getAbout: () => request<AboutInfo>('/settings/about'),
|
||||||
|
getLoginLogs: (params?: { page?: number; page_size?: number; username?: string }) => {
|
||||||
|
const query = new URLSearchParams()
|
||||||
|
if (params?.page) query.set('page', String(params.page))
|
||||||
|
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||||||
|
if (params?.username) query.set('username', params.username)
|
||||||
|
return request<LoginLogListResponse>(`/settings/login-logs?${query}`)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
files: {
|
files: {
|
||||||
tree: () => request<FileNode[]>('/files/tree'),
|
tree: () => request<FileNode[]>('/files/tree'),
|
||||||
@@ -264,3 +271,21 @@ export interface SiteSettings {
|
|||||||
page_size: string
|
page_size: string
|
||||||
cookie_days: string
|
cookie_days: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface LoginLog {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
ip: string
|
||||||
|
user_agent: string
|
||||||
|
status: string
|
||||||
|
message: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginLogListResponse {
|
||||||
|
data: LoginLog[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
page_size: number
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||||
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable } from 'lucide-vue-next'
|
import { LayoutDashboard, ListTodo, FileCode, Settings, LogOut, ScrollText, Terminal, Variable, KeyRound } from 'lucide-vue-next'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import ThemeToggle from '@/components/ThemeToggle.vue'
|
import ThemeToggle from '@/components/ThemeToggle.vue'
|
||||||
import { api } from '@/api'
|
import { api } from '@/api'
|
||||||
@@ -18,6 +18,7 @@ const navItems = [
|
|||||||
{ to: '/history', icon: ScrollText, label: '执行历史', exact: true },
|
{ to: '/history', icon: ScrollText, label: '执行历史', exact: true },
|
||||||
{ to: '/environments', icon: Variable, label: '环境变量', exact: true },
|
{ to: '/environments', icon: Variable, label: '环境变量', exact: true },
|
||||||
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
|
{ to: '/terminal', icon: Terminal, label: '终端命令', exact: true },
|
||||||
|
{ to: '/login-logs', icon: KeyRound, label: '登录日志', exact: true },
|
||||||
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
|
{ to: '/settings', icon: Settings, label: '系统设置', exact: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const router = createRouter({
|
|||||||
{ path: 'editor/:path(.*)?', name: 'editor', component: () => import('@/views/editor/Editor.vue') },
|
{ path: 'editor/:path(.*)?', name: 'editor', component: () => import('@/views/editor/Editor.vue') },
|
||||||
{ path: 'environments', name: 'environments', component: () => import('@/views/environments/Environments.vue') },
|
{ path: 'environments', name: 'environments', component: () => import('@/views/environments/Environments.vue') },
|
||||||
{ path: 'history', name: 'history', component: () => import('@/views/history/History.vue') },
|
{ path: 'history', name: 'history', component: () => import('@/views/history/History.vue') },
|
||||||
|
{ path: 'login-logs', name: 'login-logs', component: () => import('@/views/login-logs/LoginLogs.vue') },
|
||||||
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
|
{ path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') },
|
||||||
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
|
{ path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import Pagination from '@/components/Pagination.vue'
|
||||||
|
import { RefreshCw, Search } from 'lucide-vue-next'
|
||||||
|
import { api } from '@/api'
|
||||||
|
import { toast } from 'vue-sonner'
|
||||||
|
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||||
|
|
||||||
|
const { pageSize } = useSiteSettings()
|
||||||
|
|
||||||
|
interface LoginLog {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
ip: string
|
||||||
|
user_agent: string
|
||||||
|
status: string
|
||||||
|
message: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const logs = ref<LoginLog[]>([])
|
||||||
|
const filterUsername = ref('')
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const total = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
let searchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
async function loadLogs() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.settings.getLoginLogs({
|
||||||
|
page: currentPage.value,
|
||||||
|
page_size: pageSize.value,
|
||||||
|
username: filterUsername.value || undefined
|
||||||
|
})
|
||||||
|
logs.value = res.data
|
||||||
|
total.value = res.total
|
||||||
|
} catch {
|
||||||
|
toast.error('加载登录日志失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
|
searchTimer = setTimeout(() => {
|
||||||
|
currentPage.value = 1
|
||||||
|
loadLogs()
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
currentPage.value = page
|
||||||
|
loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBrowserInfo(userAgent: string): string {
|
||||||
|
if (!userAgent) return '未知'
|
||||||
|
if (userAgent.includes('Chrome')) return 'Chrome'
|
||||||
|
if (userAgent.includes('Firefox')) return 'Firefox'
|
||||||
|
if (userAgent.includes('Safari')) return 'Safari'
|
||||||
|
if (userAgent.includes('Edge')) return 'Edge'
|
||||||
|
return '其他'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadLogs)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold tracking-tight">登录日志</h2>
|
||||||
|
<p class="text-muted-foreground">查看系统登录记录</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="relative">
|
||||||
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
v-model="filterUsername"
|
||||||
|
placeholder="搜索用户名..."
|
||||||
|
class="h-9 pl-9 w-56 text-sm"
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="icon" class="h-9 w-9" @click="loadLogs" :disabled="loading">
|
||||||
|
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': loading }" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border bg-card">
|
||||||
|
<!-- 表头 -->
|
||||||
|
<div class="flex items-center gap-4 px-4 py-2 border-b bg-muted/50 text-sm text-muted-foreground font-medium">
|
||||||
|
<span class="w-24 shrink-0">用户名</span>
|
||||||
|
<span class="w-32 shrink-0">IP 地址</span>
|
||||||
|
<span class="w-20 shrink-0">浏览器</span>
|
||||||
|
<span class="w-16 shrink-0 text-center">状态</span>
|
||||||
|
<span class="flex-1">消息</span>
|
||||||
|
<span class="w-40 shrink-0 text-right">时间</span>
|
||||||
|
</div>
|
||||||
|
<!-- 列表 -->
|
||||||
|
<div class="divide-y">
|
||||||
|
<div v-if="logs.length === 0" class="text-sm text-muted-foreground text-center py-8">
|
||||||
|
暂无登录日志
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="log in logs"
|
||||||
|
:key="log.id"
|
||||||
|
class="flex items-center gap-4 px-4 py-2 hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span class="w-24 shrink-0 font-medium text-sm truncate">{{ log.username }}</span>
|
||||||
|
<code class="w-32 shrink-0 text-xs text-muted-foreground bg-muted px-2 py-1 rounded">{{ log.ip }}</code>
|
||||||
|
<span class="w-20 shrink-0 text-xs text-muted-foreground">{{ getBrowserInfo(log.user_agent) }}</span>
|
||||||
|
<span class="w-16 shrink-0 flex justify-center">
|
||||||
|
<Badge :variant="log.status === 'success' ? 'default' : 'destructive'" class="text-xs">
|
||||||
|
{{ log.status === 'success' ? '成功' : '失败' }}
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
<span class="flex-1 text-sm text-muted-foreground truncate">{{ log.message }}</span>
|
||||||
|
<span class="w-40 shrink-0 text-right text-xs text-muted-foreground">{{ log.created_at }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<Pagination :total="total" :page="currentPage" @update:page="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user