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"
|
"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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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
|
## 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 | ✓ | ✓ | ✗ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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": "",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
23
main.go
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user