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:
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"super-frpc/global"
|
||||
"super-frpc/postLog"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
postLog.Debug(fmt.Sprintf("Loaded config: %v", global.CurrentConfig))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,13 +104,13 @@ func GetConfig() *global.Config {
|
||||
return &global.CurrentConfig
|
||||
}
|
||||
|
||||
func SaveConfig(configPath string) error {
|
||||
func SaveConfig() error {
|
||||
data, err := json.MarshalIndent(global.CurrentConfig, "", " ")
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
130
docs/api.md
130
docs/api.md
@@ -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
|
||||
|
||||
| Permission | superuser | admin | visitor |
|
||||
@@ -1584,6 +1712,8 @@ Log messages are sent as JSON objects:
|
||||
| Manage users | ✓ | ✗ | ✗ |
|
||||
| List active sessions | ✓ | ✓ | ✗ |
|
||||
| View logs | ✓ | ✓ | ✓ |
|
||||
| Change settings | ✓ | ✓ | ✗ |
|
||||
| Get settings | ✓ | ✓ | ✗ |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
var ConfigPath *string
|
||||
var DBPath_data *string
|
||||
var DBPath_log *string
|
||||
|
||||
type SoftwareInfo struct {
|
||||
Name string
|
||||
Version string
|
||||
@@ -49,13 +53,23 @@ type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Port int `json:"port"`
|
||||
} `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{
|
||||
ListenAddr: "0.0.0.0",
|
||||
ListenPort: "7000",
|
||||
FrpcPath: "/usr/bin/frpc",
|
||||
InstancePath: "/var/lib/frpc",
|
||||
FrpcPath: "",
|
||||
InstancePath: "",
|
||||
Debug: false,
|
||||
Watchdog: struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -64,4 +78,26 @@ var CurrentConfig = Config{
|
||||
Enabled: false,
|
||||
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": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"super-frpc/config"
|
||||
"super-frpc/global"
|
||||
"super-frpc/postLog"
|
||||
"super-frpc/utils"
|
||||
)
|
||||
@@ -15,4 +19,113 @@ func GetSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
postLog.Warning(fmt.Sprintf("[GetSettingsHandler] Auth failed: %v", err))
|
||||
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
23
main.go
@@ -22,41 +22,44 @@ import (
|
||||
|
||||
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))
|
||||
configPath := flag.String("config", "./config.json", "path to config file")
|
||||
dbPath_data := flag.String("db", "./database.db", "path to database file")
|
||||
dbPath_log := flag.String("log", "./logs.db", "path to logs database file")
|
||||
global.ConfigPath = flag.String("config", "./config.json", "path to config file")
|
||||
global.DBPath_data = flag.String("db", "./database.db", "path to database file")
|
||||
global.DBPath_log = flag.String("log", "./logs.db", "path to logs database file")
|
||||
flag.Parse()
|
||||
|
||||
if _, err := os.Stat(*configPath); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(*global.ConfigPath); os.IsNotExist(err) {
|
||||
// Load the default config
|
||||
configData, err := json.MarshalIndent(global.CurrentConfig, "", " ")
|
||||
if err != nil {
|
||||
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.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 {
|
||||
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)
|
||||
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.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.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))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ import (
|
||||
)
|
||||
|
||||
func setupRoutes() {
|
||||
http.HandleFunc("/system/getStatus", GetStatusHandler)
|
||||
http.HandleFunc("/system/getSoftwareInfo", GetSoftwareInfoHandler)
|
||||
systemLogHandler := postLog.NewLogSocketHandler(postLog.GetLogBroadcaster())
|
||||
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("/login", handlers.LoginHandler)
|
||||
|
||||
Reference in New Issue
Block a user