Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92730a7f45 | |||
| d4d2d74811 | |||
| 530cfed686 | |||
| c7c10c7c17 | |||
| ee54689fe9 | |||
| f1b01ec18f |
+5
-4
@@ -3,7 +3,8 @@ FROM golang:1.24-alpine AS builder
|
||||
|
||||
ENV GOTOOLCHAIN=auto
|
||||
|
||||
RUN apk add --no-cache git
|
||||
# Install build dependencies for SQLite3
|
||||
RUN apk add --no-cache git gcc musl-dev
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -15,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
|
||||
|
||||
@@ -201,9 +201,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 +214,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`)
|
||||
}
|
||||
|
||||
@@ -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 数据库连接测试请求
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user