add step list for server creation
This commit is contained in:
12
local/utl/cache/cache.go
vendored
12
local/utl/cache/cache.go
vendored
@@ -9,26 +9,22 @@ import (
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
// CacheItem represents an item in the cache
|
||||
type CacheItem struct {
|
||||
Value interface{}
|
||||
Expiration int64
|
||||
}
|
||||
|
||||
// InMemoryCache is a thread-safe in-memory cache
|
||||
type InMemoryCache struct {
|
||||
items map[string]CacheItem
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewInMemoryCache creates and returns a new InMemoryCache instance
|
||||
func NewInMemoryCache() *InMemoryCache {
|
||||
return &InMemoryCache{
|
||||
items: make(map[string]CacheItem),
|
||||
}
|
||||
}
|
||||
|
||||
// Set adds an item to the cache with an expiration duration (in seconds)
|
||||
func (c *InMemoryCache) Set(key string, value interface{}, duration time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -44,7 +40,6 @@ func (c *InMemoryCache) Set(key string, value interface{}, duration time.Duratio
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves an item from the cache
|
||||
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
@@ -55,24 +50,18 @@ func (c *InMemoryCache) Get(key string) (interface{}, bool) {
|
||||
}
|
||||
|
||||
if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
|
||||
// Item has expired, but don't delete here to avoid lock upgrade.
|
||||
// It will be overwritten on the next Set.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return item.Value, true
|
||||
}
|
||||
|
||||
// Delete removes an item from the cache
|
||||
func (c *InMemoryCache) Delete(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.items, key)
|
||||
}
|
||||
|
||||
// GetOrSet retrieves an item from the cache. If the item is not found, it
|
||||
// calls the provided function to get the value, sets it in the cache, and
|
||||
// returns it.
|
||||
func GetOrSet[T any](c *InMemoryCache, key string, duration time.Duration, fetcher func() (T, error)) (T, error) {
|
||||
if cached, found := c.Get(key); found {
|
||||
if value, ok := cached.(T); ok {
|
||||
@@ -90,7 +79,6 @@ func GetOrSet[T any](c *InMemoryCache, key string, duration time.Duration, fetch
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Start initializes the cache and provides it to the DI container.
|
||||
func Start(di *dig.Container) {
|
||||
cache := NewInMemoryCache()
|
||||
err := di.Provide(func() *InMemoryCache {
|
||||
|
||||
245
local/utl/command/callback_executor.go
Normal file
245
local/utl/command/callback_executor.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/utl/logging"
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CallbackInteractiveCommandExecutor struct {
|
||||
*InteractiveCommandExecutor
|
||||
callbacks *CallbackConfig
|
||||
serverID uuid.UUID
|
||||
}
|
||||
|
||||
func NewCallbackInteractiveCommandExecutor(baseExecutor *CommandExecutor, tfaManager *model.Steam2FAManager, callbacks *CallbackConfig, serverID uuid.UUID) *CallbackInteractiveCommandExecutor {
|
||||
if callbacks == nil {
|
||||
callbacks = DefaultCallbackConfig()
|
||||
}
|
||||
|
||||
return &CallbackInteractiveCommandExecutor{
|
||||
InteractiveCommandExecutor: &InteractiveCommandExecutor{
|
||||
CommandExecutor: baseExecutor,
|
||||
tfaManager: tfaManager,
|
||||
},
|
||||
callbacks: callbacks,
|
||||
serverID: serverID,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *CallbackInteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, serverID *uuid.UUID, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, e.ExePath, args...)
|
||||
|
||||
if e.WorkDir != "" {
|
||||
cmd.Dir = e.WorkDir
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdin pipe: %v", err)
|
||||
}
|
||||
defer stdin.Close()
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdout pipe: %v", err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stderr pipe: %v", err)
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
logging.Info("Executing interactive command with callbacks: %s %s", e.ExePath, strings.Join(args, " "))
|
||||
|
||||
e.callbacks.OnCommand(e.serverID, e.ExePath, args, false, false, "")
|
||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Starting command: %s %s", e.ExePath, strings.Join(args, " ")), false)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to start command: %v", err), true)
|
||||
return fmt.Errorf("failed to start command: %v", err)
|
||||
}
|
||||
|
||||
outputDone := make(chan error, 1)
|
||||
cmdDone := make(chan error, 1)
|
||||
|
||||
go e.monitorOutputWithCallbacks(ctx, stdout, stderr, serverID, outputDone)
|
||||
|
||||
go func() {
|
||||
cmdDone <- cmd.Wait()
|
||||
}()
|
||||
|
||||
var cmdErr, outputErr error
|
||||
completedCount := 0
|
||||
|
||||
for completedCount < 2 {
|
||||
select {
|
||||
case cmdErr = <-cmdDone:
|
||||
completedCount++
|
||||
logging.Info("Command execution completed")
|
||||
e.callbacks.OnOutput(e.serverID, "Command execution completed", false)
|
||||
case outputErr = <-outputDone:
|
||||
completedCount++
|
||||
logging.Info("Output monitoring completed")
|
||||
case <-ctx.Done():
|
||||
e.callbacks.OnOutput(e.serverID, "Command execution cancelled", true)
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if outputErr != nil {
|
||||
logging.Warn("Output monitoring error: %v", outputErr)
|
||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Output monitoring error: %v", outputErr), true)
|
||||
}
|
||||
|
||||
success := cmdErr == nil
|
||||
errorMsg := ""
|
||||
if cmdErr != nil {
|
||||
errorMsg = cmdErr.Error()
|
||||
}
|
||||
e.callbacks.OnCommand(e.serverID, e.ExePath, args, true, success, errorMsg)
|
||||
|
||||
return cmdErr
|
||||
}
|
||||
|
||||
func (e *CallbackInteractiveCommandExecutor) monitorOutputWithCallbacks(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) {
|
||||
defer func() {
|
||||
select {
|
||||
case done <- nil:
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
stdoutScanner := bufio.NewScanner(stdout)
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
|
||||
outputChan := make(chan outputLine, 100)
|
||||
readersDone := make(chan struct{}, 2)
|
||||
|
||||
steamConsoleStarted := false
|
||||
tfaRequestCreated := false
|
||||
|
||||
go func() {
|
||||
defer func() { readersDone <- struct{}{} }()
|
||||
for stdoutScanner.Scan() {
|
||||
line := stdoutScanner.Text()
|
||||
if e.LogOutput {
|
||||
logging.Info("STDOUT: %s", line)
|
||||
}
|
||||
e.callbacks.OnOutput(e.serverID, line, false)
|
||||
|
||||
select {
|
||||
case outputChan <- outputLine{text: line, isError: false}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := stdoutScanner.Err(); err != nil {
|
||||
logging.Warn("Stdout scanner error: %v", err)
|
||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Stdout scanner error: %v", err), true)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer func() { readersDone <- struct{}{} }()
|
||||
for stderrScanner.Scan() {
|
||||
line := stderrScanner.Text()
|
||||
if e.LogOutput {
|
||||
logging.Info("STDERR: %s", line)
|
||||
}
|
||||
e.callbacks.OnOutput(e.serverID, line, true)
|
||||
|
||||
select {
|
||||
case outputChan <- outputLine{text: line, isError: true}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := stderrScanner.Err(); err != nil {
|
||||
logging.Warn("Stderr scanner error: %v", err)
|
||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Stderr scanner error: %v", err), true)
|
||||
}
|
||||
}()
|
||||
|
||||
readersFinished := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
done <- ctx.Err()
|
||||
return
|
||||
case <-readersDone:
|
||||
readersFinished++
|
||||
if readersFinished == 2 {
|
||||
close(outputChan)
|
||||
for lineData := range outputChan {
|
||||
if e.is2FAPrompt(lineData.text) {
|
||||
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
|
||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
case lineData, ok := <-outputChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
lowerLine := strings.ToLower(lineData.text)
|
||||
if strings.Contains(lowerLine, "steam console client") && strings.Contains(lowerLine, "valve corporation") {
|
||||
steamConsoleStarted = true
|
||||
logging.Info("Steam Console Client startup detected - will monitor for 2FA hang")
|
||||
e.callbacks.OnOutput(e.serverID, "Steam Console Client startup detected", false)
|
||||
}
|
||||
|
||||
if e.is2FAPrompt(lineData.text) {
|
||||
if !tfaRequestCreated {
|
||||
e.callbacks.OnOutput(e.serverID, "2FA prompt detected - waiting for user confirmation", false)
|
||||
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
|
||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
tfaRequestCreated = true
|
||||
}
|
||||
}
|
||||
|
||||
if tfaRequestCreated && e.isSteamContinuing(lineData.text) {
|
||||
logging.Info("Steam CMD appears to have continued after 2FA confirmation")
|
||||
e.callbacks.OnOutput(e.serverID, "Steam CMD continued after 2FA confirmation", false)
|
||||
e.autoCompletePendingRequests(serverID)
|
||||
}
|
||||
case <-time.After(15 * time.Second):
|
||||
if steamConsoleStarted && !tfaRequestCreated {
|
||||
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
|
||||
e.callbacks.OnOutput(e.serverID, "Waiting for Steam Guard 2FA confirmation...", false)
|
||||
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
||||
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
tfaRequestCreated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type outputLine struct {
|
||||
text string
|
||||
isError bool
|
||||
}
|
||||
19
local/utl/command/callbacks.go
Normal file
19
local/utl/command/callbacks.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package command
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type OutputCallback func(serverID uuid.UUID, output string, isError bool)
|
||||
|
||||
type CommandCallback func(serverID uuid.UUID, command string, args []string, completed bool, success bool, error string)
|
||||
|
||||
type CallbackConfig struct {
|
||||
OnOutput OutputCallback
|
||||
OnCommand CommandCallback
|
||||
}
|
||||
|
||||
func DefaultCallbackConfig() *CallbackConfig {
|
||||
return &CallbackConfig{
|
||||
OnOutput: func(uuid.UUID, string, bool) {},
|
||||
OnCommand: func(uuid.UUID, string, []string, bool, bool, string) {},
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CommandExecutor provides a base structure for executing commands
|
||||
type CommandExecutor struct {
|
||||
// Base executable path
|
||||
ExePath string
|
||||
// Working directory for commands
|
||||
WorkDir string
|
||||
// Whether to capture and log output
|
||||
ExePath string
|
||||
WorkDir string
|
||||
LogOutput bool
|
||||
}
|
||||
|
||||
// CommandBuilder helps build command arguments
|
||||
type CommandBuilder struct {
|
||||
args []string
|
||||
}
|
||||
@@ -48,10 +43,9 @@ func (b *CommandBuilder) Build() []string {
|
||||
return b.args
|
||||
}
|
||||
|
||||
// Execute runs a command with the given arguments
|
||||
func (e *CommandExecutor) Execute(args ...string) error {
|
||||
cmd := exec.Command(e.ExePath, args...)
|
||||
|
||||
|
||||
if e.WorkDir != "" {
|
||||
cmd.Dir = e.WorkDir
|
||||
}
|
||||
@@ -65,15 +59,13 @@ func (e *CommandExecutor) Execute(args ...string) error {
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// ExecuteWithBuilder runs a command using a CommandBuilder
|
||||
func (e *CommandExecutor) ExecuteWithBuilder(builder *CommandBuilder) error {
|
||||
return e.Execute(builder.Build()...)
|
||||
}
|
||||
|
||||
// ExecuteWithOutput runs a command and returns its output
|
||||
func (e *CommandExecutor) ExecuteWithOutput(args ...string) (string, error) {
|
||||
cmd := exec.Command(e.ExePath, args...)
|
||||
|
||||
|
||||
if e.WorkDir != "" {
|
||||
cmd.Dir = e.WorkDir
|
||||
}
|
||||
@@ -83,10 +75,9 @@ func (e *CommandExecutor) ExecuteWithOutput(args ...string) (string, error) {
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
// ExecuteWithEnv runs a command with custom environment variables
|
||||
func (e *CommandExecutor) ExecuteWithEnv(env []string, args ...string) error {
|
||||
cmd := exec.Command(e.ExePath, args...)
|
||||
|
||||
|
||||
if e.WorkDir != "" {
|
||||
cmd.Dir = e.WorkDir
|
||||
}
|
||||
@@ -100,4 +91,4 @@ func (e *CommandExecutor) ExecuteWithEnv(env []string, args ...string) error {
|
||||
|
||||
logging.Info("Executing command: %s %s", e.ExePath, strings.Join(args, " "))
|
||||
return cmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,12 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// InteractiveCommandExecutor extends CommandExecutor to handle interactive commands
|
||||
type InteractiveCommandExecutor struct {
|
||||
*CommandExecutor
|
||||
tfaManager *model.Steam2FAManager
|
||||
@@ -29,7 +27,6 @@ func NewInteractiveCommandExecutor(baseExecutor *CommandExecutor, tfaManager *mo
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteInteractive runs a command that may require 2FA input
|
||||
func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, serverID *uuid.UUID, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, e.ExePath, args...)
|
||||
|
||||
@@ -37,7 +34,6 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
|
||||
cmd.Dir = e.WorkDir
|
||||
}
|
||||
|
||||
// Create pipes for stdin, stdout, and stderr
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdin pipe: %v", err)
|
||||
@@ -58,7 +54,6 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
|
||||
|
||||
logging.Info("Executing interactive command: %s %s", e.ExePath, strings.Join(args, " "))
|
||||
|
||||
// Enable debug mode if environment variable is set
|
||||
debugMode := os.Getenv("STEAMCMD_DEBUG") == "true"
|
||||
if debugMode {
|
||||
logging.Info("STEAMCMD_DEBUG mode enabled - will log all output and create proactive 2FA requests")
|
||||
@@ -68,19 +63,15 @@ func (e *InteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, ser
|
||||
return fmt.Errorf("failed to start command: %v", err)
|
||||
}
|
||||
|
||||
// Create channels for output monitoring
|
||||
outputDone := make(chan error, 1)
|
||||
cmdDone := make(chan error, 1)
|
||||
|
||||
// Monitor stdout and stderr for 2FA prompts
|
||||
go e.monitorOutput(ctx, stdout, stderr, serverID, outputDone)
|
||||
|
||||
// Wait for the command to finish in a separate goroutine
|
||||
go func() {
|
||||
cmdDone <- cmd.Wait()
|
||||
}()
|
||||
|
||||
// Wait for both command and output monitoring to complete
|
||||
var cmdErr, outputErr error
|
||||
completedCount := 0
|
||||
|
||||
@@ -112,18 +103,15 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
}
|
||||
}()
|
||||
|
||||
// Create scanners for both outputs
|
||||
stdoutScanner := bufio.NewScanner(stdout)
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
|
||||
outputChan := make(chan string, 100) // Buffered channel to prevent blocking
|
||||
outputChan := make(chan string, 100)
|
||||
readersDone := make(chan struct{}, 2)
|
||||
|
||||
// Track Steam Console startup for this specific execution
|
||||
steamConsoleStarted := false
|
||||
tfaRequestCreated := false
|
||||
|
||||
// Read from stdout
|
||||
go func() {
|
||||
defer func() { readersDone <- struct{}{} }()
|
||||
for stdoutScanner.Scan() {
|
||||
@@ -131,7 +119,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
if e.LogOutput {
|
||||
logging.Info("STDOUT: %s", line)
|
||||
}
|
||||
// Always log Steam CMD output for debugging 2FA issues
|
||||
if strings.Contains(strings.ToLower(line), "steam") {
|
||||
logging.Info("STEAM_DEBUG: %s", line)
|
||||
}
|
||||
@@ -146,7 +133,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from stderr
|
||||
go func() {
|
||||
defer func() { readersDone <- struct{}{} }()
|
||||
for stderrScanner.Scan() {
|
||||
@@ -154,7 +140,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
if e.LogOutput {
|
||||
logging.Info("STDERR: %s", line)
|
||||
}
|
||||
// Always log Steam CMD errors for debugging 2FA issues
|
||||
if strings.Contains(strings.ToLower(line), "steam") {
|
||||
logging.Info("STEAM_DEBUG_ERR: %s", line)
|
||||
}
|
||||
@@ -169,7 +154,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
}
|
||||
}()
|
||||
|
||||
// Monitor for completion and 2FA prompts
|
||||
readersFinished := 0
|
||||
for {
|
||||
select {
|
||||
@@ -179,9 +163,7 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
case <-readersDone:
|
||||
readersFinished++
|
||||
if readersFinished == 2 {
|
||||
// Both readers are done, close output channel and finish monitoring
|
||||
close(outputChan)
|
||||
// Drain any remaining output
|
||||
for line := range outputChan {
|
||||
if e.is2FAPrompt(line) {
|
||||
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
|
||||
@@ -195,18 +177,15 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
}
|
||||
case line, ok := <-outputChan:
|
||||
if !ok {
|
||||
// Channel closed, we're done
|
||||
return
|
||||
}
|
||||
|
||||
// Check for Steam Console startup
|
||||
lowerLine := strings.ToLower(line)
|
||||
if strings.Contains(lowerLine, "steam console client") && strings.Contains(lowerLine, "valve corporation") {
|
||||
steamConsoleStarted = true
|
||||
logging.Info("Steam Console Client startup detected - will monitor for 2FA hang")
|
||||
}
|
||||
|
||||
// Check if this line indicates a 2FA prompt
|
||||
if e.is2FAPrompt(line) {
|
||||
if !tfaRequestCreated {
|
||||
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
|
||||
@@ -218,15 +197,11 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Steam CMD continued after 2FA (auto-completion)
|
||||
if tfaRequestCreated && e.isSteamContinuing(line) {
|
||||
logging.Info("Steam CMD appears to have continued after 2FA confirmation - auto-completing 2FA request")
|
||||
// Auto-complete any pending 2FA requests for this server
|
||||
e.autoCompletePendingRequests(serverID)
|
||||
}
|
||||
case <-time.After(15 * time.Second):
|
||||
// If Steam Console has started and we haven't seen output for 15 seconds,
|
||||
// it's very likely waiting for 2FA confirmation
|
||||
if steamConsoleStarted && !tfaRequestCreated {
|
||||
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
|
||||
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
||||
@@ -243,7 +218,6 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
||||
}
|
||||
|
||||
func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
|
||||
// Common SteamCMD 2FA prompts - updated with more comprehensive patterns
|
||||
twoFAKeywords := []string{
|
||||
"please enter your steam guard code",
|
||||
"steam guard",
|
||||
@@ -272,7 +246,6 @@ func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for patterns that might indicate Steam is waiting for input
|
||||
waitingPatterns := []string{
|
||||
"waiting for",
|
||||
"please enter",
|
||||
@@ -330,12 +303,9 @@ func (e *InteractiveCommandExecutor) autoCompletePendingRequests(serverID *uuid.
|
||||
func (e *InteractiveCommandExecutor) handle2FAPrompt(_ context.Context, promptLine string, serverID *uuid.UUID) error {
|
||||
logging.Info("2FA prompt detected: %s", promptLine)
|
||||
|
||||
// Create a 2FA request
|
||||
request := e.tfaManager.CreateRequest(promptLine, serverID)
|
||||
logging.Info("Created 2FA request with ID: %s", request.ID)
|
||||
|
||||
// Wait for user to complete the 2FA process
|
||||
// Use a reasonable timeout (e.g., 5 minutes)
|
||||
timeout := 5 * time.Minute
|
||||
success, err := e.tfaManager.WaitForCompletion(request.ID, timeout)
|
||||
|
||||
@@ -352,271 +322,3 @@ func (e *InteractiveCommandExecutor) handle2FAPrompt(_ context.Context, promptLi
|
||||
logging.Info("2FA completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// WebSocketInteractiveCommandExecutor extends InteractiveCommandExecutor to stream output via WebSocket
|
||||
type WebSocketInteractiveCommandExecutor struct {
|
||||
*InteractiveCommandExecutor
|
||||
wsService interface{} // Using interface{} to avoid circular import
|
||||
serverID uuid.UUID
|
||||
}
|
||||
|
||||
// NewInteractiveCommandExecutorWithWebSocket creates a new WebSocket-enabled interactive command executor
|
||||
func NewInteractiveCommandExecutorWithWebSocket(baseExecutor *CommandExecutor, tfaManager *model.Steam2FAManager, wsService interface{}, serverID uuid.UUID) *WebSocketInteractiveCommandExecutor {
|
||||
return &WebSocketInteractiveCommandExecutor{
|
||||
InteractiveCommandExecutor: &InteractiveCommandExecutor{
|
||||
CommandExecutor: baseExecutor,
|
||||
tfaManager: tfaManager,
|
||||
},
|
||||
wsService: wsService,
|
||||
serverID: serverID,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteInteractive runs a command with WebSocket output streaming
|
||||
func (e *WebSocketInteractiveCommandExecutor) ExecuteInteractive(ctx context.Context, serverID *uuid.UUID, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, e.ExePath, args...)
|
||||
|
||||
if e.WorkDir != "" {
|
||||
cmd.Dir = e.WorkDir
|
||||
}
|
||||
|
||||
// Create pipes for stdin, stdout, and stderr
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdin pipe: %v", err)
|
||||
}
|
||||
defer stdin.Close()
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stdout pipe: %v", err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create stderr pipe: %v", err)
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
logging.Info("Executing interactive command with WebSocket streaming: %s %s", e.ExePath, strings.Join(args, " "))
|
||||
|
||||
// Broadcast command start via WebSocket
|
||||
e.broadcastSteamOutput(fmt.Sprintf("Starting command: %s %s", e.ExePath, strings.Join(args, " ")), false)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
e.broadcastSteamOutput(fmt.Sprintf("Failed to start command: %v", err), true)
|
||||
return fmt.Errorf("failed to start command: %v", err)
|
||||
}
|
||||
|
||||
// Create channels for output monitoring
|
||||
outputDone := make(chan error, 1)
|
||||
cmdDone := make(chan error, 1)
|
||||
|
||||
// Monitor stdout and stderr for 2FA prompts with WebSocket streaming
|
||||
go e.monitorOutputWithWebSocket(ctx, stdout, stderr, serverID, outputDone)
|
||||
|
||||
// Wait for the command to finish in a separate goroutine
|
||||
go func() {
|
||||
cmdDone <- cmd.Wait()
|
||||
}()
|
||||
|
||||
// Wait for both command and output monitoring to complete
|
||||
var cmdErr, outputErr error
|
||||
completedCount := 0
|
||||
|
||||
for completedCount < 2 {
|
||||
select {
|
||||
case cmdErr = <-cmdDone:
|
||||
completedCount++
|
||||
logging.Info("Command execution completed")
|
||||
e.broadcastSteamOutput("Command execution completed", false)
|
||||
case outputErr = <-outputDone:
|
||||
completedCount++
|
||||
logging.Info("Output monitoring completed")
|
||||
case <-ctx.Done():
|
||||
e.broadcastSteamOutput("Command execution cancelled", true)
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
if outputErr != nil {
|
||||
logging.Warn("Output monitoring error: %v", outputErr)
|
||||
e.broadcastSteamOutput(fmt.Sprintf("Output monitoring error: %v", outputErr), true)
|
||||
}
|
||||
|
||||
return cmdErr
|
||||
}
|
||||
|
||||
// broadcastSteamOutput sends output to WebSocket using reflection to avoid circular imports
|
||||
func (e *WebSocketInteractiveCommandExecutor) broadcastSteamOutput(output string, isError bool) {
|
||||
if e.wsService == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Use reflection to call BroadcastSteamOutput method
|
||||
wsServiceVal := reflect.ValueOf(e.wsService)
|
||||
method := wsServiceVal.MethodByName("BroadcastSteamOutput")
|
||||
if !method.IsValid() {
|
||||
logging.Warn("BroadcastSteamOutput method not found on WebSocket service")
|
||||
return
|
||||
}
|
||||
|
||||
// Call the method with parameters: serverID, output, isError
|
||||
args := []reflect.Value{
|
||||
reflect.ValueOf(e.serverID),
|
||||
reflect.ValueOf(output),
|
||||
reflect.ValueOf(isError),
|
||||
}
|
||||
method.Call(args)
|
||||
}
|
||||
|
||||
// monitorOutputWithWebSocket monitors command output and streams it via WebSocket
|
||||
func (e *WebSocketInteractiveCommandExecutor) monitorOutputWithWebSocket(ctx context.Context, stdout, stderr io.Reader, serverID *uuid.UUID, done chan error) {
|
||||
defer func() {
|
||||
select {
|
||||
case done <- nil:
|
||||
default:
|
||||
}
|
||||
}()
|
||||
|
||||
// Create scanners for both outputs
|
||||
stdoutScanner := bufio.NewScanner(stdout)
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
|
||||
outputChan := make(chan outputLine, 100) // Buffered channel to prevent blocking
|
||||
readersDone := make(chan struct{}, 2)
|
||||
|
||||
// Track Steam Console startup for this specific execution
|
||||
steamConsoleStarted := false
|
||||
tfaRequestCreated := false
|
||||
|
||||
// Read from stdout
|
||||
go func() {
|
||||
defer func() { readersDone <- struct{}{} }()
|
||||
for stdoutScanner.Scan() {
|
||||
line := stdoutScanner.Text()
|
||||
if e.LogOutput {
|
||||
logging.Info("STDOUT: %s", line)
|
||||
}
|
||||
// Stream output via WebSocket
|
||||
e.broadcastSteamOutput(line, false)
|
||||
|
||||
select {
|
||||
case outputChan <- outputLine{text: line, isError: false}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := stdoutScanner.Err(); err != nil {
|
||||
logging.Warn("Stdout scanner error: %v", err)
|
||||
e.broadcastSteamOutput(fmt.Sprintf("Stdout scanner error: %v", err), true)
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from stderr
|
||||
go func() {
|
||||
defer func() { readersDone <- struct{}{} }()
|
||||
for stderrScanner.Scan() {
|
||||
line := stderrScanner.Text()
|
||||
if e.LogOutput {
|
||||
logging.Info("STDERR: %s", line)
|
||||
}
|
||||
// Stream error output via WebSocket
|
||||
e.broadcastSteamOutput(line, true)
|
||||
|
||||
select {
|
||||
case outputChan <- outputLine{text: line, isError: true}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := stderrScanner.Err(); err != nil {
|
||||
logging.Warn("Stderr scanner error: %v", err)
|
||||
e.broadcastSteamOutput(fmt.Sprintf("Stderr scanner error: %v", err), true)
|
||||
}
|
||||
}()
|
||||
|
||||
// Monitor for completion and 2FA prompts
|
||||
readersFinished := 0
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
done <- ctx.Err()
|
||||
return
|
||||
case <-readersDone:
|
||||
readersFinished++
|
||||
if readersFinished == 2 {
|
||||
// Both readers are done, close output channel and finish monitoring
|
||||
close(outputChan)
|
||||
// Drain any remaining output
|
||||
for lineData := range outputChan {
|
||||
if e.is2FAPrompt(lineData.text) {
|
||||
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
|
||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||
e.broadcastSteamOutput(fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
case lineData, ok := <-outputChan:
|
||||
if !ok {
|
||||
// Channel closed, we're done
|
||||
return
|
||||
}
|
||||
|
||||
// Check for Steam Console startup
|
||||
lowerLine := strings.ToLower(lineData.text)
|
||||
if strings.Contains(lowerLine, "steam console client") && strings.Contains(lowerLine, "valve corporation") {
|
||||
steamConsoleStarted = true
|
||||
logging.Info("Steam Console Client startup detected - will monitor for 2FA hang")
|
||||
e.broadcastSteamOutput("Steam Console Client startup detected", false)
|
||||
}
|
||||
|
||||
// Check if this line indicates a 2FA prompt
|
||||
if e.is2FAPrompt(lineData.text) {
|
||||
if !tfaRequestCreated {
|
||||
e.broadcastSteamOutput("2FA prompt detected - waiting for user confirmation", false)
|
||||
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
|
||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||
e.broadcastSteamOutput(fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
tfaRequestCreated = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if Steam CMD continued after 2FA (auto-completion)
|
||||
if tfaRequestCreated && e.isSteamContinuing(lineData.text) {
|
||||
logging.Info("Steam CMD appears to have continued after 2FA confirmation")
|
||||
e.broadcastSteamOutput("Steam CMD continued after 2FA confirmation", false)
|
||||
// Auto-complete any pending 2FA requests for this server
|
||||
e.autoCompletePendingRequests(serverID)
|
||||
}
|
||||
case <-time.After(15 * time.Second):
|
||||
// If Steam Console has started and we haven't seen output for 15 seconds,
|
||||
// it's very likely waiting for 2FA confirmation
|
||||
if steamConsoleStarted && !tfaRequestCreated {
|
||||
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
|
||||
e.broadcastSteamOutput("Waiting for Steam Guard 2FA confirmation...", false)
|
||||
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
||||
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
||||
e.broadcastSteamOutput(fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
tfaRequestCreated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outputLine represents a line of output with error status
|
||||
type outputLine struct {
|
||||
text string
|
||||
isError bool
|
||||
}
|
||||
|
||||
@@ -79,12 +79,6 @@ func IndentJson(body []byte) ([]byte, error) {
|
||||
return unmarshaledBody.Bytes(), nil
|
||||
}
|
||||
|
||||
// ParseQueryFilter parses query parameters into a filter struct using reflection.
|
||||
// It supports various field types and uses struct tags to determine parsing behavior.
|
||||
// Supported tags:
|
||||
// - `query:"field_name"` - specifies the query parameter name
|
||||
// - `param:"param_name"` - specifies the path parameter name
|
||||
// - `time_format:"format"` - specifies the time format for parsing dates (default: RFC3339)
|
||||
func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
|
||||
val := reflect.ValueOf(filter)
|
||||
if val.Kind() != reflect.Ptr || val.IsNil() {
|
||||
@@ -94,14 +88,12 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
|
||||
elem := val.Elem()
|
||||
typ := elem.Type()
|
||||
|
||||
// Process all fields including embedded structs
|
||||
var processFields func(reflect.Value, reflect.Type) error
|
||||
processFields = func(val reflect.Value, typ reflect.Type) error {
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
// Handle embedded structs recursively
|
||||
if fieldType.Anonymous {
|
||||
if err := processFields(field, fieldType.Type); err != nil {
|
||||
return err
|
||||
@@ -109,12 +101,10 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if field cannot be set
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for param tag first (path parameters)
|
||||
if paramName := fieldType.Tag.Get("param"); paramName != "" {
|
||||
if err := parsePathParam(c, field, paramName); err != nil {
|
||||
return fmt.Errorf("error parsing path parameter %s: %v", paramName, err)
|
||||
@@ -122,15 +112,14 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// Then check for query tag
|
||||
queryName := fieldType.Tag.Get("query")
|
||||
if queryName == "" {
|
||||
queryName = ToSnakeCase(fieldType.Name) // Default to snake_case of field name
|
||||
queryName = ToSnakeCase(fieldType.Name)
|
||||
}
|
||||
|
||||
queryVal := c.Query(queryName)
|
||||
if queryVal == "" {
|
||||
continue // Skip empty values
|
||||
continue
|
||||
}
|
||||
|
||||
if err := parseValue(field, queryVal, fieldType.Tag); err != nil {
|
||||
|
||||
@@ -18,7 +18,6 @@ var (
|
||||
|
||||
func Init() {
|
||||
godotenv.Load()
|
||||
// Fail fast if critical environment variables are missing
|
||||
Secret = getEnvRequired("APP_SECRET")
|
||||
SecretCode = getEnvRequired("APP_SECRET_CODE")
|
||||
EncryptionKey = getEnvRequired("ENCRYPTION_KEY")
|
||||
@@ -29,7 +28,6 @@ func Init() {
|
||||
}
|
||||
}
|
||||
|
||||
// getEnv retrieves an environment variable or returns a fallback value.
|
||||
func getEnv(key, fallback string) string {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
return value
|
||||
@@ -38,12 +36,10 @@ func getEnv(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// getEnvRequired retrieves an environment variable and fails if it's not set.
|
||||
// This should be used for critical configuration that must not have defaults.
|
||||
func getEnvRequired(key string) string {
|
||||
if value, exists := os.LookupEnv(key); exists && value != "" {
|
||||
return value
|
||||
}
|
||||
log.Fatalf("Required environment variable %s is not set or is empty", key)
|
||||
return "" // This line will never be reached due to log.Fatalf
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ func Start(di *dig.Container) {
|
||||
func Migrate(db *gorm.DB) {
|
||||
logging.Info("Migrating database")
|
||||
|
||||
// Run GORM AutoMigrate for all models
|
||||
err := db.AutoMigrate(
|
||||
&model.ServiceControlModel{},
|
||||
&model.Config{},
|
||||
@@ -52,7 +51,6 @@ func Migrate(db *gorm.DB) {
|
||||
|
||||
if err != nil {
|
||||
logging.Error("GORM AutoMigrate failed: %v", err)
|
||||
// Don't panic, just log the error as custom migrations may have handled this
|
||||
}
|
||||
|
||||
db.FirstOrCreate(&model.ServiceControlModel{ServiceControl: "Works"})
|
||||
@@ -63,10 +61,8 @@ func Migrate(db *gorm.DB) {
|
||||
func runMigrations(db *gorm.DB) {
|
||||
logging.Info("Running custom database migrations...")
|
||||
|
||||
// Migration 001: Password security upgrade
|
||||
if err := migrations.RunPasswordSecurityMigration(db); err != nil {
|
||||
logging.Error("Failed to run password security migration: %v", err)
|
||||
// Continue - this migration might not be needed for all setups
|
||||
}
|
||||
|
||||
logging.Info("Custom database migrations completed")
|
||||
@@ -132,7 +128,6 @@ func seedCarModels(db *gorm.DB) error {
|
||||
carModels := []model.CarModel{
|
||||
{Value: 0, CarModel: "Porsche 991 GT3 R"},
|
||||
{Value: 1, CarModel: "Mercedes-AMG GT3"},
|
||||
// ... Add all car models from your list
|
||||
}
|
||||
|
||||
for _, cm := range carModels {
|
||||
|
||||
7
local/utl/env/env.go
vendored
7
local/utl/env/env.go
vendored
@@ -6,12 +6,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// Default paths for when environment variables are not set
|
||||
DefaultSteamCMDPath = "c:\\steamcmd\\steamcmd.exe"
|
||||
DefaultNSSMPath = ".\\nssm.exe"
|
||||
)
|
||||
|
||||
// GetSteamCMDPath returns the SteamCMD executable path from environment variable or default
|
||||
func GetSteamCMDPath() string {
|
||||
if path := os.Getenv("STEAMCMD_PATH"); path != "" {
|
||||
return path
|
||||
@@ -19,13 +17,11 @@ func GetSteamCMDPath() string {
|
||||
return DefaultSteamCMDPath
|
||||
}
|
||||
|
||||
// GetSteamCMDDirPath returns the directory containing SteamCMD executable
|
||||
func GetSteamCMDDirPath() string {
|
||||
steamCMDPath := GetSteamCMDPath()
|
||||
return filepath.Dir(steamCMDPath)
|
||||
}
|
||||
|
||||
// GetNSSMPath returns the NSSM executable path from environment variable or default
|
||||
func GetNSSMPath() string {
|
||||
if path := os.Getenv("NSSM_PATH"); path != "" {
|
||||
return path
|
||||
@@ -33,17 +29,14 @@ func GetNSSMPath() string {
|
||||
return DefaultNSSMPath
|
||||
}
|
||||
|
||||
// ValidatePaths checks if the configured paths exist (optional validation)
|
||||
func ValidatePaths() map[string]error {
|
||||
errors := make(map[string]error)
|
||||
|
||||
// Check SteamCMD path
|
||||
steamCMDPath := GetSteamCMDPath()
|
||||
if _, err := os.Stat(steamCMDPath); os.IsNotExist(err) {
|
||||
errors["STEAMCMD_PATH"] = err
|
||||
}
|
||||
|
||||
// Check NSSM path
|
||||
nssmPath := GetNSSMPath()
|
||||
if _, err := os.Stat(nssmPath); os.IsNotExist(err) {
|
||||
errors["NSSM_PATH"] = err
|
||||
|
||||
@@ -9,45 +9,37 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// ControllerErrorHandler provides centralized error handling for controllers
|
||||
type ControllerErrorHandler struct {
|
||||
errorLogger *logging.ErrorLogger
|
||||
}
|
||||
|
||||
// NewControllerErrorHandler creates a new controller error handler instance
|
||||
func NewControllerErrorHandler() *ControllerErrorHandler {
|
||||
return &ControllerErrorHandler{
|
||||
errorLogger: logging.GetErrorLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorResponse represents a standardized error response
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code int `json:"code,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// HandleError handles controller errors with logging and standardized responses
|
||||
func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCode int, context ...string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get caller information for logging
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
file = strings.TrimPrefix(file, "acc-server-manager/")
|
||||
|
||||
// Build context string
|
||||
contextStr := ""
|
||||
if len(context) > 0 {
|
||||
contextStr = fmt.Sprintf("[%s] ", strings.Join(context, "|"))
|
||||
}
|
||||
|
||||
// Clean error message (remove null bytes)
|
||||
cleanErrorMsg := strings.ReplaceAll(err.Error(), "\x00", "")
|
||||
|
||||
// Log the error with context
|
||||
ceh.errorLogger.LogWithContext(
|
||||
fmt.Sprintf("CONTROLLER_ERROR [%s:%d]", file, line),
|
||||
"%s%s",
|
||||
@@ -55,31 +47,26 @@ func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCo
|
||||
cleanErrorMsg,
|
||||
)
|
||||
|
||||
// Create standardized error response
|
||||
errorResponse := ErrorResponse{
|
||||
Error: cleanErrorMsg,
|
||||
Code: statusCode,
|
||||
}
|
||||
|
||||
// Add request details if available
|
||||
if c != nil {
|
||||
if errorResponse.Details == nil {
|
||||
errorResponse.Details = make(map[string]string)
|
||||
}
|
||||
|
||||
// Safely extract request details
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// If any of these panic, just skip adding the details
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
errorResponse.Details["method"] = c.Method()
|
||||
errorResponse.Details["path"] = c.Path()
|
||||
|
||||
// Safely get IP address
|
||||
|
||||
if ip := c.IP(); ip != "" {
|
||||
errorResponse.Details["ip"] = ip
|
||||
} else {
|
||||
@@ -88,14 +75,11 @@ func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCo
|
||||
}()
|
||||
}
|
||||
|
||||
// Return appropriate response based on status code
|
||||
if c == nil {
|
||||
// If context is nil, we can't return a response
|
||||
return fmt.Errorf("cannot return HTTP response: context is nil")
|
||||
}
|
||||
|
||||
|
||||
if statusCode >= 500 {
|
||||
// For server errors, don't expose internal details
|
||||
return c.Status(statusCode).JSON(ErrorResponse{
|
||||
Error: "Internal server error",
|
||||
Code: statusCode,
|
||||
@@ -105,52 +89,42 @@ func (ceh *ControllerErrorHandler) HandleError(c *fiber.Ctx, err error, statusCo
|
||||
return c.Status(statusCode).JSON(errorResponse)
|
||||
}
|
||||
|
||||
// HandleValidationError handles validation errors specifically
|
||||
func (ceh *ControllerErrorHandler) HandleValidationError(c *fiber.Ctx, err error, field string) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusBadRequest, "VALIDATION", field)
|
||||
}
|
||||
|
||||
// HandleDatabaseError handles database-related errors
|
||||
func (ceh *ControllerErrorHandler) HandleDatabaseError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusInternalServerError, "DATABASE")
|
||||
}
|
||||
|
||||
// HandleAuthError handles authentication/authorization errors
|
||||
func (ceh *ControllerErrorHandler) HandleAuthError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusUnauthorized, "AUTH")
|
||||
}
|
||||
|
||||
// HandleNotFoundError handles resource not found errors
|
||||
func (ceh *ControllerErrorHandler) HandleNotFoundError(c *fiber.Ctx, resource string) error {
|
||||
err := fmt.Errorf("%s not found", resource)
|
||||
return ceh.HandleError(c, err, fiber.StatusNotFound, "NOT_FOUND")
|
||||
}
|
||||
|
||||
// HandleBusinessLogicError handles business logic errors
|
||||
func (ceh *ControllerErrorHandler) HandleBusinessLogicError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusBadRequest, "BUSINESS_LOGIC")
|
||||
}
|
||||
|
||||
// HandleServiceError handles service layer errors
|
||||
func (ceh *ControllerErrorHandler) HandleServiceError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusInternalServerError, "SERVICE")
|
||||
}
|
||||
|
||||
// HandleParsingError handles request parsing errors
|
||||
func (ceh *ControllerErrorHandler) HandleParsingError(c *fiber.Ctx, err error) error {
|
||||
return ceh.HandleError(c, err, fiber.StatusBadRequest, "PARSING")
|
||||
}
|
||||
|
||||
// HandleUUIDError handles UUID parsing errors
|
||||
func (ceh *ControllerErrorHandler) HandleUUIDError(c *fiber.Ctx, field string) error {
|
||||
err := fmt.Errorf("invalid %s format", field)
|
||||
return ceh.HandleError(c, err, fiber.StatusBadRequest, "UUID_VALIDATION", field)
|
||||
}
|
||||
|
||||
// Global controller error handler instance
|
||||
var globalErrorHandler *ControllerErrorHandler
|
||||
|
||||
// GetControllerErrorHandler returns the global controller error handler instance
|
||||
func GetControllerErrorHandler() *ControllerErrorHandler {
|
||||
if globalErrorHandler == nil {
|
||||
globalErrorHandler = NewControllerErrorHandler()
|
||||
@@ -158,49 +132,38 @@ func GetControllerErrorHandler() *ControllerErrorHandler {
|
||||
return globalErrorHandler
|
||||
}
|
||||
|
||||
// Convenience functions using the global error handler
|
||||
|
||||
// HandleError handles controller errors using the global error handler
|
||||
func HandleError(c *fiber.Ctx, err error, statusCode int, context ...string) error {
|
||||
return GetControllerErrorHandler().HandleError(c, err, statusCode, context...)
|
||||
}
|
||||
|
||||
// HandleValidationError handles validation errors using the global error handler
|
||||
func HandleValidationError(c *fiber.Ctx, err error, field string) error {
|
||||
return GetControllerErrorHandler().HandleValidationError(c, err, field)
|
||||
}
|
||||
|
||||
// HandleDatabaseError handles database errors using the global error handler
|
||||
func HandleDatabaseError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleDatabaseError(c, err)
|
||||
}
|
||||
|
||||
// HandleAuthError handles auth errors using the global error handler
|
||||
func HandleAuthError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleAuthError(c, err)
|
||||
}
|
||||
|
||||
// HandleNotFoundError handles not found errors using the global error handler
|
||||
func HandleNotFoundError(c *fiber.Ctx, resource string) error {
|
||||
return GetControllerErrorHandler().HandleNotFoundError(c, resource)
|
||||
}
|
||||
|
||||
// HandleBusinessLogicError handles business logic errors using the global error handler
|
||||
func HandleBusinessLogicError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleBusinessLogicError(c, err)
|
||||
}
|
||||
|
||||
// HandleServiceError handles service errors using the global error handler
|
||||
func HandleServiceError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleServiceError(c, err)
|
||||
}
|
||||
|
||||
// HandleParsingError handles parsing errors using the global error handler
|
||||
func HandleParsingError(c *fiber.Ctx, err error) error {
|
||||
return GetControllerErrorHandler().HandleParsingError(c, err)
|
||||
}
|
||||
|
||||
// HandleUUIDError handles UUID errors using the global error handler
|
||||
func HandleUUIDError(c *fiber.Ctx, field string) error {
|
||||
return GetControllerErrorHandler().HandleUUIDError(c, field)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// Claims represents the JWT claims.
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
IsOpenToken bool `json:"is_open_token"`
|
||||
@@ -27,7 +26,6 @@ type OpenJWTHandler struct {
|
||||
*JWTHandler
|
||||
}
|
||||
|
||||
// NewJWTHandler creates a new JWTHandler instance with the provided secret key.
|
||||
func NewOpenJWTHandler(jwtSecret string) *OpenJWTHandler {
|
||||
jwtHandler := NewJWTHandler(jwtSecret)
|
||||
jwtHandler.IsOpenToken = true
|
||||
@@ -36,7 +34,6 @@ func NewOpenJWTHandler(jwtSecret string) *OpenJWTHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// NewJWTHandler creates a new JWTHandler instance with the provided secret key.
|
||||
func NewJWTHandler(jwtSecret string) *JWTHandler {
|
||||
if jwtSecret == "" {
|
||||
errors.SafeFatal("JWT_SECRET environment variable is required and cannot be empty")
|
||||
@@ -44,14 +41,12 @@ func NewJWTHandler(jwtSecret string) *JWTHandler {
|
||||
|
||||
var secretKey []byte
|
||||
|
||||
// Decode base64 secret if it looks like base64, otherwise use as-is
|
||||
if decoded, err := base64.StdEncoding.DecodeString(jwtSecret); err == nil && len(decoded) >= 32 {
|
||||
secretKey = decoded
|
||||
} else {
|
||||
secretKey = []byte(jwtSecret)
|
||||
}
|
||||
|
||||
// Ensure minimum key length for security
|
||||
if len(secretKey) < 32 {
|
||||
errors.SafeFatal("JWT_SECRET must be at least 32 bytes long for security")
|
||||
}
|
||||
@@ -60,8 +55,6 @@ func NewJWTHandler(jwtSecret string) *JWTHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateSecretKey generates a cryptographically secure random key for JWT signing
|
||||
// This is a utility function for generating new secrets, not used in normal operation
|
||||
func (jh *JWTHandler) GenerateSecretKey() string {
|
||||
key := make([]byte, 64) // 512 bits
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
@@ -70,7 +63,6 @@ func (jh *JWTHandler) GenerateSecretKey() string {
|
||||
return base64.StdEncoding.EncodeToString(key)
|
||||
}
|
||||
|
||||
// GenerateToken generates a new JWT for a given user.
|
||||
func (jh *JWTHandler) GenerateToken(userId string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
claims := &Claims{
|
||||
@@ -99,7 +91,6 @@ func (jh *JWTHandler) GenerateTokenWithExpiry(user *model.User, expiry time.Time
|
||||
return token.SignedString(jh.SecretKey)
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT and returns the claims if the token is valid.
|
||||
func (jh *JWTHandler) ValidateToken(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ var (
|
||||
timeFormat = "2006-01-02 15:04:05.000"
|
||||
)
|
||||
|
||||
// BaseLogger provides the core logging functionality
|
||||
type BaseLogger struct {
|
||||
file *os.File
|
||||
logger *log.Logger
|
||||
@@ -23,7 +22,6 @@ type BaseLogger struct {
|
||||
initialized bool
|
||||
}
|
||||
|
||||
// LogLevel represents different logging levels
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
@@ -34,28 +32,23 @@ const (
|
||||
LogLevelPanic LogLevel = "PANIC"
|
||||
)
|
||||
|
||||
// Initialize creates a new base logger instance
|
||||
func InitializeBase(tp string) (*BaseLogger, error) {
|
||||
return newBaseLogger(tp)
|
||||
}
|
||||
|
||||
func newBaseLogger(tp string) (*BaseLogger, error) {
|
||||
// Ensure logs directory exists
|
||||
if err := os.MkdirAll("logs", 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %v", err)
|
||||
}
|
||||
|
||||
// Open log file with date in name
|
||||
logPath := filepath.Join("logs", fmt.Sprintf("acc-server-%s-%s.log", time.Now().Format("2006-01-02"), tp))
|
||||
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %v", err)
|
||||
}
|
||||
|
||||
// Create multi-writer for both file and console
|
||||
multiWriter := io.MultiWriter(file, os.Stdout)
|
||||
|
||||
// Create base logger
|
||||
logger := &BaseLogger{
|
||||
file: file,
|
||||
logger: log.New(multiWriter, "", 0),
|
||||
@@ -65,13 +58,11 @@ func newBaseLogger(tp string) (*BaseLogger, error) {
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// GetBaseLogger creates and returns a new base logger instance
|
||||
func GetBaseLogger(tp string) *BaseLogger {
|
||||
baseLogger, _ := InitializeBase(tp)
|
||||
return baseLogger
|
||||
}
|
||||
|
||||
// Close closes the log file
|
||||
func (bl *BaseLogger) Close() error {
|
||||
bl.mu.Lock()
|
||||
defer bl.mu.Unlock()
|
||||
@@ -82,7 +73,6 @@ func (bl *BaseLogger) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log writes a log entry with the specified level
|
||||
func (bl *BaseLogger) Log(level LogLevel, format string, v ...interface{}) {
|
||||
if bl == nil || !bl.initialized {
|
||||
return
|
||||
@@ -91,14 +81,11 @@ func (bl *BaseLogger) Log(level LogLevel, format string, v ...interface{}) {
|
||||
bl.mu.RLock()
|
||||
defer bl.mu.RUnlock()
|
||||
|
||||
// Get caller info (skip 2 frames: this function and the calling Log function)
|
||||
_, file, line, _ := runtime.Caller(2)
|
||||
file = filepath.Base(file)
|
||||
|
||||
// Format message
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
|
||||
// Format final log line
|
||||
logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s",
|
||||
time.Now().Format(timeFormat),
|
||||
string(level),
|
||||
@@ -110,7 +97,6 @@ func (bl *BaseLogger) Log(level LogLevel, format string, v ...interface{}) {
|
||||
bl.logger.Println(logLine)
|
||||
}
|
||||
|
||||
// LogWithCaller writes a log entry with custom caller depth
|
||||
func (bl *BaseLogger) LogWithCaller(level LogLevel, callerDepth int, format string, v ...interface{}) {
|
||||
if bl == nil || !bl.initialized {
|
||||
return
|
||||
@@ -119,14 +105,11 @@ func (bl *BaseLogger) LogWithCaller(level LogLevel, callerDepth int, format stri
|
||||
bl.mu.RLock()
|
||||
defer bl.mu.RUnlock()
|
||||
|
||||
// Get caller info with custom depth
|
||||
_, file, line, _ := runtime.Caller(callerDepth)
|
||||
file = filepath.Base(file)
|
||||
|
||||
// Format message
|
||||
msg := fmt.Sprintf(format, v...)
|
||||
|
||||
// Format final log line
|
||||
logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s",
|
||||
time.Now().Format(timeFormat),
|
||||
string(level),
|
||||
@@ -138,7 +121,6 @@ func (bl *BaseLogger) LogWithCaller(level LogLevel, callerDepth int, format stri
|
||||
bl.logger.Println(logLine)
|
||||
}
|
||||
|
||||
// IsInitialized returns whether the base logger is initialized
|
||||
func (bl *BaseLogger) IsInitialized() bool {
|
||||
if bl == nil {
|
||||
return false
|
||||
@@ -148,19 +130,16 @@ func (bl *BaseLogger) IsInitialized() bool {
|
||||
return bl.initialized
|
||||
}
|
||||
|
||||
// RecoverAndLog recovers from panics and logs them
|
||||
func RecoverAndLog() {
|
||||
baseLogger := GetBaseLogger("panic")
|
||||
if baseLogger != nil && baseLogger.IsInitialized() {
|
||||
if r := recover(); r != nil {
|
||||
// Get stack trace
|
||||
buf := make([]byte, 4096)
|
||||
n := runtime.Stack(buf, false)
|
||||
stackTrace := string(buf[:n])
|
||||
|
||||
baseLogger.LogWithCaller(LogLevelPanic, 2, "Recovered from panic: %v\nStack Trace:\n%s", r, stackTrace)
|
||||
|
||||
// Re-panic to maintain original behavior if needed
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DebugLogger handles debug-level logging
|
||||
type DebugLogger struct {
|
||||
base *BaseLogger
|
||||
}
|
||||
|
||||
// NewDebugLogger creates a new debug logger instance
|
||||
func NewDebugLogger() *DebugLogger {
|
||||
base, _ := InitializeBase("debug")
|
||||
return &DebugLogger{
|
||||
@@ -19,14 +17,12 @@ func NewDebugLogger() *DebugLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes a debug-level log entry
|
||||
func (dl *DebugLogger) Log(format string, v ...interface{}) {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithContext writes a debug-level log entry with additional context
|
||||
func (dl *DebugLogger) LogWithContext(context string, format string, v ...interface{}) {
|
||||
if dl.base != nil {
|
||||
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
|
||||
@@ -34,7 +30,6 @@ func (dl *DebugLogger) LogWithContext(context string, format string, v ...interf
|
||||
}
|
||||
}
|
||||
|
||||
// LogFunction logs function entry and exit for debugging
|
||||
func (dl *DebugLogger) LogFunction(functionName string, args ...interface{}) {
|
||||
if dl.base != nil {
|
||||
if len(args) > 0 {
|
||||
@@ -45,21 +40,18 @@ func (dl *DebugLogger) LogFunction(functionName string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// LogVariable logs variable values for debugging
|
||||
func (dl *DebugLogger) LogVariable(varName string, value interface{}) {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, "VARIABLE [%s]: %+v", varName, value)
|
||||
}
|
||||
}
|
||||
|
||||
// LogState logs application state information
|
||||
func (dl *DebugLogger) LogState(component string, state interface{}) {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, "STATE [%s]: %+v", component, state)
|
||||
}
|
||||
}
|
||||
|
||||
// LogSQL logs SQL queries for debugging
|
||||
func (dl *DebugLogger) LogSQL(query string, args ...interface{}) {
|
||||
if dl.base != nil {
|
||||
if len(args) > 0 {
|
||||
@@ -70,7 +62,6 @@ func (dl *DebugLogger) LogSQL(query string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// LogMemory logs memory usage information
|
||||
func (dl *DebugLogger) LogMemory() {
|
||||
if dl.base != nil {
|
||||
var m runtime.MemStats
|
||||
@@ -80,32 +71,27 @@ func (dl *DebugLogger) LogMemory() {
|
||||
}
|
||||
}
|
||||
|
||||
// LogGoroutines logs current number of goroutines
|
||||
func (dl *DebugLogger) LogGoroutines() {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, "GOROUTINES: %d active", runtime.NumGoroutine())
|
||||
}
|
||||
}
|
||||
|
||||
// LogTiming logs timing information for performance debugging
|
||||
func (dl *DebugLogger) LogTiming(operation string, duration interface{}) {
|
||||
if dl.base != nil {
|
||||
dl.base.Log(LogLevelDebug, "TIMING [%s]: %v", operation, duration)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert bytes to kilobytes
|
||||
func bToKb(b uint64) uint64 {
|
||||
return b / 1024
|
||||
}
|
||||
|
||||
// Global debug logger instance
|
||||
var (
|
||||
debugLogger *DebugLogger
|
||||
debugOnce sync.Once
|
||||
)
|
||||
|
||||
// GetDebugLogger returns the global debug logger instance
|
||||
func GetDebugLogger() *DebugLogger {
|
||||
debugOnce.Do(func() {
|
||||
debugLogger = NewDebugLogger()
|
||||
@@ -113,47 +99,38 @@ func GetDebugLogger() *DebugLogger {
|
||||
return debugLogger
|
||||
}
|
||||
|
||||
// Debug logs a debug-level message using the global debug logger
|
||||
func Debug(format string, v ...interface{}) {
|
||||
GetDebugLogger().Log(format, v...)
|
||||
}
|
||||
|
||||
// DebugWithContext logs a debug-level message with context using the global debug logger
|
||||
func DebugWithContext(context string, format string, v ...interface{}) {
|
||||
GetDebugLogger().LogWithContext(context, format, v...)
|
||||
}
|
||||
|
||||
// DebugFunction logs function entry and exit using the global debug logger
|
||||
func DebugFunction(functionName string, args ...interface{}) {
|
||||
GetDebugLogger().LogFunction(functionName, args...)
|
||||
}
|
||||
|
||||
// DebugVariable logs variable values using the global debug logger
|
||||
func DebugVariable(varName string, value interface{}) {
|
||||
GetDebugLogger().LogVariable(varName, value)
|
||||
}
|
||||
|
||||
// DebugState logs application state information using the global debug logger
|
||||
func DebugState(component string, state interface{}) {
|
||||
GetDebugLogger().LogState(component, state)
|
||||
}
|
||||
|
||||
// DebugSQL logs SQL queries using the global debug logger
|
||||
func DebugSQL(query string, args ...interface{}) {
|
||||
GetDebugLogger().LogSQL(query, args...)
|
||||
}
|
||||
|
||||
// DebugMemory logs memory usage information using the global debug logger
|
||||
func DebugMemory() {
|
||||
GetDebugLogger().LogMemory()
|
||||
}
|
||||
|
||||
// DebugGoroutines logs current number of goroutines using the global debug logger
|
||||
func DebugGoroutines() {
|
||||
GetDebugLogger().LogGoroutines()
|
||||
}
|
||||
|
||||
// DebugTiming logs timing information using the global debug logger
|
||||
func DebugTiming(operation string, duration interface{}) {
|
||||
GetDebugLogger().LogTiming(operation, duration)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ErrorLogger handles error-level logging
|
||||
type ErrorLogger struct {
|
||||
base *BaseLogger
|
||||
}
|
||||
|
||||
// NewErrorLogger creates a new error logger instance
|
||||
func NewErrorLogger() *ErrorLogger {
|
||||
base, _ := InitializeBase("error")
|
||||
return &ErrorLogger{
|
||||
@@ -19,14 +17,12 @@ func NewErrorLogger() *ErrorLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes an error-level log entry
|
||||
func (el *ErrorLogger) Log(format string, v ...interface{}) {
|
||||
if el.base != nil {
|
||||
el.base.Log(LogLevelError, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithContext writes an error-level log entry with additional context
|
||||
func (el *ErrorLogger) LogWithContext(context string, format string, v ...interface{}) {
|
||||
if el.base != nil {
|
||||
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
|
||||
@@ -34,7 +30,6 @@ func (el *ErrorLogger) LogWithContext(context string, format string, v ...interf
|
||||
}
|
||||
}
|
||||
|
||||
// LogError logs an error object with optional message
|
||||
func (el *ErrorLogger) LogError(err error, message ...string) {
|
||||
if el.base != nil && err != nil {
|
||||
if len(message) > 0 {
|
||||
@@ -45,7 +40,6 @@ func (el *ErrorLogger) LogError(err error, message ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithStackTrace logs an error with stack trace
|
||||
func (el *ErrorLogger) LogWithStackTrace(format string, v ...interface{}) {
|
||||
if el.base != nil {
|
||||
// Get stack trace
|
||||
@@ -58,7 +52,6 @@ func (el *ErrorLogger) LogWithStackTrace(format string, v ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// LogFatal logs a fatal error and exits the program
|
||||
func (el *ErrorLogger) LogFatal(format string, v ...interface{}) {
|
||||
if el.base != nil {
|
||||
el.base.Log(LogLevelError, "[FATAL] "+format, v...)
|
||||
@@ -66,13 +59,11 @@ func (el *ErrorLogger) LogFatal(format string, v ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Global error logger instance
|
||||
var (
|
||||
errorLogger *ErrorLogger
|
||||
errorOnce sync.Once
|
||||
)
|
||||
|
||||
// GetErrorLogger returns the global error logger instance
|
||||
func GetErrorLogger() *ErrorLogger {
|
||||
errorOnce.Do(func() {
|
||||
errorLogger = NewErrorLogger()
|
||||
@@ -80,27 +71,22 @@ func GetErrorLogger() *ErrorLogger {
|
||||
return errorLogger
|
||||
}
|
||||
|
||||
// Error logs an error-level message using the global error logger
|
||||
func Error(format string, v ...interface{}) {
|
||||
GetErrorLogger().Log(format, v...)
|
||||
}
|
||||
|
||||
// ErrorWithContext logs an error-level message with context using the global error logger
|
||||
func ErrorWithContext(context string, format string, v ...interface{}) {
|
||||
GetErrorLogger().LogWithContext(context, format, v...)
|
||||
}
|
||||
|
||||
// LogError logs an error object using the global error logger
|
||||
func LogError(err error, message ...string) {
|
||||
GetErrorLogger().LogError(err, message...)
|
||||
}
|
||||
|
||||
// ErrorWithStackTrace logs an error with stack trace using the global error logger
|
||||
func ErrorWithStackTrace(format string, v ...interface{}) {
|
||||
GetErrorLogger().LogWithStackTrace(format, v...)
|
||||
}
|
||||
|
||||
// Fatal logs a fatal error and exits the program using the global error logger
|
||||
func Fatal(format string, v ...interface{}) {
|
||||
GetErrorLogger().LogFatal(format, v...)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,10 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// InfoLogger handles info-level logging
|
||||
type InfoLogger struct {
|
||||
base *BaseLogger
|
||||
}
|
||||
|
||||
// NewInfoLogger creates a new info logger instance
|
||||
func NewInfoLogger() *InfoLogger {
|
||||
base, _ := InitializeBase("info")
|
||||
return &InfoLogger{
|
||||
@@ -18,14 +16,12 @@ func NewInfoLogger() *InfoLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes an info-level log entry
|
||||
func (il *InfoLogger) Log(format string, v ...interface{}) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithContext writes an info-level log entry with additional context
|
||||
func (il *InfoLogger) LogWithContext(context string, format string, v ...interface{}) {
|
||||
if il.base != nil {
|
||||
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
|
||||
@@ -33,55 +29,47 @@ func (il *InfoLogger) LogWithContext(context string, format string, v ...interfa
|
||||
}
|
||||
}
|
||||
|
||||
// LogStartup logs application startup information
|
||||
func (il *InfoLogger) LogStartup(component string, message string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "STARTUP [%s]: %s", component, message)
|
||||
}
|
||||
}
|
||||
|
||||
// LogShutdown logs application shutdown information
|
||||
func (il *InfoLogger) LogShutdown(component string, message string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "SHUTDOWN [%s]: %s", component, message)
|
||||
}
|
||||
}
|
||||
|
||||
// LogOperation logs general operation information
|
||||
func (il *InfoLogger) LogOperation(operation string, details string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "OPERATION [%s]: %s", operation, details)
|
||||
}
|
||||
}
|
||||
|
||||
// LogStatus logs status changes or updates
|
||||
func (il *InfoLogger) LogStatus(component string, status string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "STATUS [%s]: %s", component, status)
|
||||
}
|
||||
}
|
||||
|
||||
// LogRequest logs incoming requests
|
||||
func (il *InfoLogger) LogRequest(method string, path string, userAgent string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "REQUEST [%s %s] User-Agent: %s", method, path, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
// LogResponse logs outgoing responses
|
||||
func (il *InfoLogger) LogResponse(method string, path string, statusCode int, duration string) {
|
||||
if il.base != nil {
|
||||
il.base.Log(LogLevelInfo, "RESPONSE [%s %s] Status: %d, Duration: %s", method, path, statusCode, duration)
|
||||
}
|
||||
}
|
||||
|
||||
// Global info logger instance
|
||||
var (
|
||||
infoLogger *InfoLogger
|
||||
infoOnce sync.Once
|
||||
)
|
||||
|
||||
// GetInfoLogger returns the global info logger instance
|
||||
func GetInfoLogger() *InfoLogger {
|
||||
infoOnce.Do(func() {
|
||||
infoLogger = NewInfoLogger()
|
||||
@@ -89,42 +77,34 @@ func GetInfoLogger() *InfoLogger {
|
||||
return infoLogger
|
||||
}
|
||||
|
||||
// Info logs an info-level message using the global info logger
|
||||
func Info(format string, v ...interface{}) {
|
||||
GetInfoLogger().Log(format, v...)
|
||||
}
|
||||
|
||||
// InfoWithContext logs an info-level message with context using the global info logger
|
||||
func InfoWithContext(context string, format string, v ...interface{}) {
|
||||
GetInfoLogger().LogWithContext(context, format, v...)
|
||||
}
|
||||
|
||||
// InfoStartup logs application startup information using the global info logger
|
||||
func InfoStartup(component string, message string) {
|
||||
GetInfoLogger().LogStartup(component, message)
|
||||
}
|
||||
|
||||
// InfoShutdown logs application shutdown information using the global info logger
|
||||
func InfoShutdown(component string, message string) {
|
||||
GetInfoLogger().LogShutdown(component, message)
|
||||
}
|
||||
|
||||
// InfoOperation logs general operation information using the global info logger
|
||||
func InfoOperation(operation string, details string) {
|
||||
GetInfoLogger().LogOperation(operation, details)
|
||||
}
|
||||
|
||||
// InfoStatus logs status changes or updates using the global info logger
|
||||
func InfoStatus(component string, status string) {
|
||||
GetInfoLogger().LogStatus(component, status)
|
||||
}
|
||||
|
||||
// InfoRequest logs incoming requests using the global info logger
|
||||
func InfoRequest(method string, path string, userAgent string) {
|
||||
GetInfoLogger().LogRequest(method, path, userAgent)
|
||||
}
|
||||
|
||||
// InfoResponse logs outgoing responses using the global info logger
|
||||
func InfoResponse(method string, path string, statusCode int, duration string) {
|
||||
GetInfoLogger().LogResponse(method, path, statusCode, duration)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// Legacy logger for backward compatibility
|
||||
logger *Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Logger maintains backward compatibility with existing code
|
||||
type Logger struct {
|
||||
base *BaseLogger
|
||||
errorLogger *ErrorLogger
|
||||
@@ -20,8 +18,6 @@ type Logger struct {
|
||||
debugLogger *DebugLogger
|
||||
}
|
||||
|
||||
// Initialize creates or gets the singleton logger instance
|
||||
// This maintains backward compatibility with existing code
|
||||
func Initialize() (*Logger, error) {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
@@ -31,13 +27,11 @@ func Initialize() (*Logger, error) {
|
||||
}
|
||||
|
||||
func newLogger() (*Logger, error) {
|
||||
// Initialize the base logger
|
||||
baseLogger, err := InitializeBase("log")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the legacy logger wrapper
|
||||
logger := &Logger{
|
||||
base: baseLogger,
|
||||
errorLogger: GetErrorLogger(),
|
||||
@@ -49,7 +43,6 @@ func newLogger() (*Logger, error) {
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// Close closes the logger
|
||||
func (l *Logger) Close() error {
|
||||
if l.base != nil {
|
||||
return l.base.Close()
|
||||
@@ -57,7 +50,6 @@ func (l *Logger) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Legacy methods for backward compatibility
|
||||
func (l *Logger) log(level, format string, v ...interface{}) {
|
||||
if l.base != nil {
|
||||
l.base.LogWithCaller(LogLevel(level), 3, format, v...)
|
||||
@@ -94,13 +86,10 @@ func (l *Logger) Panic(format string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Global convenience functions for backward compatibility
|
||||
// These are now implemented in individual logger files to avoid redeclaration
|
||||
func LegacyInfo(format string, v ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Info(format, v...)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetInfoLogger().Log(format, v...)
|
||||
}
|
||||
}
|
||||
@@ -109,7 +98,6 @@ func LegacyError(format string, v ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Error(format, v...)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetErrorLogger().Log(format, v...)
|
||||
}
|
||||
}
|
||||
@@ -118,7 +106,6 @@ func LegacyWarn(format string, v ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Warn(format, v...)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetWarnLogger().Log(format, v...)
|
||||
}
|
||||
}
|
||||
@@ -127,7 +114,6 @@ func LegacyDebug(format string, v ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.Debug(format, v...)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetDebugLogger().Log(format, v...)
|
||||
}
|
||||
}
|
||||
@@ -136,55 +122,42 @@ func Panic(format string) {
|
||||
if logger != nil {
|
||||
logger.Panic(format)
|
||||
} else {
|
||||
// Fallback to direct logger if legacy logger not initialized
|
||||
GetErrorLogger().LogFatal(format)
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced logging convenience functions
|
||||
// These provide direct access to specialized logging functions
|
||||
|
||||
// LogStartup logs application startup information
|
||||
func LogStartup(component string, message string) {
|
||||
GetInfoLogger().LogStartup(component, message)
|
||||
}
|
||||
|
||||
// LogShutdown logs application shutdown information
|
||||
func LogShutdown(component string, message string) {
|
||||
GetInfoLogger().LogShutdown(component, message)
|
||||
}
|
||||
|
||||
// LogOperation logs general operation information
|
||||
func LogOperation(operation string, details string) {
|
||||
GetInfoLogger().LogOperation(operation, details)
|
||||
}
|
||||
|
||||
// LogRequest logs incoming HTTP requests
|
||||
func LogRequest(method string, path string, userAgent string) {
|
||||
GetInfoLogger().LogRequest(method, path, userAgent)
|
||||
}
|
||||
|
||||
// LogResponse logs outgoing HTTP responses
|
||||
func LogResponse(method string, path string, statusCode int, duration string) {
|
||||
GetInfoLogger().LogResponse(method, path, statusCode, duration)
|
||||
}
|
||||
|
||||
// LogSQL logs SQL queries for debugging
|
||||
func LogSQL(query string, args ...interface{}) {
|
||||
GetDebugLogger().LogSQL(query, args...)
|
||||
}
|
||||
|
||||
// LogMemory logs memory usage information
|
||||
func LogMemory() {
|
||||
GetDebugLogger().LogMemory()
|
||||
}
|
||||
|
||||
// LogTiming logs timing information for performance debugging
|
||||
func LogTiming(operation string, duration interface{}) {
|
||||
GetDebugLogger().LogTiming(operation, duration)
|
||||
}
|
||||
|
||||
// GetLegacyLogger returns the legacy logger instance for backward compatibility
|
||||
func GetLegacyLogger() *Logger {
|
||||
if logger == nil {
|
||||
logger, _ = Initialize()
|
||||
@@ -192,21 +165,17 @@ func GetLegacyLogger() *Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
// InitializeLogging initializes all logging components
|
||||
func InitializeLogging() error {
|
||||
// Initialize legacy logger for backward compatibility
|
||||
_, err := Initialize()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize legacy logger: %v", err)
|
||||
}
|
||||
|
||||
// Pre-initialize all logger types to ensure separate log files
|
||||
GetErrorLogger()
|
||||
GetWarnLogger()
|
||||
GetInfoLogger()
|
||||
GetDebugLogger()
|
||||
|
||||
// Log successful initialization
|
||||
Info("Logging system initialized successfully")
|
||||
|
||||
return nil
|
||||
|
||||
@@ -5,12 +5,10 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// WarnLogger handles warn-level logging
|
||||
type WarnLogger struct {
|
||||
base *BaseLogger
|
||||
}
|
||||
|
||||
// NewWarnLogger creates a new warn logger instance
|
||||
func NewWarnLogger() *WarnLogger {
|
||||
base, _ := InitializeBase("warn")
|
||||
return &WarnLogger{
|
||||
@@ -18,14 +16,12 @@ func NewWarnLogger() *WarnLogger {
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes a warn-level log entry
|
||||
func (wl *WarnLogger) Log(format string, v ...interface{}) {
|
||||
if wl.base != nil {
|
||||
wl.base.Log(LogLevelWarn, format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// LogWithContext writes a warn-level log entry with additional context
|
||||
func (wl *WarnLogger) LogWithContext(context string, format string, v ...interface{}) {
|
||||
if wl.base != nil {
|
||||
contextualFormat := fmt.Sprintf("[%s] %s", context, format)
|
||||
@@ -33,7 +29,6 @@ func (wl *WarnLogger) LogWithContext(context string, format string, v ...interfa
|
||||
}
|
||||
}
|
||||
|
||||
// LogDeprecation logs a deprecation warning
|
||||
func (wl *WarnLogger) LogDeprecation(feature string, alternative string) {
|
||||
if wl.base != nil {
|
||||
if alternative != "" {
|
||||
@@ -44,27 +39,23 @@ func (wl *WarnLogger) LogDeprecation(feature string, alternative string) {
|
||||
}
|
||||
}
|
||||
|
||||
// LogConfiguration logs configuration-related warnings
|
||||
func (wl *WarnLogger) LogConfiguration(setting string, message string) {
|
||||
if wl.base != nil {
|
||||
wl.base.Log(LogLevelWarn, "CONFIG WARNING [%s]: %s", setting, message)
|
||||
}
|
||||
}
|
||||
|
||||
// LogPerformance logs performance-related warnings
|
||||
func (wl *WarnLogger) LogPerformance(operation string, threshold string, actual string) {
|
||||
if wl.base != nil {
|
||||
wl.base.Log(LogLevelWarn, "PERFORMANCE WARNING [%s]: exceeded threshold %s, actual: %s", operation, threshold, actual)
|
||||
}
|
||||
}
|
||||
|
||||
// Global warn logger instance
|
||||
var (
|
||||
warnLogger *WarnLogger
|
||||
warnOnce sync.Once
|
||||
)
|
||||
|
||||
// GetWarnLogger returns the global warn logger instance
|
||||
func GetWarnLogger() *WarnLogger {
|
||||
warnOnce.Do(func() {
|
||||
warnLogger = NewWarnLogger()
|
||||
@@ -72,27 +63,22 @@ func GetWarnLogger() *WarnLogger {
|
||||
return warnLogger
|
||||
}
|
||||
|
||||
// Warn logs a warn-level message using the global warn logger
|
||||
func Warn(format string, v ...interface{}) {
|
||||
GetWarnLogger().Log(format, v...)
|
||||
}
|
||||
|
||||
// WarnWithContext logs a warn-level message with context using the global warn logger
|
||||
func WarnWithContext(context string, format string, v ...interface{}) {
|
||||
GetWarnLogger().LogWithContext(context, format, v...)
|
||||
}
|
||||
|
||||
// WarnDeprecation logs a deprecation warning using the global warn logger
|
||||
func WarnDeprecation(feature string, alternative string) {
|
||||
GetWarnLogger().LogDeprecation(feature, alternative)
|
||||
}
|
||||
|
||||
// WarnConfiguration logs configuration-related warnings using the global warn logger
|
||||
func WarnConfiguration(setting string, message string) {
|
||||
GetWarnLogger().LogConfiguration(setting, message)
|
||||
}
|
||||
|
||||
// WarnPerformance logs performance-related warnings using the global warn logger
|
||||
func WarnPerformance(operation string, threshold string, actual string) {
|
||||
GetWarnLogger().LogPerformance(operation, threshold, actual)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// IsPortAvailable checks if a port is available for both TCP and UDP
|
||||
func IsPortAvailable(port int) bool {
|
||||
return IsTCPPortAvailable(port) && IsUDPPortAvailable(port)
|
||||
}
|
||||
|
||||
// IsTCPPortAvailable checks if a TCP port is available
|
||||
func IsTCPPortAvailable(port int) bool {
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
listener, err := net.Listen("tcp", addr)
|
||||
@@ -22,7 +20,6 @@ func IsTCPPortAvailable(port int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsUDPPortAvailable checks if a UDP port is available
|
||||
func IsUDPPortAvailable(port int) bool {
|
||||
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port})
|
||||
if err != nil {
|
||||
@@ -32,7 +29,6 @@ func IsUDPPortAvailable(port int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// FindAvailablePort finds an available port starting from the given port
|
||||
func FindAvailablePort(startPort int) (int, error) {
|
||||
maxPort := 65535
|
||||
for port := startPort; port <= maxPort; port++ {
|
||||
@@ -43,14 +39,12 @@ func FindAvailablePort(startPort int) (int, error) {
|
||||
return 0, fmt.Errorf("no available ports found between %d and %d", startPort, maxPort)
|
||||
}
|
||||
|
||||
// FindAvailablePortRange finds a range of consecutive available ports
|
||||
func FindAvailablePortRange(startPort, count int) ([]int, error) {
|
||||
maxPort := 65535
|
||||
ports := make([]int, 0, count)
|
||||
currentPort := startPort
|
||||
|
||||
for len(ports) < count && currentPort <= maxPort {
|
||||
// Check if we have enough consecutive ports available
|
||||
available := true
|
||||
for i := 0; i < count-len(ports); i++ {
|
||||
if !IsPortAvailable(currentPort + i) {
|
||||
@@ -74,7 +68,6 @@ func FindAvailablePortRange(startPort, count int) ([]int, error) {
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
// WaitForPortAvailable waits for a port to become available with timeout
|
||||
func WaitForPortAvailable(port int, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
@@ -84,4 +77,4 @@ func WaitForPortAvailable(port int, timeout time.Duration) error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("timeout waiting for port %d to become available", port)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// MinPasswordLength defines the minimum password length
|
||||
MinPasswordLength = 8
|
||||
// BcryptCost defines the cost factor for bcrypt hashing
|
||||
BcryptCost = 12
|
||||
BcryptCost = 12
|
||||
)
|
||||
|
||||
// HashPassword hashes a plain text password using bcrypt
|
||||
func HashPassword(password string) (string, error) {
|
||||
if len(password) < MinPasswordLength {
|
||||
return "", errors.New("password must be at least 8 characters long")
|
||||
@@ -28,12 +25,10 @@ func HashPassword(password string) (string, error) {
|
||||
return string(hashedBytes), nil
|
||||
}
|
||||
|
||||
// VerifyPassword verifies a plain text password against a hashed password
|
||||
func VerifyPassword(hashedPassword, password string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
}
|
||||
|
||||
// ValidatePasswordStrength validates password complexity requirements
|
||||
func ValidatePasswordStrength(password string) error {
|
||||
if len(password) < MinPasswordLength {
|
||||
return errors.New("password must be at least 8 characters long")
|
||||
|
||||
@@ -17,25 +17,23 @@ import (
|
||||
func Start(di *dig.Container) *fiber.App {
|
||||
app := fiber.New(fiber.Config{
|
||||
EnablePrintRoutes: true,
|
||||
ReadTimeout: 20 * time.Minute, // Increased for long-running Steam operations
|
||||
WriteTimeout: 20 * time.Minute, // Increased for long-running Steam operations
|
||||
IdleTimeout: 25 * time.Minute, // Increased accordingly
|
||||
BodyLimit: 10 * 1024 * 1024, // 10MB
|
||||
ReadTimeout: 20 * time.Minute,
|
||||
WriteTimeout: 20 * time.Minute,
|
||||
IdleTimeout: 25 * time.Minute,
|
||||
BodyLimit: 10 * 1024 * 1024,
|
||||
})
|
||||
|
||||
// Initialize security middleware
|
||||
securityMW := security.NewSecurityMiddleware()
|
||||
|
||||
// Add security middleware stack
|
||||
app.Use(securityMW.SecurityHeaders())
|
||||
app.Use(securityMW.LogSecurityEvents())
|
||||
app.Use(securityMW.TimeoutMiddleware(20 * time.Minute)) // Increased for Steam operations
|
||||
app.Use(securityMW.RequestContextTimeout(20 * time.Minute)) // Increased for Steam operations
|
||||
app.Use(securityMW.RequestSizeLimit(10 * 1024 * 1024)) // 10MB
|
||||
app.Use(securityMW.TimeoutMiddleware(20 * time.Minute))
|
||||
app.Use(securityMW.RequestContextTimeout(20 * time.Minute))
|
||||
app.Use(securityMW.RequestSizeLimit(10 * 1024 * 1024))
|
||||
app.Use(securityMW.ValidateUserAgent())
|
||||
app.Use(securityMW.ValidateContentType("application/json", "application/x-www-form-urlencoded", "multipart/form-data"))
|
||||
app.Use(securityMW.InputSanitization())
|
||||
app.Use(securityMW.RateLimit(100, 1*time.Minute)) // 100 requests per minute global
|
||||
app.Use(securityMW.RateLimit(100, 1*time.Minute))
|
||||
|
||||
app.Use(helmet.New())
|
||||
|
||||
@@ -62,7 +60,7 @@ func Start(di *dig.Container) *fiber.App {
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "3000" // Default port
|
||||
port = "3000"
|
||||
}
|
||||
|
||||
logging.Info("Starting server on port %s", port)
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
)
|
||||
|
||||
type LogTailer struct {
|
||||
filePath string
|
||||
handleLine func(string)
|
||||
stopChan chan struct{}
|
||||
isRunning bool
|
||||
tracker *PositionTracker
|
||||
filePath string
|
||||
handleLine func(string)
|
||||
stopChan chan struct{}
|
||||
isRunning bool
|
||||
tracker *PositionTracker
|
||||
}
|
||||
|
||||
func NewLogTailer(filePath string, handleLine func(string)) *LogTailer {
|
||||
@@ -30,10 +30,9 @@ func (t *LogTailer) Start() {
|
||||
t.isRunning = true
|
||||
|
||||
go func() {
|
||||
// Load last position from tracker
|
||||
pos, err := t.tracker.LoadPosition()
|
||||
if err != nil {
|
||||
pos = &LogPosition{} // Start from beginning if error
|
||||
pos = &LogPosition{}
|
||||
}
|
||||
lastSize := pos.LastPosition
|
||||
|
||||
@@ -43,7 +42,6 @@ func (t *LogTailer) Start() {
|
||||
t.isRunning = false
|
||||
return
|
||||
default:
|
||||
// Try to open and read the file
|
||||
if file, err := os.Open(t.filePath); err == nil {
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
@@ -52,12 +50,10 @@ func (t *LogTailer) Start() {
|
||||
continue
|
||||
}
|
||||
|
||||
// If file was truncated, start from beginning
|
||||
if stat.Size() < lastSize {
|
||||
lastSize = 0
|
||||
}
|
||||
|
||||
// Seek to last read position
|
||||
if lastSize > 0 {
|
||||
file.Seek(lastSize, 0)
|
||||
}
|
||||
@@ -66,9 +62,8 @@ func (t *LogTailer) Start() {
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
t.handleLine(line)
|
||||
lastSize, _ = file.Seek(0, 1) // Get current position
|
||||
|
||||
// Save position periodically
|
||||
lastSize, _ = file.Seek(0, 1)
|
||||
|
||||
t.tracker.SavePosition(&LogPosition{
|
||||
LastPosition: lastSize,
|
||||
LastRead: line,
|
||||
@@ -78,7 +73,6 @@ func (t *LogTailer) Start() {
|
||||
file.Close()
|
||||
}
|
||||
|
||||
// Wait before next attempt
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
@@ -90,4 +84,4 @@ func (t *LogTailer) Stop() {
|
||||
return
|
||||
}
|
||||
close(t.stopChan)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
type LogPosition struct {
|
||||
LastPosition int64 `json:"last_position"`
|
||||
LastRead string `json:"last_read"`
|
||||
LastPosition int64 `json:"last_position"`
|
||||
LastRead string `json:"last_read"`
|
||||
}
|
||||
|
||||
type PositionTracker struct {
|
||||
@@ -16,11 +16,10 @@ type PositionTracker struct {
|
||||
}
|
||||
|
||||
func NewPositionTracker(logPath string) *PositionTracker {
|
||||
// Create position file in same directory as log file
|
||||
dir := filepath.Dir(logPath)
|
||||
base := filepath.Base(logPath)
|
||||
positionFile := filepath.Join(dir, "."+base+".position")
|
||||
|
||||
|
||||
return &PositionTracker{
|
||||
positionFile: positionFile,
|
||||
}
|
||||
@@ -30,7 +29,6 @@ func (t *PositionTracker) LoadPosition() (*LogPosition, error) {
|
||||
data, err := os.ReadFile(t.positionFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Return empty position if file doesn't exist
|
||||
return &LogPosition{}, nil
|
||||
}
|
||||
return nil, err
|
||||
@@ -51,4 +49,4 @@ func (t *PositionTracker) SavePosition(pos *LogPosition) error {
|
||||
}
|
||||
|
||||
return os.WriteFile(t.positionFile, data, 0644)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func TailLogFile(path string, callback func(string)) {
|
||||
file, _ := os.Open(path)
|
||||
defer file.Close()
|
||||
|
||||
file.Seek(0, os.SEEK_END) // Start at end of file
|
||||
file.Seek(0, os.SEEK_END)
|
||||
reader := bufio.NewReader(file)
|
||||
|
||||
for {
|
||||
@@ -88,7 +88,7 @@ func TailLogFile(path string, callback func(string)) {
|
||||
if err == nil {
|
||||
callback(line)
|
||||
} else {
|
||||
time.Sleep(500 * time.Millisecond) // wait for new data
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
169
local/utl/websocket/websocket.go
Normal file
169
local/utl/websocket/websocket.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/utl/logging"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/websocket/v2"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type WebSocketConnection struct {
|
||||
conn *websocket.Conn
|
||||
serverID *uuid.UUID
|
||||
userID *uuid.UUID
|
||||
}
|
||||
|
||||
type WebSocketService struct {
|
||||
connections sync.Map
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewWebSocketService() *WebSocketService {
|
||||
return &WebSocketService{}
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) AddConnection(connID string, conn *websocket.Conn, userID *uuid.UUID) {
|
||||
wsConn := &WebSocketConnection{
|
||||
conn: conn,
|
||||
userID: userID,
|
||||
}
|
||||
ws.connections.Store(connID, wsConn)
|
||||
logging.Info("WebSocket connection added: %s for user: %v", connID, userID)
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) RemoveConnection(connID string) {
|
||||
if conn, exists := ws.connections.LoadAndDelete(connID); exists {
|
||||
if wsConn, ok := conn.(*WebSocketConnection); ok {
|
||||
wsConn.conn.Close()
|
||||
}
|
||||
}
|
||||
logging.Info("WebSocket connection removed: %s", connID)
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) SetServerID(connID string, serverID uuid.UUID) {
|
||||
if conn, exists := ws.connections.Load(connID); exists {
|
||||
if wsConn, ok := conn.(*WebSocketConnection); ok {
|
||||
wsConn.serverID = &serverID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) BroadcastStep(serverID uuid.UUID, step model.ServerCreationStep, status model.StepStatus, message string, errorMsg string) {
|
||||
stepMsg := model.StepMessage{
|
||||
Step: step,
|
||||
Status: status,
|
||||
Message: message,
|
||||
Error: errorMsg,
|
||||
}
|
||||
|
||||
wsMsg := model.WebSocketMessage{
|
||||
Type: model.MessageTypeStep,
|
||||
ServerID: &serverID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: stepMsg,
|
||||
}
|
||||
|
||||
ws.broadcastToServer(serverID, wsMsg)
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) BroadcastSteamOutput(serverID uuid.UUID, output string, isError bool) {
|
||||
steamMsg := model.SteamOutputMessage{
|
||||
Output: output,
|
||||
IsError: isError,
|
||||
}
|
||||
|
||||
wsMsg := model.WebSocketMessage{
|
||||
Type: model.MessageTypeSteamOutput,
|
||||
ServerID: &serverID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: steamMsg,
|
||||
}
|
||||
|
||||
ws.broadcastToServer(serverID, wsMsg)
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) BroadcastError(serverID uuid.UUID, error string, details string) {
|
||||
errorMsg := model.ErrorMessage{
|
||||
Error: error,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
wsMsg := model.WebSocketMessage{
|
||||
Type: model.MessageTypeError,
|
||||
ServerID: &serverID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: errorMsg,
|
||||
}
|
||||
|
||||
ws.broadcastToServer(serverID, wsMsg)
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) BroadcastComplete(serverID uuid.UUID, success bool, message string) {
|
||||
completeMsg := model.CompleteMessage{
|
||||
ServerID: serverID,
|
||||
Success: success,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
wsMsg := model.WebSocketMessage{
|
||||
Type: model.MessageTypeComplete,
|
||||
ServerID: &serverID,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: completeMsg,
|
||||
}
|
||||
|
||||
ws.broadcastToServer(serverID, wsMsg)
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.WebSocketMessage) {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
logging.Error("Failed to marshal WebSocket message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ws.connections.Range(func(key, value interface{}) bool {
|
||||
if wsConn, ok := value.(*WebSocketConnection); ok {
|
||||
if wsConn.serverID != nil && *wsConn.serverID == serverID {
|
||||
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
||||
ws.RemoveConnection(key.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebSocketMessage) {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
logging.Error("Failed to marshal WebSocket message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ws.connections.Range(func(key, value interface{}) bool {
|
||||
if wsConn, ok := value.(*WebSocketConnection); ok {
|
||||
if wsConn.userID != nil && *wsConn.userID == userID {
|
||||
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
||||
ws.RemoveConnection(key.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (ws *WebSocketService) GetActiveConnections() int {
|
||||
count := 0
|
||||
ws.connections.Range(func(key, value interface{}) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
Reference in New Issue
Block a user