Files
backend/frpAct.go
NanamiAdmin 49048597f2 refactor(database): make DBListFrpcInstances able to list all users' frpc instances but not single user
feat(frpcAct): user will get createdBy when query frpc instances on current server
docs(api): update related api documentation
2026-03-12 22:41:17 +08:00

697 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"super-frpc/postLog"
"time"
"gopkg.in/ini.v1"
)
type InstanceInfo struct {
Name string `json:"name"`
ServerAddr string `json:"serverAddr"`
ServerPort string `json:"serverPort"`
AuthMethod string `json:"auth_method"`
// BootAtStart bool `json:"bootAtStart"`
RunUser string `json:"runUser"`
Additional map[string]interface{} `json:"additionalProperties"`
}
type FrpcProxyInfo struct {
Name string `json:"name"`
Type string `json:"type"`
LocalIP string `json:"local_ip"`
LocalPort string `json:"local_port"`
RemotePort string `json:"remote_port"`
}
type CreateInstanceRequest struct {
InstanceInfo InstanceInfo `json:"instanceInfo"`
BootAtStart bool `json:"bootAtStart"`
RunUser string `json:"runUser"`
Additional map[string]interface{} `json:"additionalProperties"`
}
var frpcDB *sql.DB
func CloseFrpcDatabase() error {
if frpcDB != nil {
return frpcDB.Close()
}
return nil
}
func CreateInstanceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method")
postLog.Debug(fmt.Sprintf("[CreateInstanceHandler] Invalid request method: %s", r.Method))
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to read request body: %v", err))
SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body")
return
}
defer r.Body.Close()
// 先解析为map处理类型不匹配的情况
var reqMap map[string]interface{}
if err := json.Unmarshal(body, &reqMap); err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to unmarshal request body: %v", err))
SendErrorResponse(w, http.StatusBadRequest, "Invalid request format")
return
}
// 处理bootAtStart字段
bootAtStart := false
if bas, ok := reqMap["bootAtStart"]; ok {
switch v := bas.(type) {
case bool:
bootAtStart = v
case string:
if v == "true" {
bootAtStart = true
}
}
}
// 处理instanceInfo字段
instanceInfoMap, ok := reqMap["instanceInfo"].(map[string]interface{})
if !ok {
SendErrorResponse(w, http.StatusBadRequest, "Invalid instanceInfo format")
return
}
instanceInfo := InstanceInfo{
Name: getStringFromMap(instanceInfoMap, "name"),
ServerAddr: getStringFromMap(instanceInfoMap, "serverAddr"),
ServerPort: getStringFromMap(instanceInfoMap, "serverPort"),
AuthMethod: getStringFromMap(instanceInfoMap, "auth_method"),
RunUser: getStringFromMap(reqMap, "runUser"),
}
// 处理additionalProperties字段
if additional, ok := reqMap["additionalProperties"].(map[string]interface{}); ok {
instanceInfo.Additional = additional
}
// 从Header中验证token和timeStamp
userID, _, err := ValidateRequestWithHeader(w, r)
if err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to validate request header: %v", err))
SendErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
// 构建请求结构体
req := CreateInstanceRequest{
InstanceInfo: instanceInfo,
BootAtStart: bootAtStart,
RunUser: instanceInfo.RunUser,
Additional: instanceInfo.Additional,
}
if err := CheckPermission(userID, "superuser", "admin"); err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to check permission: %v", err))
SendErrorResponse(w, http.StatusForbidden, err.Error())
return
}
if req.InstanceInfo.Name == "" || req.InstanceInfo.ServerAddr == "" ||
req.InstanceInfo.ServerPort == "" || req.InstanceInfo.AuthMethod == "" {
SendErrorResponse(w, http.StatusBadRequest, "Missing required fields in instanceInfo")
return
}
runUser := req.RunUser
if runUser == "" {
runUser = "root"
}
user, err := GetUserByID(userID)
if err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to get user info: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info")
return
}
configDir, err := GetConfigDir()
if err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to get config directory: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to get config directory")
return
}
// Add frpc instance
configFileName := fmt.Sprintf("superfrpc_%s_%s.toml", user.Username, req.InstanceInfo.Name)
configPath := filepath.Join(configDir, configFileName)
configContent := generateFrpcConfig(req.InstanceInfo)
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to create config file %s: %v", configPath, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to create config file")
return
}
instance := FrpcInstance{
UserID: userID,
Name: req.InstanceInfo.Name,
ServerAddr: req.InstanceInfo.ServerAddr,
ServerPort: req.InstanceInfo.ServerPort,
AuthMethod: req.InstanceInfo.AuthMethod,
BootAtStart: req.BootAtStart,
RunUser: runUser,
ConfigPath: configPath,
}
if err := DBAddFrpcInstance(instance); err != nil {
os.Remove(configPath)
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to save instance %s to database: %v", req.InstanceInfo.Name, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to save instance to database")
return
}
if req.BootAtStart {
if err := createBootService(user.Username, req.InstanceInfo.Name, configPath, runUser); err != nil {
frpcDB.Exec("DELETE FROM frpcInstances WHERE userID = ? AND name = ?", userID, req.InstanceInfo.Name)
os.Remove(configPath)
postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to create boot service for instance %s: %v", req.InstanceInfo.Name, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to create boot service")
return
}
}
// Finish add frpc instance
SendSuccessResponse(w, "Instance created successfully", map[string]interface{}{
"name": req.InstanceInfo.Name,
"configPath": configPath,
"bootAtStart": req.BootAtStart,
})
postLog.Info(fmt.Sprintf("[CreateInstanceHandler] Instance %s created successfully: configPath=%s, bootAtStart=%v, runUser=%s, additionalProperties=%v", req.InstanceInfo.Name, configPath, req.BootAtStart, runUser, req.Additional))
}
func DeleteInstanceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method")
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to read request body: %v", err))
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("[DeleteInstanceHandler] Failed to unmarshal request body: %v", err))
SendErrorResponse(w, http.StatusBadRequest, "Invalid request format")
return
}
instanceName := getStringFromMap(reqMap, "instanceName")
if instanceName == "" {
SendErrorResponse(w, http.StatusBadRequest, "instanceName is required")
return
}
userID, _, err := ValidateRequestWithHeader(w, r)
if err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to validate request header: %v", err))
SendErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if err := CheckPermission(userID, "superuser", "admin"); err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to check permission: %v", err))
SendErrorResponse(w, http.StatusForbidden, err.Error())
return
}
user, err := GetUserByID(userID)
if err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to get user info: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info")
return
}
var instance FrpcInstance
// err = frpcDB.QueryRow(`
// SELECT id, userID, name, serverAddr, serverPort, auth_method, bootAtStart, runUser, configPath, createdAt
// FROM frpcInstances WHERE userID = ? AND name = ?
// `, userID, instanceName).Scan(
// &instance.ID, &instance.UserID, &instance.Name, &instance.ServerAddr, &instance.ServerPort,
// &instance.AuthMethod, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &instance.CreatedAt)
instance, err = DBQueryFrpcInstance(userID, instanceName)
if err == sql.ErrNoRows {
SendErrorResponse(w, http.StatusNotFound, "Instance not found")
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] User %d tried to delete a not existed instance: %s", userID, instanceName))
return
}
if err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to query instance: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to query instance")
return
}
if instance.BootAtStart {
if err := removeBootService(user.Username, instanceName); err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to remove boot service for instance %s: %v", instanceName, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to remove boot service")
return
}
}
if _, err := os.Stat(instance.ConfigPath); err == nil {
if err := os.Remove(instance.ConfigPath); err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to remove config file %s: %v", instance.ConfigPath, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to remove config file")
return
}
}
if err := DBRemoveFrpcInstance(userID, instanceName); err != nil {
postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to delete instance %s from database: %v", instanceName, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to delete instance from database")
return
}
SendSuccessResponse(w, "Instance deleted successfully", map[string]interface{}{
"name": instanceName,
})
postLog.Info(fmt.Sprintf("[DeleteInstanceHandler] Instance %s deleted successfully", instanceName))
}
func ModifyInstanceHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Invalid request method: %s", r.Method))
SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method")
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to read request body: %v", err))
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("[ModifyInstanceHandler] Failed to unmarshal request body: %v", err))
SendErrorResponse(w, http.StatusBadRequest, "Invalid request format")
return
}
instanceName := getStringFromMap(reqMap, "instanceName")
if instanceName == "" {
SendErrorResponse(w, http.StatusBadRequest, "instanceName is required")
return
}
instanceID := getStringFromMap(reqMap, "instanceID")
if instanceID == "" {
SendErrorResponse(w, http.StatusBadRequest, "instanceID is required")
return
}
modifyType := getStringFromMap(reqMap, "type")
if modifyType == "" {
SendErrorResponse(w, http.StatusBadRequest, "type is required")
return
}
if modifyType != "configFile" && modifyType != "systemConfig" { // Detect valid modify type
SendErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("Unknown modify type %s", modifyType))
return
}
modifiedData, ok := reqMap["modifiedData"].(map[string]interface{})
if !ok || modifiedData == nil {
SendErrorResponse(w, http.StatusBadRequest, "modifiedData is required and must be an object")
return
}
// 从Header中验证token和timeStamp
userID, _, err := ValidateRequestWithHeader(w, r)
if err != nil {
postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to validate request header: %v", err))
SendErrorResponse(w, http.StatusUnauthorized, err.Error())
return
}
if err := CheckPermission(userID, "superuser", "admin"); err != nil {
postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to check permission: %v", err))
SendErrorResponse(w, http.StatusForbidden, err.Error())
return
}
user, err := GetUserByID(userID)
if err != nil {
postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to get user info: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info")
return
}
instance, err := DBQueryFrpcInstance(userID, instanceName)
if err == sql.ErrNoRows {
postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] User %d tried to modify a not existed instance: %s", userID, instanceName))
SendErrorResponse(w, http.StatusNotFound, "Instance not found")
return
}
if err != nil {
postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to query instance: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to query instance")
return
}
// 验证 instanceID 是否匹配
if fmt.Sprintf("%d", instance.ID) != instanceID {
SendErrorResponse(w, http.StatusBadRequest, "instanceID does not match instanceName")
return
}
if modifyType == "configFile" {
handleConfigFileModify(w, instance, modifiedData, user.Username)
} else {
handleSystemConfigModify(w, r, instance, modifiedData, user)
}
}
func handleConfigFileModify(w http.ResponseWriter, instance FrpcInstance, modifiedData map[string]interface{}, username string) {
configPath := instance.ConfigPath
// Read current config file content
configContent, err := os.ReadFile(configPath)
if err != nil {
postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to read config file %s: %v", configPath, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to read config file")
return
}
// Parse config file content
updatedConfig, err := updateCommonSection(string(configContent), modifiedData)
if err != nil {
postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to update common section: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to update config file")
return
}
// Write updated config file content back to file
if err := os.WriteFile(configPath, []byte(updatedConfig), 0644); err != nil {
postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to write config file %s: %v", configPath, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to write config file")
return
}
// Update instance fields in database
if v, ok := modifiedData["server_addr"].(string); ok && v != "" {
instance.ServerAddr = v
}
if v, ok := modifiedData["server_port"].(string); ok && v != "" {
instance.ServerPort = v
}
if v, ok := modifiedData["auth_method"].(string); ok && v != "" {
instance.AuthMethod = v
}
if err := DBUpdateFrpcInstance(instance); err != nil {
postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to update instance in database: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to update instance in database")
return
}
SendSuccessResponse(w, "Config file modified successfully", map[string]interface{}{
"instanceName": instance.Name,
"instanceID": instance.ID,
"configPath": configPath,
})
postLog.Info(fmt.Sprintf("[handleConfigFileModify] Config file for instance %s modified successfully", instance.Name))
}
func updateCommonSection(configContent string, modifiedData map[string]interface{}) (string, error) {
cfg, err := ini.Load([]byte(configContent))
if err != nil {
return "", fmt.Errorf("failed to parse config: %w", err)
}
commonSection := cfg.Section("common")
if commonSection == nil {
return "", fmt.Errorf("common section not found")
}
for key, value := range modifiedData {
commonSection.Key(key).SetValue(formatConfigValue(value))
}
var buf strings.Builder
if _, err := cfg.WriteTo(&buf); err != nil {
return "", fmt.Errorf("failed to write config: %w", err)
}
return buf.String(), nil
}
func formatConfigValue(value interface{}) string {
switch v := value.(type) {
case string:
return v
case bool:
return fmt.Sprintf("%t", v)
case float64:
if v == float64(int64(v)) {
return fmt.Sprintf("%d", int64(v))
}
return fmt.Sprintf("%f", v)
default:
return fmt.Sprintf("%v", v)
}
}
func handleSystemConfigModify(w http.ResponseWriter, r *http.Request, instance FrpcInstance, modifiedData map[string]interface{}, user *User) {
newName := instance.Name
newRunUser := instance.RunUser
newBootAtStart := instance.BootAtStart
oldBootAtStart := instance.BootAtStart
var bootServiceError string
if v, ok := modifiedData["name"].(string); ok && v != "" {
newName = v
}
if v, ok := modifiedData["runUser"].(string); ok {
newRunUser = v
}
if v, ok := modifiedData["bootAtStart"].(bool); ok {
newBootAtStart = v
}
oldConfigPath := instance.ConfigPath
var newConfigPath string
// If instance name or run user changed, need to rename config file
if newName != instance.Name || newRunUser != instance.RunUser {
configDir, err := GetConfigDir()
if err != nil {
postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to get config directory: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to get config directory")
return
}
newConfigFileName := fmt.Sprintf("superfrpc_%s_%s.toml", user.Username, newName)
newConfigPath = filepath.Join(configDir, newConfigFileName)
if oldConfigPath != newConfigPath {
if _, err := os.Stat(oldConfigPath); err == nil {
if err := os.Rename(oldConfigPath, newConfigPath); err != nil {
postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to rename config file %s to %s: %v", oldConfigPath, newConfigPath, err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to rename config file")
return
}
}
}
} else {
newConfigPath = oldConfigPath
}
// Update instance fields in database
instance.Name = newName
instance.RunUser = newRunUser
instance.BootAtStart = newBootAtStart
instance.ConfigPath = newConfigPath
if err := DBUpdateFrpcInstance(instance); err != nil {
postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to update instance in database: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to update instance in database")
return
}
// Handle boot service creation and removal
if oldBootAtStart && !newBootAtStart {
if err := removeBootService(user.Username, instance.Name); err != nil {
postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to remove boot service: %v", err))
bootServiceError = fmt.Sprintf("Failed to remove boot service: %v", err)
}
} else if !oldBootAtStart && newBootAtStart {
if err := createBootService(user.Username, newName, newConfigPath, newRunUser); err != nil {
postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to create boot service: %v", err))
bootServiceError = fmt.Sprintf("Failed to create boot service: %v", err)
}
} else if oldBootAtStart && newBootAtStart && (instance.Name != newName || instance.RunUser != newRunUser) {
if err := removeBootService(user.Username, instance.Name); err != nil {
postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to remove old boot service: %v", err))
bootServiceError = fmt.Sprintf("Failed to remove old boot service: %v", err)
}
if err := createBootService(user.Username, newName, newConfigPath, newRunUser); err != nil {
postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to create new boot service: %v", err))
if bootServiceError != "" {
bootServiceError += "; "
}
bootServiceError += fmt.Sprintf("Failed to create new boot service: %v", err)
}
}
data := map[string]interface{}{
"instanceName": newName,
"instanceID": instance.ID,
"configPath": newConfigPath,
"bootAtStart": newBootAtStart,
"runUser": newRunUser,
}
if bootServiceError != "" {
data["bootServiceError"] = bootServiceError
}
SendSuccessResponse(w, "System config modified successfully", data)
postLog.Info(fmt.Sprintf("[handleSystemConfigModify] System config for instance %s modified successfully: bootAtStart=%v, runUser=%s", newName, newBootAtStart, newRunUser))
}
func ListInstancesHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method")
return
}
userID, _, err := ValidateRequestWithHeader(w, r)
if err != nil {
postLog.Error(fmt.Sprintf("[ListInstancesHandler] Failed to validate request: %v", err))
SendErrorResponse(w, http.StatusUnauthorized, "Failed to validate request")
return
}
userType, err := GetUserType(userID)
if err != nil {
postLog.Error(fmt.Sprintf("[ListInstancesHandler] Failed to get user type: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user type")
return
}
instanceList, err := DBListFrpcInstances()
if err != nil {
postLog.Error(fmt.Sprintf("[ListInstancesHandler] Failed to query instances: %v", err))
SendErrorResponse(w, http.StatusInternalServerError, "Failed to query instances")
return
}
var responseInstances []map[string]interface{}
for _, instance := range instanceList {
instanceData := map[string]interface{}{
"instanceID": instance.ID,
"name": instance.Name,
"serverAddr": instance.ServerAddr,
"serverPort": instance.ServerPort,
"auth_method": instance.AuthMethod,
"bootAtStart": instance.BootAtStart,
"runUser": instance.RunUser,
"configPath": instance.ConfigPath,
"createdAt": instance.CreatedAt,
"createdBy": instance.CreatedBy,
}
if userType == "visitor" {
delete(instanceData, "serverAddr")
delete(instanceData, "serverPort")
delete(instanceData, "auth_method")
}
responseInstances = append(responseInstances, instanceData)
}
if responseInstances == nil {
responseInstances = []map[string]interface{}{}
}
SendSuccessResponse(w, "Instances retrieved successfully", responseInstances)
}
func generateFrpcConfig(info InstanceInfo) string {
var sb strings.Builder
sb.WriteString("[common]\n")
sb.WriteString(fmt.Sprintf("server_addr = %s\n", info.ServerAddr))
sb.WriteString(fmt.Sprintf("server_port = %s\n", info.ServerPort))
sb.WriteString(fmt.Sprintf("auth_method = %s\n", info.AuthMethod))
for key, value := range info.Additional {
sb.WriteString(fmt.Sprintf("%s = %v\n", key, value))
}
return sb.String()
}
func addFrpcProxy(info FrpcProxyInfo) string {
var sb strings.Builder
sb.WriteString("[[proxies]]\n")
sb.WriteString(fmt.Sprintf("name = %s\n", info.Name))
sb.WriteString(fmt.Sprintf("type = %s\n", info.Type))
sb.WriteString(fmt.Sprintf("local_ip = %s\n", info.LocalIP))
sb.WriteString(fmt.Sprintf("local_port = %s\n", info.LocalPort))
sb.WriteString(fmt.Sprintf("remote_port = %s\n", info.RemotePort))
return sb.String()
}
func GetUserInstances(userID int) ([]FrpcInstance, error) {
rows, err := frpcDB.Query(`
SELECT id, userID, name, serverAddr, serverPort, auth_method, bootAtStart, runUser, configPath, createdAt
FROM frpcInstances WHERE userID = ?
`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var instances []FrpcInstance
for rows.Next() {
var instance FrpcInstance
var createdAtStr string
if err := rows.Scan(
&instance.ID, &instance.UserID, &instance.Name, &instance.ServerAddr, &instance.ServerPort,
&instance.AuthMethod, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr,
); err != nil {
return nil, err
}
instance.CreatedAt, _ = time.Parse(time.RFC3339, createdAtStr)
instances = append(instances, instance)
}
return instances, nil
}
func getStringFromMap(m map[string]interface{}, key string) string {
if v, ok := m[key].(string); ok {
return v
}
return ""
}