feat(settings): add settings management API endpoints

- Add GET /settings/get and POST /settings/set endpoints for managing system settings
- Implement config file persistence for settings changes
- Update config schema to include notification and webhook settings
- Add API documentation for new endpoints
- Move config path variables to global package for consistency
This commit is contained in:
2026-04-23 12:43:11 +08:00
parent 489c37e095
commit ac51641e93
6 changed files with 303 additions and 16 deletions

View File

@@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"super-frpc/global" "super-frpc/global"
"super-frpc/postLog"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
@@ -94,6 +95,8 @@ func LoadConfig(configPath string, getInitSystem func() string) error {
return fmt.Errorf("failed to create config directory: %w", err) return fmt.Errorf("failed to create config directory: %w", err)
} }
postLog.Debug(fmt.Sprintf("Loaded config: %v", global.CurrentConfig))
return nil return nil
} }
@@ -101,13 +104,13 @@ func GetConfig() *global.Config {
return &global.CurrentConfig return &global.CurrentConfig
} }
func SaveConfig(configPath string) error { func SaveConfig() error {
data, err := json.MarshalIndent(global.CurrentConfig, "", " ") data, err := json.MarshalIndent(global.CurrentConfig, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal config: %w", err) return fmt.Errorf("failed to marshal config: %w", err)
} }
if err := os.WriteFile(configPath, data, 0644); err != nil { if err := os.WriteFile(*global.ConfigPath, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err) return fmt.Errorf("failed to write config file: %w", err)
} }

View File

@@ -1573,6 +1573,134 @@ Log messages are sent as JSON objects:
--- ---
## Get Settings
**Endpoint:** `/settings/get`
**Method:** GET
**Auth Required:** Yes (token)
**Permission Level:** Superuser, Admin
Get system settings. If no `key` parameter is provided, returns all settings. If `key` is provided, returns only that specific setting.
**Request Headers:**
```
X-Token: your_token
X-Timestamp: 1704067200000
```
**Query Parameters (optional):**
```
key=ListenAddr
```
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| X-Token | string | Yes | Authentication token |
| X-Timestamp | int64 | Yes | Client timestamp in milliseconds |
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| key | string | No | Specific setting key to retrieve |
**Response (all settings):**
```json
{
"success": true,
"message": "Settings retrieved successfully",
"data": {
"ListenAddr": "0.0.0.0",
"ListenPort": "8080",
"FrpcPath": "/usr/bin/frpc",
"InstancePath": "./configs",
"Debug": true,
"Watchdog.Enabled": true,
"Watchdog.Port": 12380,
"Notification.Enabled": true,
"Notification.Method": "webhook",
"Webhook.Method": "POST",
"Webhook.URL": "https://example.com/webhook",
"Webhook.Headers": {
"Content-Type": "application/json"
},
"Webhook.Body": {
"text": "Super-frpc notification"
}
}
}
```
**Response (single setting):**
```json
{
"success": true,
"message": "Setting retrieved successfully",
"data": {
"key": "ListenAddr",
"value": "0.0.0.0"
}
}
```
**Error Response (unknown key):**
```json
{
"success": false,
"message": "Unknown setting key: InvalidKey"
}
```
---
## Set Settings
**Endpoint:** `/settings/set`
**Method:** POST
**Content-Type:** application/json
**Auth Required:** Yes (token)
**Permission Level:** Superuser, Admin
Update system settings. Multiple settings can be updated in a single request.
**Request Headers:**
```
X-Token: your_token
X-Timestamp: 1704067200000
```
**Request Body:**
```json
{
"ListenAddr": "127.0.0.1",
"ListenPort": "9090",
"Debug": false
}
```
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| X-Token | string | Yes | Authentication token |
| X-Timestamp | int64 | Yes | Client timestamp in milliseconds |
**Response:**
```json
{
"success": true,
"message": "Config saved successfully"
}
```
**Error Response:**
```json
{
"success": false,
"message": "Failed to save config"
}
```
> **Note:** Settings are saved to the config file immediately after update. Unknown keys will be logged as warnings but will not cause the request to fail.
---
## User Permissions ## User Permissions
| Permission | superuser | admin | visitor | | Permission | superuser | admin | visitor |
@@ -1584,6 +1712,8 @@ Log messages are sent as JSON objects:
| Manage users | ✓ | ✗ | ✗ | | Manage users | ✓ | ✗ | ✗ |
| List active sessions | ✓ | ✓ | ✗ | | List active sessions | ✓ | ✓ | ✗ |
| View logs | ✓ | ✓ | ✓ | | View logs | ✓ | ✓ | ✓ |
| Change settings | ✓ | ✓ | ✗ |
| Get settings | ✓ | ✓ | ✗ |
--- ---

View File

@@ -4,6 +4,10 @@ import (
"database/sql" "database/sql"
) )
var ConfigPath *string
var DBPath_data *string
var DBPath_log *string
type SoftwareInfo struct { type SoftwareInfo struct {
Name string Name string
Version string Version string
@@ -49,13 +53,23 @@ type Config struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Port int `json:"port"` Port int `json:"port"`
} `json:"watchdog"` } `json:"watchdog"`
Notification struct {
Enabled bool `json:"enabled"`
Method string `json:"method"`
} `json:"notification"`
Webhook struct {
Method string `json:"method"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Body map[string]string `json:"body"`
} `json:"webhook"`
} }
var CurrentConfig = Config{ var CurrentConfig = Config{
ListenAddr: "0.0.0.0", ListenAddr: "0.0.0.0",
ListenPort: "7000", ListenPort: "7000",
FrpcPath: "/usr/bin/frpc", FrpcPath: "",
InstancePath: "/var/lib/frpc", InstancePath: "",
Debug: false, Debug: false,
Watchdog: struct { Watchdog: struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
@@ -64,4 +78,26 @@ var CurrentConfig = Config{
Enabled: false, Enabled: false,
Port: 0, Port: 0,
}, },
Notification: struct {
Enabled bool `json:"enabled"`
Method string `json:"method"`
}{
Enabled: false,
Method: "webhook",
},
Webhook: struct {
Method string `json:"method"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Body map[string]string `json:"body"`
}{
Method: "",
URL: "",
Headers: map[string]string{
"Content-Type": "",
},
Body: map[string]string{
"text": "",
},
},
} }

View File

@@ -1,9 +1,13 @@
package handlers package handlers
import ( import (
"encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"super-frpc/config"
"super-frpc/global"
"super-frpc/postLog" "super-frpc/postLog"
"super-frpc/utils" "super-frpc/utils"
) )
@@ -15,4 +19,113 @@ func GetSettingsHandler(w http.ResponseWriter, r *http.Request) {
postLog.Warning(fmt.Sprintf("[GetSettingsHandler] Auth failed: %v", err)) postLog.Warning(fmt.Sprintf("[GetSettingsHandler] Auth failed: %v", err))
return return
} }
key := r.URL.Query().Get("key")
if key != "" {
var value interface{}
switch key {
case "ListenAddr":
value = global.CurrentConfig.ListenAddr
case "ListenPort":
value = global.CurrentConfig.ListenPort
case "FrpcPath":
value = global.CurrentConfig.FrpcPath
case "InstancePath":
value = global.CurrentConfig.InstancePath
case "Debug":
value = global.CurrentConfig.Debug
case "Watchdog.Enabled":
value = global.CurrentConfig.Watchdog.Enabled
case "Watchdog.Port":
value = global.CurrentConfig.Watchdog.Port
case "Notification.Enabled":
value = global.CurrentConfig.Notification.Enabled
case "Notification.Method":
value = global.CurrentConfig.Notification.Method
case "Webhook.Method":
value = global.CurrentConfig.Webhook.Method
case "Webhook.URL":
value = global.CurrentConfig.Webhook.URL
case "Webhook.Headers":
value = global.CurrentConfig.Webhook.Headers
case "Webhook.Body":
value = global.CurrentConfig.Webhook.Body
default:
utils.SendErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("Unknown setting key: %s", key))
return
}
utils.SendSuccessResponse(w, "Setting retrieved successfully", map[string]interface{}{
"key": key,
"value": value,
})
} else {
settings := map[string]interface{}{
"ListenAddr": global.CurrentConfig.ListenAddr,
"ListenPort": global.CurrentConfig.ListenPort,
"FrpcPath": global.CurrentConfig.FrpcPath,
"InstancePath": global.CurrentConfig.InstancePath,
"Debug": global.CurrentConfig.Debug,
"Watchdog.Enabled": global.CurrentConfig.Watchdog.Enabled,
"Watchdog.Port": global.CurrentConfig.Watchdog.Port,
"Notification.Enabled": global.CurrentConfig.Notification.Enabled,
"Notification.Method": global.CurrentConfig.Notification.Method,
"Webhook.Method": global.CurrentConfig.Webhook.Method,
"Webhook.URL": global.CurrentConfig.Webhook.URL,
"Webhook.Headers": global.CurrentConfig.Webhook.Headers,
"Webhook.Body": global.CurrentConfig.Webhook.Body,
}
utils.SendSuccessResponse(w, "Settings retrieved successfully", settings)
}
}
func SetSettingsHandler(w http.ResponseWriter, r *http.Request) {
_, err := utils.Auth(w, r, http.MethodPost, "superuser", "admin")
if err != nil {
utils.SendErrorResponse(w, http.StatusUnauthorized, err.Error())
postLog.Warning(fmt.Sprintf("[SetSettingsHandler] Auth failed: %v", err))
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
postLog.Error(fmt.Sprintf("[SetSettingsHandler] Failed to read request body: %v", err))
utils.SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body")
return
}
defer r.Body.Close()
var reqMap map[string]interface{}
if err := json.Unmarshal(body, &reqMap); err != nil {
postLog.Error(fmt.Sprintf("[SetSettingsHandler] Failed to unmarshal request body: %v", err))
utils.SendErrorResponse(w, http.StatusBadRequest, "Invalid request format")
return
}
for key, value := range reqMap {
postLog.Debug(fmt.Sprintf("[SetSettingsHandler] Modify setting: %s = %v", key, value))
switch key {
case "ListenAddr":
if v, ok := value.(string); ok {
global.CurrentConfig.ListenAddr = v
}
case "ListenPort":
if v, ok := value.(float64); ok {
global.CurrentConfig.ListenPort = fmt.Sprintf("%d", int(v))
}
default:
postLog.Warning(fmt.Sprintf("[SetSettingsHandler] Unknown setting key: %s", key))
}
}
err = config.SaveConfig()
if err != nil {
postLog.Error(fmt.Sprintf("[SetSettingsHandler] Failed to save config: %v", err))
utils.SendErrorResponse(w, http.StatusInternalServerError, "Failed to save config")
return
}
postLog.Debug("[SetSettingsHandler] Config saved successfully")
utils.SendSuccessResponse(w, "Config saved successfully", nil)
} }

23
main.go
View File

@@ -22,41 +22,44 @@ import (
func main() { func main() {
postLog.Info(fmt.Sprintf("%s %s (Build %d.%s) by %s", global.Software.Name, global.Software.Version, global.Software.BuildVer, global.Software.BuildType, global.Software.Developer)) postLog.Info(fmt.Sprintf("%s %s (Build %d.%s) by %s", global.Software.Name, global.Software.Version, global.Software.BuildVer, global.Software.BuildType, global.Software.Developer))
configPath := flag.String("config", "./config.json", "path to config file") global.ConfigPath = flag.String("config", "./config.json", "path to config file")
dbPath_data := flag.String("db", "./database.db", "path to database file") global.DBPath_data = flag.String("db", "./database.db", "path to database file")
dbPath_log := flag.String("log", "./logs.db", "path to logs database file") global.DBPath_log = flag.String("log", "./logs.db", "path to logs database file")
flag.Parse() flag.Parse()
if _, err := os.Stat(*configPath); os.IsNotExist(err) { if _, err := os.Stat(*global.ConfigPath); os.IsNotExist(err) {
// Load the default config // Load the default config
configData, err := json.MarshalIndent(global.CurrentConfig, "", " ") configData, err := json.MarshalIndent(global.CurrentConfig, "", " ")
if err != nil { if err != nil {
postLog.Warning(fmt.Sprintf("Failed to marshal default config: %v", err)) postLog.Warning(fmt.Sprintf("Failed to marshal default config: %v", err))
} else if err := os.WriteFile(*configPath, configData, 0644); err != nil { } else if err := os.WriteFile(*global.ConfigPath, configData, 0644); err != nil {
postLog.Warning(fmt.Sprintf("Failed to create default config file: %v", err)) postLog.Warning(fmt.Sprintf("Failed to create default config file: %v", err))
} }
postLog.Info(fmt.Sprintf("Created default config file at %s", *configPath)) postLog.Info(fmt.Sprintf("Created default config file at %s", *global.ConfigPath))
} }
err := config.LoadConfig(*configPath, service.GetInitSystem) err := config.LoadConfig(*global.ConfigPath, service.GetInitSystem)
if err != nil { if err != nil {
postLog.Fatal(fmt.Sprintf("Failed to load config: %v", err)) postLog.Fatal(fmt.Sprintf("Failed to load config: %v", err))
} }
if global.Software.BuildType == "debug" { // If debug build, set debug mode to true
global.CurrentConfig.Debug = true
}
postLog.SetDebugMode(global.CurrentConfig.Debug) postLog.SetDebugMode(global.CurrentConfig.Debug)
global.Is.Debug = global.CurrentConfig.Debug global.Is.Debug = global.CurrentConfig.Debug
if err := postLog.InitLogsDatabase(*dbPath_log); err != nil { if err := postLog.InitLogsDatabase(*global.DBPath_log); err != nil {
postLog.Fatal(fmt.Sprintf("Failed to initialize logs database: %v", err)) postLog.Fatal(fmt.Sprintf("Failed to initialize logs database: %v", err))
} }
postLog.Info("Logs database initialized successfully") postLog.Info("Logs database initialized successfully")
if err := database.InitDatabase(*dbPath_data, *dbPath_log); err != nil { if err := database.InitDatabase(*global.DBPath_data, *global.DBPath_log); err != nil {
postLog.Fatal(fmt.Sprintf("Failed to initialize database: %v", err)) postLog.Fatal(fmt.Sprintf("Failed to initialize database: %v", err))
} }
postLog.Info("Database initialized successfully") postLog.Info("Database initialized successfully")
if err := database.InitFrpcDatabase(*dbPath_data); err != nil { if err := database.InitFrpcDatabase(*global.DBPath_data); err != nil {
postLog.Fatal(fmt.Sprintf("Failed to initialize frpc database: %v", err)) postLog.Fatal(fmt.Sprintf("Failed to initialize frpc database: %v", err))
} }

View File

@@ -11,10 +11,12 @@ import (
) )
func setupRoutes() { func setupRoutes() {
http.HandleFunc("/system/getStatus", GetStatusHandler)
http.HandleFunc("/system/getSoftwareInfo", GetSoftwareInfoHandler)
systemLogHandler := postLog.NewLogSocketHandler(postLog.GetLogBroadcaster()) systemLogHandler := postLog.NewLogSocketHandler(postLog.GetLogBroadcaster())
http.HandleFunc("/system/getLogs", systemLogHandler.Handle) http.HandleFunc("/system/getLogs", systemLogHandler.Handle)
http.HandleFunc("/system/getStatus", GetStatusHandler)
http.HandleFunc("/system/getSoftwareInfo", GetSoftwareInfoHandler)
http.HandleFunc("/system/settings/get", handlers.GetSettingsHandler)
http.HandleFunc("/system/settings/set", handlers.SetSettingsHandler)
http.HandleFunc("/register", handlers.RegisterHandler) http.HandleFunc("/register", handlers.RegisterHandler)
http.HandleFunc("/login", handlers.LoginHandler) http.HandleFunc("/login", handlers.LoginHandler)