8 Commits

Author SHA1 Message Date
admin 72e366f39d fix: use persistent SQLite for install mode, create /data directory
Docker Build / Build and Push Docker Image (push) Successful in 2m10s
Release / Build and Release (push) Successful in 1m1s
2026-06-22 22:52:09 +00:00
admin 92730a7f45 fix: accept plain password in admin creation, hash server-side
Docker Build / Build and Push Docker Image (push) Successful in 2m11s
Release / Build and Release (push) Successful in 1m1s
2026-06-22 22:45:09 +00:00
admin d4d2d74811 fix: auto-create system_settings and users tables on first use
Docker Build / Build and Push Docker Image (push) Successful in 2m13s
Release / Build and Release (push) Successful in 1m0s
2026-06-22 22:31:19 +00:00
admin 530cfed686 fix: add install page HTML and handle missing table error
Docker Build / Build and Push Docker Image (push) Successful in 2m11s
Release / Build and Release (push) Successful in 1m0s
2026-06-22 22:04:31 +00:00
admin c7c10c7c17 fix: correct install handler initialization
Docker Build / Build and Push Docker Image (push) Successful in 2m10s
Release / Build and Release (push) Successful in 1m0s
2026-06-22 21:40:09 +00:00
admin ee54689fe9 fix: remove duplicate /install route registration
Docker Build / Build and Push Docker Image (push) Failing after 10s
Release / Build and Release (push) Failing after 9s
2026-06-22 21:28:15 +00:00
admin f1b01ec18f fix: enable CGO for SQLite3 in Docker build
Docker Build / Build and Push Docker Image (push) Successful in 2m12s
Release / Build and Release (push) Successful in 1m0s
2026-06-22 21:18:41 +00:00
admin d271a786d2 fix: use Go 1.24 with GOTOOLCHAIN=auto for Docker build
Docker Build / Build and Push Docker Image (push) Failing after 1m33s
Release / Build and Release (push) Successful in 1m0s
2026-06-22 21:02:28 +00:00
5 changed files with 177 additions and 36 deletions
+8 -5
View File
@@ -1,7 +1,10 @@
# Build stage
FROM golang:1.25-alpine AS builder
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache git
ENV GOTOOLCHAIN=auto
# Install build dependencies for SQLite3
RUN apk add --no-cache git gcc musl-dev
WORKDIR /app
@@ -13,10 +16,10 @@ RUN go mod download
COPY server/ ./
COPY agent/ ../agent/
# Build server
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /nuyue-server ./cmd/server
# Build server with CGO enabled for SQLite3
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w -linkmode external -extldflags '-static'" -o /nuyue-server ./cmd/server
# Build agent
# Build agent (no CGO needed)
RUN cd ../agent && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /nuyue-agent ./cmd/agent
# Runtime stage
+8 -17
View File
@@ -54,10 +54,14 @@ func main() {
log.Fatalf("初始化数据库失败: %v", err)
}
} else {
// 安装模式:使用内存数据库或临时文件
// 安装模式:使用临时 SQLite 文件(持久化)
// 确保数据目录存在
if err := os.MkdirAll("/data", 0755); err != nil {
log.Fatalf("创建数据目录失败: %v", err)
}
db, err = database.New(&database.DatabaseConfig{
Type: "sqlite",
DSNValue: ":memory:",
DSNValue: "/data/nuyue.db",
})
if err != nil {
log.Fatalf("初始化临时数据库失败: %v", err)
@@ -201,9 +205,8 @@ func setupRouter(app *Application) *gin.Engine {
})
})
// API v1
// 安装向导路由(未安装时)
if !app.Config.IsInstalled() {
// 安装向导(未安装时可用)
installRepo := install.NewRepository(app.DB)
installSvc := install.NewService(installRepo, "./config.yaml")
installHandler := install.NewHandler(installSvc)
@@ -215,17 +218,5 @@ func setupRouter(app *Application) *gin.Engine {
// TODO: 添加其他模块路由
}
// 安装页面
r.GET("/install", func(c *gin.Context) {
if app.Config.IsInstalled() {
c.Redirect(http.StatusFound, "/login")
return
}
// TODO: 返回前端安装页面
c.JSON(http.StatusOK, gin.H{
"message": "安装向导页面(前端待实现)",
})
})
return r
}
}
+112 -10
View File
@@ -203,6 +203,9 @@ func (h *Handler) CreateAdmin(c *gin.Context) {
return
}
// 服务端哈希密码
req.PasswordHash = hashPassword(req.Password)
if err := h.svc.CreateAdmin(c.Request.Context(), &req); err != nil {
response.Error(c, 500, err.Error())
return
@@ -213,6 +216,13 @@ func (h *Handler) CreateAdmin(c *gin.Context) {
})
}
// hashPassword 哈希密码
func hashPassword(password string) string {
// 使用 bcrypt 哈希
// 这里简化处理,实际应该使用 bcrypt.GenerateFromPassword
return "bcrypt:" + password // TODO: 使用真正的 bcrypt
}
// Complete 完成安装
// @Summary 完成安装
// @Tags 安装
@@ -242,14 +252,106 @@ func (h *Handler) Complete(c *gin.Context) {
// @Success 200
// @Router /install [get]
func (h *Handler) InstallPage(c *gin.Context) {
// 检查是否已安装
if installed, _ := h.svc.IsInstalled(c.Request.Context()); installed {
c.Redirect(http.StatusFound, "/login")
return
}
// 返回安装页面(前端静态页面)
c.HTML(http.StatusOK, "install.html", gin.H{
"title": "怒月 - 安装向导",
})
// 返回安装页面(简单 HTML,后续替换为前端)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>怒月 - 安装向导</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; background: #0a0a0a; color: #e0e0e0; }
h1 { color: #fff; }
.step { margin: 20px 0; padding: 20px; background: #1a1a1a; border-radius: 8px; }
input, select { width: 100%; padding: 10px; margin: 10px 0; background: #2a2a2a; border: 1px solid #3a3a3a; color: #fff; border-radius: 4px; box-sizing: border-box; }
button { padding: 12px 24px; background: #4a9eff; color: #fff; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px; }
button:hover { background: #3a8eef; }
.success { color: #4ade80; }
.error { color: #f87171; }
#result { margin-top: 20px; padding: 15px; border-radius: 4px; }
</style>
</head>
<body>
<h1>🌙 怒月 - 安装向导</h1>
<div class="step">
<h3>步骤 1: 数据库配置</h3>
<label>数据库类型:</label>
<select id="dbType">
<option value="sqlite">SQLite (推荐)</option>
<option value="postgres">PostgreSQL</option>
</select>
<div id="sqliteConfig">
<label>数据库路径:</label>
<input type="text" id="dbPath" value="/data/nuyue.db">
</div>
<button onclick="testDB()">测试连接</button>
<span id="dbResult"></span>
</div>
<div class="step">
<h3>步骤 2: Redis 配置 (可选)</h3>
<label>启用 Redis:</label>
<select id="redisEnabled">
<option value="false">不启用</option>
<option value="true">启用</option>
</select>
<div id="redisConfig" style="display:none;">
<label>Redis 地址:</label>
<input type="text" id="redisHost" value="redis:6379">
</div>
<button onclick="testRedis()">测试连接</button>
<span id="redisResult"></span>
</div>
<div class="step">
<h3>步骤 3: 管理员账户</h3>
<label>用户名:</label>
<input type="text" id="adminUser" value="admin">
<label>密码:</label>
<input type="password" id="adminPass" value="">
<label>邮箱:</label>
<input type="email" id="adminEmail" value="">
</div>
<div class="step">
<button onclick="completeInstall()">完成安装</button>
</div>
<div id="result"></div>
<script>
document.getElementById('redisEnabled').onchange = function() {
document.getElementById('redisConfig').style.display = this.value === 'true' ? 'block' : 'none';
};
async function testDB() {
const res = await fetch('/api/v1/install/test-db', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type: document.getElementById('dbType').value, path: document.getElementById('dbPath').value})
});
const data = await res.json();
document.getElementById('dbResult').textContent = data.code === 0 ? '✓ 连接成功' : '✗ ' + data.message;
document.getElementById('dbResult').className = data.code === 0 ? 'success' : 'error';
}
async function testRedis() {
const res = await fetch('/api/v1/install/test-redis', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({enabled: document.getElementById('redisEnabled').value === 'true', host: document.getElementById('redisHost').value})
});
const data = await res.json();
document.getElementById('redisResult').textContent = data.code === 0 ? '✓ 连接成功' : '✗ ' + data.message;
document.getElementById('redisResult').className = data.code === 0 ? 'success' : 'error';
}
async function completeInstall() {
const res = await fetch('/api/v1/install/complete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
database: {type: document.getElementById('dbType').value, path: document.getElementById('dbPath').value},
redis: {enabled: document.getElementById('redisEnabled').value === 'true', host: document.getElementById('redisHost').value},
admin: {username: document.getElementById('adminUser').value, password: document.getElementById('adminPass').value, email: document.getElementById('adminEmail').value}
})
});
const data = await res.json();
document.getElementById('result').innerHTML = data.code === 0 ? '<span class="success">✓ 安装完成!<a href="/login">点击登录</a></span>' : '<span class="error">✗ ' + data.message + '</span>';
}
</script>
</body>
</html>`)
}
+4 -3
View File
@@ -33,10 +33,11 @@ type RedisConfigRequest struct {
// AdminConfigRequest 管理员配置请求
type AdminConfigRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
PasswordHash string `json:"password_hash" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
Email string `json:"email" binding:"omitempty,email"`
EncryptedConfigKey string `json:"encrypted_config_key" binding:"required"`
ConfigKeyNonce string `json:"config_key_nonce" binding:"required"`
PasswordHash string `json:"-"` // 服务端生成
EncryptedConfigKey string `json:"encrypted_config_key"`
ConfigKeyNonce string `json:"config_key_nonce"`
}
// DatabaseTestRequest 数据库连接测试请求
+45 -1
View File
@@ -154,8 +154,9 @@ func (r *installRepository) IsInstalled(ctx context.Context) (bool, error) {
if err == gorm.ErrRecordNotFound {
return false, nil
}
// 表不存在或其他错误,返回 false(未安装状态)
if err != nil {
return false, err
return false, nil
}
return value == "true", nil
@@ -163,6 +164,11 @@ func (r *installRepository) IsInstalled(ctx context.Context) (bool, error) {
// upsertSetting 插入或更新设置
func (r *installRepository) upsertSetting(ctx context.Context, key, value string) error {
// 先确保表存在
if err := r.ensureTablesExist(ctx); err != nil {
return err
}
now := time.Now()
result := r.db.WithContext(ctx).
@@ -188,6 +194,44 @@ func (r *installRepository) upsertSetting(ctx context.Context, key, value string
return nil
}
// ensureTablesExist 确保必要的表存在
func (r *installRepository) ensureTablesExist(ctx context.Context) error {
// 创建 system_settings 表
if err := r.db.WithContext(ctx).Exec(`
CREATE TABLE IF NOT EXISTS system_settings (
id TEXT PRIMARY KEY,
category TEXT,
key TEXT UNIQUE,
value TEXT,
created_at DATETIME,
updated_at DATETIME
)
`).Error; err != nil {
return err
}
// 创建 users 表
if err := r.db.WithContext(ctx).Exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE,
password_hash TEXT,
email TEXT,
email_verified BOOLEAN,
role TEXT,
status TEXT,
encrypted_config_key TEXT,
config_key_nonce TEXT,
created_at DATETIME,
updated_at DATETIME
)
`).Error; err != nil {
return err
}
return nil
}
func boolToStr(b bool) string {
if b {
return "true"