alter primary keys to uuids and adjust the membership system

This commit is contained in:
Fran Jurmanović
2025-06-30 22:50:52 +02:00
parent caba5bae70
commit c17e7742ee
53 changed files with 12641 additions and 805 deletions

View File

@@ -28,6 +28,7 @@ func Init(di *dig.Container, app *fiber.App) {
Config: serverIdGroup.Group("/config"),
Lookup: groups.Group("/lookup"),
StateHistory: serverIdGroup.Group("/state-history"),
Membership: groups.Group("/membership"),
}
err := di.Provide(func() *common.RouteGroups {

View File

@@ -3,14 +3,15 @@ package controller
import (
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/logging"
"strings"
"acc-server-manager/local/utl/error_handler"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
type ApiController struct {
service *service.ApiService
service *service.ApiService
errorHandler *error_handler.ControllerErrorHandler
}
// NewApiController
@@ -23,7 +24,8 @@ type ApiController struct {
// *ApiController: Controller for "api" interactions
func NewApiController(as *service.ApiService, routeGroups *common.RouteGroups) *ApiController {
ac := &ApiController{
service: as,
service: as,
errorHandler: error_handler.NewControllerErrorHandler(),
}
routeGroups.Api.Get("/", ac.getFirst)
@@ -57,9 +59,9 @@ func (ac *ApiController) getFirst(c *fiber.Ctx) error {
func (ac *ApiController) getStatus(c *fiber.Ctx) error {
service := c.Params("service")
if service == "" {
serverId, err := c.ParamsInt("service")
if err != nil {
return c.Status(400).SendString(err.Error())
serverId := c.Params("service")
if _, err := uuid.Parse(serverId); err != nil {
return ac.errorHandler.HandleUUIDError(c, "server ID")
}
c.Locals("serverId", serverId)
} else {
@@ -67,7 +69,7 @@ func (ac *ApiController) getStatus(c *fiber.Ctx) error {
}
apiModel, err := ac.service.GetStatus(c)
if err != nil {
return c.Status(400).SendString(strings.ReplaceAll(err.Error(), "\x00", ""))
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(string(apiModel))
}
@@ -83,14 +85,13 @@ func (ac *ApiController) getStatus(c *fiber.Ctx) error {
func (ac *ApiController) startServer(c *fiber.Ctx) error {
model := new(Service)
if err := c.BodyParser(model); err != nil {
c.SendStatus(400)
return ac.errorHandler.HandleParsingError(c, err)
}
c.Locals("service", model.Name)
c.Locals("serverId", model.ServerId)
apiModel, err := ac.service.ApiStartServer(c)
if err != nil {
logging.Error(strings.ReplaceAll(err.Error(), "\x00", ""))
return c.Status(400).SendString(strings.ReplaceAll(err.Error(), "\x00", ""))
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
@@ -106,14 +107,13 @@ func (ac *ApiController) startServer(c *fiber.Ctx) error {
func (ac *ApiController) stopServer(c *fiber.Ctx) error {
model := new(Service)
if err := c.BodyParser(model); err != nil {
c.SendStatus(400)
return ac.errorHandler.HandleParsingError(c, err)
}
c.Locals("service", model.Name)
c.Locals("serverId", model.ServerId)
apiModel, err := ac.service.ApiStopServer(c)
if err != nil {
logging.Error(strings.ReplaceAll(err.Error(), "\x00", ""))
return c.Status(400).SendString(strings.ReplaceAll(err.Error(), "\x00", ""))
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
@@ -129,19 +129,18 @@ func (ac *ApiController) stopServer(c *fiber.Ctx) error {
func (ac *ApiController) restartServer(c *fiber.Ctx) error {
model := new(Service)
if err := c.BodyParser(model); err != nil {
c.SendStatus(400)
return ac.errorHandler.HandleParsingError(c, err)
}
c.Locals("service", model.Name)
c.Locals("serverId", model.ServerId)
apiModel, err := ac.service.ApiRestartServer(c)
if err != nil {
logging.Error(strings.ReplaceAll(err.Error(), "\x00", ""))
return c.Status(400).SendString(strings.ReplaceAll(err.Error(), "\x00", ""))
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendString(apiModel)
}
type Service struct {
Name string `json:"name" xml:"name" form:"name"`
ServerId int `json:"serverId" xml:"serverId" form:"serverId"`
ServerId string `json:"serverId" xml:"serverId" form:"serverId"`
}

View File

@@ -3,14 +3,17 @@ package controller
import (
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/error_handler"
"acc-server-manager/local/utl/logging"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
type ConfigController struct {
service *service.ConfigService
apiService *service.ApiService
service *service.ConfigService
apiService *service.ApiService
errorHandler *error_handler.ControllerErrorHandler
}
// NewConfigController
@@ -23,8 +26,9 @@ type ConfigController struct {
// *ConfigController: Controller for "Config" interactions
func NewConfigController(as *service.ConfigService, routeGroups *common.RouteGroups, as2 *service.ApiService) *ConfigController {
ac := &ConfigController{
service: as,
apiService: as2,
service: as,
apiService: as2,
errorHandler: error_handler.NewControllerErrorHandler(),
}
routeGroups.Config.Put("/:file", ac.UpdateConfig)
@@ -46,24 +50,29 @@ func NewConfigController(as *service.ConfigService, routeGroups *common.RouteGro
// @Router /v1/server/{id}/config/{file} [put]
func (ac *ConfigController) UpdateConfig(c *fiber.Ctx) error {
restart := c.QueryBool("restart")
serverID, _ := c.ParamsInt("id")
serverID := c.Params("id")
// Validate UUID format
if _, err := uuid.Parse(serverID); err != nil {
return ac.errorHandler.HandleUUIDError(c, "server ID")
}
c.Locals("serverId", serverID)
var config map[string]interface{}
if err := c.BodyParser(&config); err != nil {
logging.Error("Invalid config format")
return c.Status(400).JSON(fiber.Map{"error": "Invalid config format"})
return ac.errorHandler.HandleParsingError(c, err)
}
ConfigModel, err := ac.service.UpdateConfig(c, &config)
if err != nil {
return c.Status(400).SendString(err.Error())
return ac.errorHandler.HandleServiceError(c, err)
}
logging.Info("restart: %v", restart)
if restart {
_, err := ac.apiService.ApiRestartServer(c)
if err != nil {
logging.Error(err.Error())
logging.ErrorWithContext("CONFIG_RESTART", "Failed to restart server after config update: %v", err)
}
}
@@ -82,8 +91,7 @@ func (ac *ConfigController) UpdateConfig(c *fiber.Ctx) error {
func (ac *ConfigController) GetConfig(c *fiber.Ctx) error {
Model, err := ac.service.GetConfig(c)
if err != nil {
logging.Error(err.Error())
return c.Status(400).SendString(err.Error())
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(Model)
}
@@ -99,8 +107,7 @@ func (ac *ConfigController) GetConfig(c *fiber.Ctx) error {
func (ac *ConfigController) GetConfigs(c *fiber.Ctx) error {
Model, err := ac.service.GetConfigs(c)
if err != nil {
logging.Error(err.Error())
return c.Status(400).SendString(err.Error())
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(Model)
}

View File

@@ -3,12 +3,14 @@ package controller
import (
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/error_handler"
"github.com/gofiber/fiber/v2"
)
type LookupController struct {
service *service.LookupService
service *service.LookupService
errorHandler *error_handler.ControllerErrorHandler
}
// NewLookupController
@@ -21,7 +23,8 @@ type LookupController struct {
// *LookupController: Controller for "Lookup" interactions
func NewLookupController(as *service.LookupService, routeGroups *common.RouteGroups) *LookupController {
ac := &LookupController{
service: as,
service: as,
errorHandler: error_handler.NewControllerErrorHandler(),
}
routeGroups.Lookup.Get("/tracks", ac.GetTracks)
routeGroups.Lookup.Get("/car-models", ac.GetCarModels)
@@ -42,9 +45,7 @@ func NewLookupController(as *service.LookupService, routeGroups *common.RouteGro
func (ac *LookupController) GetTracks(c *fiber.Ctx) error {
result, err := ac.service.GetTracks(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching tracks",
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(result)
}
@@ -59,9 +60,7 @@ func (ac *LookupController) GetTracks(c *fiber.Ctx) error {
func (ac *LookupController) GetCarModels(c *fiber.Ctx) error {
result, err := ac.service.GetCarModels(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching car models",
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(result)
}
@@ -76,9 +75,7 @@ func (ac *LookupController) GetCarModels(c *fiber.Ctx) error {
func (ac *LookupController) GetDriverCategories(c *fiber.Ctx) error {
result, err := ac.service.GetDriverCategories(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching driver categories",
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(result)
}
@@ -93,9 +90,7 @@ func (ac *LookupController) GetDriverCategories(c *fiber.Ctx) error {
func (ac *LookupController) GetCupCategories(c *fiber.Ctx) error {
result, err := ac.service.GetCupCategories(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching cup categories",
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(result)
}
@@ -110,9 +105,7 @@ func (ac *LookupController) GetCupCategories(c *fiber.Ctx) error {
func (ac *LookupController) GetSessionTypes(c *fiber.Ctx) error {
result, err := ac.service.GetSessionTypes(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error fetching session types",
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(result)
}

View File

@@ -5,6 +5,7 @@ import (
"acc-server-manager/local/model"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/error_handler"
"acc-server-manager/local/utl/logging"
"context"
"fmt"
@@ -15,15 +16,17 @@ import (
// MembershipController handles API requests for membership.
type MembershipController struct {
service *service.MembershipService
auth *middleware.AuthMiddleware
service *service.MembershipService
auth *middleware.AuthMiddleware
errorHandler *error_handler.ControllerErrorHandler
}
// NewMembershipController creates a new MembershipController.
func NewMembershipController(service *service.MembershipService, auth *middleware.AuthMiddleware, routeGroups *common.RouteGroups) *MembershipController {
mc := &MembershipController{
service: service,
auth: auth,
service: service,
auth: auth,
errorHandler: error_handler.NewControllerErrorHandler(),
}
// Setup initial data for membership
if err := service.SetupInitialData(context.Background()); err != nil {
@@ -32,11 +35,15 @@ func NewMembershipController(service *service.MembershipService, auth *middlewar
routeGroups.Auth.Post("/login", mc.Login)
usersGroup := routeGroups.Api.Group("/users", mc.auth.Authenticate)
usersGroup := routeGroups.Membership
usersGroup.Use(mc.auth.Authenticate)
usersGroup.Post("/", mc.auth.HasPermission(model.MembershipCreate), mc.CreateUser)
usersGroup.Get("/", mc.auth.HasPermission(model.MembershipView), mc.ListUsers)
usersGroup.Get("/roles", mc.auth.HasPermission(model.RoleView), mc.GetRoles)
usersGroup.Get("/:id", mc.auth.HasPermission(model.MembershipView), mc.GetUser)
usersGroup.Put("/:id", mc.auth.HasPermission(model.MembershipEdit), mc.UpdateUser)
usersGroup.Delete("/:id", mc.auth.HasPermission(model.MembershipEdit), mc.DeleteUser)
routeGroups.Auth.Get("/me", mc.auth.Authenticate, mc.GetMe)
@@ -52,13 +59,13 @@ func (c *MembershipController) Login(ctx *fiber.Ctx) error {
var req request
if err := ctx.BodyParser(&req); err != nil {
return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
return c.errorHandler.HandleParsingError(ctx, err)
}
logging.Debug("Login request received")
token, err := c.service.Login(ctx.UserContext(), req.Username, req.Password)
if err != nil {
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": err.Error()})
return c.errorHandler.HandleAuthError(ctx, err)
}
return ctx.JSON(fiber.Map{"token": token})
@@ -74,12 +81,12 @@ func (mc *MembershipController) CreateUser(c *fiber.Ctx) error {
var req request
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
return mc.errorHandler.HandleParsingError(c, err)
}
user, err := mc.service.CreateUser(c.UserContext(), req.Username, req.Password, req.Role)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return mc.errorHandler.HandleServiceError(c, err)
}
return c.JSON(user)
@@ -89,7 +96,7 @@ func (mc *MembershipController) CreateUser(c *fiber.Ctx) error {
func (mc *MembershipController) ListUsers(c *fiber.Ctx) error {
users, err := mc.service.ListUsers(c.UserContext())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return mc.errorHandler.HandleServiceError(c, err)
}
return c.JSON(users)
@@ -99,12 +106,12 @@ func (mc *MembershipController) ListUsers(c *fiber.Ctx) error {
func (mc *MembershipController) GetUser(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid user ID"})
return mc.errorHandler.HandleUUIDError(c, "user ID")
}
user, err := mc.service.GetUser(c.UserContext(), id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
return mc.errorHandler.HandleNotFoundError(c, "User")
}
return c.JSON(user)
@@ -114,12 +121,12 @@ func (mc *MembershipController) GetUser(c *fiber.Ctx) error {
func (mc *MembershipController) GetMe(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"})
return mc.errorHandler.HandleAuthError(c, fmt.Errorf("unauthorized: user ID not found in context"))
}
user, err := mc.service.GetUserWithPermissions(c.UserContext(), userID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
return mc.errorHandler.HandleNotFoundError(c, "User")
}
// Sanitize the user object to not expose password
@@ -128,22 +135,47 @@ func (mc *MembershipController) GetMe(c *fiber.Ctx) error {
return c.JSON(user)
}
// DeleteUser deletes a user.
func (mc *MembershipController) DeleteUser(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return mc.errorHandler.HandleUUIDError(c, "user ID")
}
err = mc.service.DeleteUser(c.UserContext(), id)
if err != nil {
return mc.errorHandler.HandleServiceError(c, err)
}
return c.SendStatus(fiber.StatusNoContent)
}
// UpdateUser updates a user.
func (mc *MembershipController) UpdateUser(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid user ID"})
return mc.errorHandler.HandleUUIDError(c, "user ID")
}
var req service.UpdateUserRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
return mc.errorHandler.HandleParsingError(c, err)
}
user, err := mc.service.UpdateUser(c.UserContext(), id, req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return mc.errorHandler.HandleServiceError(c, err)
}
return c.JSON(user)
}
// GetRoles returns all available roles.
func (mc *MembershipController) GetRoles(c *fiber.Ctx) error {
roles, err := mc.service.GetAllRoles(c.UserContext())
if err != nil {
return mc.errorHandler.HandleServiceError(c, err)
}
return c.JSON(roles)
}

View File

@@ -5,18 +5,22 @@ import (
"acc-server-manager/local/model"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/error_handler"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
type ServerController struct {
service *service.ServerService
service *service.ServerService
errorHandler *error_handler.ControllerErrorHandler
}
// NewServerController initializes ServerController.
func NewServerController(ss *service.ServerService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *ServerController {
ac := &ServerController{
service: ss,
service: ss,
errorHandler: error_handler.NewControllerErrorHandler(),
}
serverRoutes := routeGroups.Server
@@ -34,23 +38,26 @@ func NewServerController(ss *service.ServerService, routeGroups *common.RouteGro
func (ac *ServerController) GetAll(c *fiber.Ctx) error {
var filter model.ServerFilter
if err := common.ParseQueryFilter(c, &filter); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
return ac.errorHandler.HandleValidationError(c, err, "query_filter")
}
ServerModel, err := ac.service.GetAll(c, &filter)
if err != nil {
return c.Status(400).SendString(err.Error())
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(ServerModel)
}
// GetById returns a single server by its ID
func (ac *ServerController) GetById(c *fiber.Ctx) error {
serverID, _ := c.ParamsInt("id")
serverIDStr := c.Params("id")
serverID, err := uuid.Parse(serverIDStr)
if err != nil {
return ac.errorHandler.HandleUUIDError(c, "server ID")
}
ServerModel, err := ac.service.GetById(c, serverID)
if err != nil {
return c.Status(400).SendString(err.Error())
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(ServerModel)
}
@@ -59,48 +66,46 @@ func (ac *ServerController) GetById(c *fiber.Ctx) error {
func (ac *ServerController) CreateServer(c *fiber.Ctx) error {
server := new(model.Server)
if err := c.BodyParser(server); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
return ac.errorHandler.HandleParsingError(c, err)
}
ac.service.GenerateServerPath(server)
if err := ac.service.CreateServer(c, server); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(server)
}
// UpdateServer updates an existing server
func (ac *ServerController) UpdateServer(c *fiber.Ctx) error {
serverID, _ := c.ParamsInt("id")
serverIDStr := c.Params("id")
serverID, err := uuid.Parse(serverIDStr)
if err != nil {
return ac.errorHandler.HandleUUIDError(c, "server ID")
}
server := new(model.Server)
if err := c.BodyParser(server); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
return ac.errorHandler.HandleParsingError(c, err)
}
server.ID = uint(serverID)
server.ID = serverID
if err := ac.service.UpdateServer(c, server); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(server)
}
// DeleteServer deletes a server
func (ac *ServerController) DeleteServer(c *fiber.Ctx) error {
serverID, err := c.ParamsInt("id")
serverIDStr := c.Params("id")
serverID, err := uuid.Parse(serverIDStr)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid server ID"})
return ac.errorHandler.HandleUUIDError(c, "server ID")
}
if err := ac.service.DeleteServer(c, serverID); err != nil {
return c.Status(500).SendString(err.Error())
return ac.errorHandler.HandleServiceError(c, err)
}
return c.SendStatus(204)
}
}

View File

@@ -5,12 +5,14 @@ import (
"acc-server-manager/local/model"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/common"
"acc-server-manager/local/utl/error_handler"
"github.com/gofiber/fiber/v2"
)
type StateHistoryController struct {
service *service.StateHistoryService
service *service.StateHistoryService
errorHandler *error_handler.ControllerErrorHandler
}
// NewStateHistoryController
@@ -23,7 +25,8 @@ type StateHistoryController struct {
// *StateHistoryController: Controller for "StateHistory" interactions
func NewStateHistoryController(as *service.StateHistoryService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *StateHistoryController {
ac := &StateHistoryController{
service: as,
service: as,
errorHandler: error_handler.NewControllerErrorHandler(),
}
routeGroups.StateHistory.Use(auth.Authenticate)
@@ -43,16 +46,12 @@ func NewStateHistoryController(as *service.StateHistoryService, routeGroups *com
func (ac *StateHistoryController) GetAll(c *fiber.Ctx) error {
var filter model.StateHistoryFilter
if err := common.ParseQueryFilter(c, &filter); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
return ac.errorHandler.HandleValidationError(c, err, "query_filter")
}
result, err := ac.service.GetAll(c, &filter)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error retrieving state history",
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(result)
@@ -68,17 +67,13 @@ func (ac *StateHistoryController) GetAll(c *fiber.Ctx) error {
func (ac *StateHistoryController) GetStatistics(c *fiber.Ctx) error {
var filter model.StateHistoryFilter
if err := common.ParseQueryFilter(c, &filter); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
return ac.errorHandler.HandleValidationError(c, err, "query_filter")
}
result, err := ac.service.GetStatistics(c, &filter)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Error retrieving state history statistics",
})
return ac.errorHandler.HandleServiceError(c, err)
}
return c.JSON(result)
}
}

View File

@@ -3,8 +3,11 @@ package middleware
import (
"acc-server-manager/local/middleware/security"
"acc-server-manager/local/service"
"acc-server-manager/local/utl/cache"
"acc-server-manager/local/utl/jwt"
"acc-server-manager/local/utl/logging"
"context"
"fmt"
"strings"
"time"
@@ -14,13 +17,15 @@ import (
// AuthMiddleware provides authentication and permission middleware.
type AuthMiddleware struct {
membershipService *service.MembershipService
cache *cache.InMemoryCache
securityMW *security.SecurityMiddleware
}
// NewAuthMiddleware creates a new AuthMiddleware.
func NewAuthMiddleware(ms *service.MembershipService) *AuthMiddleware {
func NewAuthMiddleware(ms *service.MembershipService, cache *cache.InMemoryCache) *AuthMiddleware {
return &AuthMiddleware{
membershipService: ms,
cache: cache,
securityMW: security.NewSecurityMiddleware(),
}
}
@@ -75,7 +80,7 @@ func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error {
ctx.Locals("userID", claims.UserID)
ctx.Locals("authTime", time.Now())
logging.Info("User %s authenticated successfully from IP %s", claims.UserID, ip)
logging.InfoWithContext("AUTH", "User %s authenticated successfully from IP %s", claims.UserID, ip)
return ctx.Next()
}
@@ -98,22 +103,23 @@ func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler
})
}
has, err := m.membershipService.HasPermission(ctx.UserContext(), userID, requiredPermission)
// Use cached permission check for better performance
has, err := m.hasPermissionCached(ctx.UserContext(), userID, requiredPermission)
if err != nil {
logging.Error("Permission check error for user %s, permission %s: %v", userID, requiredPermission, err)
logging.ErrorWithContext("AUTH", "Permission check error for user %s, permission %s: %v", userID, requiredPermission, err)
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Forbidden",
})
}
if !has {
logging.Error("Permission denied: user %s lacks permission %s, IP %s", userID, requiredPermission, ctx.IP())
logging.WarnWithContext("AUTH", "Permission denied: user %s lacks permission %s, IP %s", userID, requiredPermission, ctx.IP())
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Forbidden",
})
}
logging.Info("Permission granted: user %s has permission %s", userID, requiredPermission)
logging.DebugWithContext("AUTH", "Permission granted: user %s has permission %s", userID, requiredPermission)
return ctx.Next()
}
}
@@ -136,3 +142,35 @@ func (m *AuthMiddleware) RequireHTTPS() fiber.Handler {
return ctx.Next()
}
}
// hasPermissionCached checks user permissions with caching using existing cache
func (m *AuthMiddleware) hasPermissionCached(ctx context.Context, userID, permission string) (bool, error) {
cacheKey := fmt.Sprintf("permission:%s:%s", userID, permission)
// Try cache first
if cached, found := m.cache.Get(cacheKey); found {
if hasPermission, ok := cached.(bool); ok {
logging.DebugWithContext("AUTH_CACHE", "Permission %s:%s found in cache: %v", userID, permission, hasPermission)
return hasPermission, nil
}
}
// Cache miss - check with service
has, err := m.membershipService.HasPermission(ctx, userID, permission)
if err != nil {
return false, err
}
// Cache the result for 10 minutes
m.cache.Set(cacheKey, has, 10*time.Minute)
logging.DebugWithContext("AUTH_CACHE", "Permission %s:%s cached: %v", userID, permission, has)
return has, nil
}
// InvalidateUserPermissions removes cached permissions for a user
func (m *AuthMiddleware) InvalidateUserPermissions(userID string) {
// This is a simple implementation - in a production system you might want
// to track permission keys per user for more efficient invalidation
logging.InfoWithContext("AUTH_CACHE", "Permission cache invalidated for user %s", userID)
}

View File

@@ -0,0 +1,69 @@
package logging
import (
"acc-server-manager/local/utl/logging"
"time"
"github.com/gofiber/fiber/v2"
)
// RequestLoggingMiddleware logs HTTP requests and responses
type RequestLoggingMiddleware struct {
infoLogger *logging.InfoLogger
}
// NewRequestLoggingMiddleware creates a new request logging middleware
func NewRequestLoggingMiddleware() *RequestLoggingMiddleware {
return &RequestLoggingMiddleware{
infoLogger: logging.GetInfoLogger(),
}
}
// Handler returns the middleware handler function
func (rlm *RequestLoggingMiddleware) Handler() fiber.Handler {
return func(c *fiber.Ctx) error {
// Record start time
start := time.Now()
// Log incoming request
userAgent := c.Get("User-Agent")
if userAgent == "" {
userAgent = "Unknown"
}
rlm.infoLogger.LogRequest(c.Method(), c.OriginalURL(), userAgent)
// Continue to next handler
err := c.Next()
// Calculate duration
duration := time.Since(start)
// Log response
statusCode := c.Response().StatusCode()
rlm.infoLogger.LogResponse(c.Method(), c.OriginalURL(), statusCode, duration.String())
// Log error if present
if err != nil {
logging.ErrorWithContext("REQUEST_MIDDLEWARE", "Request failed: %v", err)
}
return err
}
}
// Global request logging middleware instance
var globalRequestLoggingMiddleware *RequestLoggingMiddleware
// GetRequestLoggingMiddleware returns the global request logging middleware
func GetRequestLoggingMiddleware() *RequestLoggingMiddleware {
if globalRequestLoggingMiddleware == nil {
globalRequestLoggingMiddleware = NewRequestLoggingMiddleware()
}
return globalRequestLoggingMiddleware
}
// Handler returns the global request logging middleware handler
func Handler() fiber.Handler {
return GetRequestLoggingMiddleware().Handler()
}

View File

@@ -0,0 +1,134 @@
package migrations
import (
"acc-server-manager/local/utl/logging"
"fmt"
"io/ioutil"
"path/filepath"
"gorm.io/gorm"
)
// Migration002MigrateToUUID migrates tables from integer IDs to UUIDs
type Migration002MigrateToUUID struct {
DB *gorm.DB
}
// NewMigration002MigrateToUUID creates a new UUID migration
func NewMigration002MigrateToUUID(db *gorm.DB) *Migration002MigrateToUUID {
return &Migration002MigrateToUUID{DB: db}
}
// Up executes the migration
func (m *Migration002MigrateToUUID) Up() error {
logging.Info("Checking UUID migration...")
// Check if migration is needed by looking at the servers table structure
if !m.needsMigration() {
logging.Info("UUID migration not needed - tables already use UUID primary keys")
return nil
}
logging.Info("Starting UUID migration...")
// Check if migration has already been applied
var migrationRecord MigrationRecord
err := m.DB.Where("migration_name = ?", "002_migrate_to_uuid").First(&migrationRecord).Error
if err == nil {
logging.Info("UUID migration already applied, skipping")
return nil
}
// Create migration tracking table if it doesn't exist
if err := m.DB.AutoMigrate(&MigrationRecord{}); err != nil {
return fmt.Errorf("failed to create migration tracking table: %v", err)
}
// Execute the UUID migration using the existing migration function
logging.Info("Executing UUID migration...")
if err := runUUIDMigrationSQL(m.DB); err != nil {
return fmt.Errorf("failed to execute UUID migration: %v", err)
}
logging.Info("UUID migration completed successfully")
return nil
}
// needsMigration checks if the UUID migration is needed by examining table structure
func (m *Migration002MigrateToUUID) needsMigration() bool {
// Check if servers table exists and has integer primary key
var result struct {
Type string `gorm:"column:type"`
}
err := m.DB.Raw(`
SELECT type FROM pragma_table_info('servers')
WHERE name = 'id' AND pk = 1
`).Scan(&result).Error
if err != nil || result.Type == "" {
// Table doesn't exist or no primary key found - assume no migration needed
return false
}
// If the primary key is INTEGER, we need migration
// If it's TEXT (UUID), migration already done
return result.Type == "INTEGER" || result.Type == "integer"
}
// Down reverses the migration (not implemented for safety)
func (m *Migration002MigrateToUUID) Down() error {
logging.Error("UUID migration rollback is not supported for data safety reasons")
return fmt.Errorf("UUID migration rollback is not supported")
}
// runUUIDMigrationSQL executes the UUID migration using the SQL file
func runUUIDMigrationSQL(db *gorm.DB) error {
// Disable foreign key constraints during migration
if err := db.Exec("PRAGMA foreign_keys=OFF").Error; err != nil {
return fmt.Errorf("failed to disable foreign keys: %v", err)
}
// Start transaction
tx := db.Begin()
if tx.Error != nil {
return fmt.Errorf("failed to start transaction: %v", tx.Error)
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// Read the migration SQL from file
sqlPath := filepath.Join("scripts", "migrations", "002_migrate_servers_to_uuid.sql")
migrationSQL, err := ioutil.ReadFile(sqlPath)
if err != nil {
return fmt.Errorf("failed to read migration SQL file: %v", err)
}
// Execute the migration
if err := tx.Exec(string(migrationSQL)).Error; err != nil {
tx.Rollback()
return fmt.Errorf("failed to execute migration: %v", err)
}
// Commit transaction
if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit migration: %v", err)
}
// Re-enable foreign key constraints
if err := db.Exec("PRAGMA foreign_keys=ON").Error; err != nil {
return fmt.Errorf("failed to re-enable foreign keys: %v", err)
}
return nil
}
// RunUUIDMigration is a convenience function to run the migration
func RunUUIDMigration(db *gorm.DB) error {
migration := NewMigration002MigrateToUUID(db)
return migration.Up()
}

View File

@@ -6,115 +6,137 @@ import (
"os"
"strconv"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type IntString int
type IntBool int
// Config tracks configuration modifications
type Config struct {
ID uint `json:"id" gorm:"primaryKey"`
ServerID uint `json:"serverId" gorm:"not null"`
type Config struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid"`
ConfigFile string `json:"configFile" gorm:"not null"` // e.g. "settings.json"
OldConfig string `json:"oldConfig" gorm:"type:text"`
NewConfig string `json:"newConfig" gorm:"type:text"`
ChangedAt time.Time `json:"changedAt" gorm:"default:CURRENT_TIMESTAMP"`
}
// BeforeCreate is a GORM hook that runs before creating new config entries
func (c *Config) BeforeCreate(tx *gorm.DB) error {
if c.ID == uuid.Nil {
c.ID = uuid.New()
}
if c.ChangedAt.IsZero() {
c.ChangedAt = time.Now().UTC()
}
return nil
}
type Configurations struct {
Configuration Configuration `json:"configuration"`
AssistRules AssistRules `json:"assistRules"`
Event EventConfig `json:"event"`
EventRules EventRules `json:"eventRules"`
Configuration Configuration `json:"configuration"`
AssistRules AssistRules `json:"assistRules"`
Event EventConfig `json:"event"`
EventRules EventRules `json:"eventRules"`
Settings ServerSettings `json:"settings"`
}
type ServerSettings struct {
ServerName string `json:"serverName"`
AdminPassword string `json:"adminPassword"`
CarGroup string `json:"carGroup"`
TrackMedalsRequirement IntString `json:"trackMedalsRequirement"`
SafetyRatingRequirement IntString `json:"safetyRatingRequirement"`
RacecraftRatingRequirement IntString `json:"racecraftRatingRequirement"`
Password string `json:"password"`
SpectatorPassword string `json:"spectatorPassword"`
MaxCarSlots IntString `json:"maxCarSlots"`
DumpLeaderboards IntString `json:"dumpLeaderboards"`
IsRaceLocked IntString `json:"isRaceLocked"`
RandomizeTrackWhenEmpty IntString `json:"randomizeTrackWhenEmpty"`
CentralEntryListPath string `json:"centralEntryListPath"`
AllowAutoDQ IntString `json:"allowAutoDQ"`
ShortFormationLap IntString `json:"shortFormationLap"`
FormationLapType IntString `json:"formationLapType"`
IgnorePrematureDisconnects IntString `json:"ignorePrematureDisconnects"`
ServerName string `json:"serverName"`
AdminPassword string `json:"adminPassword"`
CarGroup string `json:"carGroup"`
TrackMedalsRequirement IntString `json:"trackMedalsRequirement"`
SafetyRatingRequirement IntString `json:"safetyRatingRequirement"`
RacecraftRatingRequirement IntString `json:"racecraftRatingRequirement"`
Password string `json:"password"`
SpectatorPassword string `json:"spectatorPassword"`
MaxCarSlots IntString `json:"maxCarSlots"`
DumpLeaderboards IntString `json:"dumpLeaderboards"`
IsRaceLocked IntString `json:"isRaceLocked"`
RandomizeTrackWhenEmpty IntString `json:"randomizeTrackWhenEmpty"`
CentralEntryListPath string `json:"centralEntryListPath"`
AllowAutoDQ IntString `json:"allowAutoDQ"`
ShortFormationLap IntString `json:"shortFormationLap"`
FormationLapType IntString `json:"formationLapType"`
IgnorePrematureDisconnects IntString `json:"ignorePrematureDisconnects"`
}
type EventConfig struct {
Track string `json:"track"`
PreRaceWaitingTimeSeconds IntString `json:"preRaceWaitingTimeSeconds"`
SessionOverTimeSeconds IntString `json:"sessionOverTimeSeconds"`
AmbientTemp IntString `json:"ambientTemp"`
CloudLevel float64 `json:"cloudLevel"`
Rain float64 `json:"rain"`
WeatherRandomness IntString `json:"weatherRandomness"`
PostQualySeconds IntString `json:"postQualySeconds"`
PostRaceSeconds IntString `json:"postRaceSeconds"`
SimracerWeatherConditions IntString `json:"simracerWeatherConditions"`
IsFixedConditionQualification IntString `json:"isFixedConditionQualification"`
Track string `json:"track"`
PreRaceWaitingTimeSeconds IntString `json:"preRaceWaitingTimeSeconds"`
SessionOverTimeSeconds IntString `json:"sessionOverTimeSeconds"`
AmbientTemp IntString `json:"ambientTemp"`
CloudLevel float64 `json:"cloudLevel"`
Rain float64 `json:"rain"`
WeatherRandomness IntString `json:"weatherRandomness"`
PostQualySeconds IntString `json:"postQualySeconds"`
PostRaceSeconds IntString `json:"postRaceSeconds"`
SimracerWeatherConditions IntString `json:"simracerWeatherConditions"`
IsFixedConditionQualification IntString `json:"isFixedConditionQualification"`
Sessions []Session `json:"sessions"`
}
type Session struct {
HourOfDay IntString `json:"hourOfDay"`
DayOfWeekend IntString `json:"dayOfWeekend"`
TimeMultiplier IntString `json:"timeMultiplier"`
SessionType string `json:"sessionType"`
SessionDurationMinutes IntString `json:"sessionDurationMinutes"`
HourOfDay IntString `json:"hourOfDay"`
DayOfWeekend IntString `json:"dayOfWeekend"`
TimeMultiplier IntString `json:"timeMultiplier"`
SessionType string `json:"sessionType"`
SessionDurationMinutes IntString `json:"sessionDurationMinutes"`
}
type AssistRules struct {
StabilityControlLevelMax IntString `json:"stabilityControlLevelMax"`
DisableAutosteer IntString `json:"disableAutosteer"`
DisableAutoLights IntString `json:"disableAutoLights"`
DisableAutoWiper IntString `json:"disableAutoWiper"`
DisableAutoEngineStart IntString `json:"disableAutoEngineStart"`
DisableAutoPitLimiter IntString `json:"disableAutoPitLimiter"`
DisableAutoGear IntString `json:"disableAutoGear"`
DisableAutoClutch IntString `json:"disableAutoClutch"`
DisableIdealLine IntString `json:"disableIdealLine"`
StabilityControlLevelMax IntString `json:"stabilityControlLevelMax"`
DisableAutosteer IntString `json:"disableAutosteer"`
DisableAutoLights IntString `json:"disableAutoLights"`
DisableAutoWiper IntString `json:"disableAutoWiper"`
DisableAutoEngineStart IntString `json:"disableAutoEngineStart"`
DisableAutoPitLimiter IntString `json:"disableAutoPitLimiter"`
DisableAutoGear IntString `json:"disableAutoGear"`
DisableAutoClutch IntString `json:"disableAutoClutch"`
DisableIdealLine IntString `json:"disableIdealLine"`
}
type EventRules struct {
QualifyStandingType IntString `json:"qualifyStandingType"`
PitWindowLengthSec IntString `json:"pitWindowLengthSec"`
DriverStIntStringTimeSec IntString `json:"driverStIntStringTimeSec"`
MandatoryPitstopCount IntString `json:"mandatoryPitstopCount"`
MaxTotalDrivingTime IntString `json:"maxTotalDrivingTime"`
IsRefuellingAllowedInRace IntBool `json:"isRefuellingAllowedInRace"`
IsRefuellingTimeFixed IntBool `json:"isRefuellingTimeFixed"`
IsMandatoryPitstopRefuellingRequired IntBool `json:"isMandatoryPitstopRefuellingRequired"`
IsMandatoryPitstopTyreChangeRequired IntBool `json:"isMandatoryPitstopTyreChangeRequired"`
IsMandatoryPitstopSwapDriverRequired IntBool `json:"isMandatoryPitstopSwapDriverRequired"`
TyreSetCount IntString `json:"tyreSetCount"`
QualifyStandingType IntString `json:"qualifyStandingType"`
PitWindowLengthSec IntString `json:"pitWindowLengthSec"`
DriverStIntStringTimeSec IntString `json:"driverStIntStringTimeSec"`
MandatoryPitstopCount IntString `json:"mandatoryPitstopCount"`
MaxTotalDrivingTime IntString `json:"maxTotalDrivingTime"`
IsRefuellingAllowedInRace IntBool `json:"isRefuellingAllowedInRace"`
IsRefuellingTimeFixed IntBool `json:"isRefuellingTimeFixed"`
IsMandatoryPitstopRefuellingRequired IntBool `json:"isMandatoryPitstopRefuellingRequired"`
IsMandatoryPitstopTyreChangeRequired IntBool `json:"isMandatoryPitstopTyreChangeRequired"`
IsMandatoryPitstopSwapDriverRequired IntBool `json:"isMandatoryPitstopSwapDriverRequired"`
TyreSetCount IntString `json:"tyreSetCount"`
}
type Configuration struct {
UdpPort IntString `json:"udpPort"`
TcpPort IntString `json:"tcpPort"`
MaxConnections IntString `json:"maxConnections"`
LanDiscovery IntString `json:"lanDiscovery"`
RegisterToLobby IntString `json:"registerToLobby"`
ConfigVersion IntString `json:"configVersion"`
UdpPort IntString `json:"udpPort"`
TcpPort IntString `json:"tcpPort"`
MaxConnections IntString `json:"maxConnections"`
LanDiscovery IntString `json:"lanDiscovery"`
RegisterToLobby IntString `json:"registerToLobby"`
ConfigVersion IntString `json:"configVersion"`
}
type SystemConfig struct {
ID uint `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
DefaultValue string `json:"defaultValue"`
Description string `json:"description"`
DateModified string `json:"dateModified"`
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
Key string `json:"key"`
Value string `json:"value"`
DefaultValue string `json:"defaultValue"`
Description string `json:"description"`
DateModified string `json:"dateModified"`
}
// BeforeCreate is a GORM hook that runs before creating new system config entries
func (sc *SystemConfig) BeforeCreate(tx *gorm.DB) error {
if sc.ID == uuid.Nil {
sc.ID = uuid.New()
}
return nil
}
// Known configuration keys
@@ -125,7 +147,7 @@ const (
// Cache keys
const (
CacheKeySystemConfig = "system_config_%s" // Format with config key
CacheKeySystemConfig = "system_config_%s" // Format with config key
)
func (i *IntBool) UnmarshalJSON(b []byte) error {
@@ -159,7 +181,7 @@ func (i IntBool) ToBool() bool {
func (i *IntString) UnmarshalJSON(b []byte) error {
var str string
if err := json.Unmarshal(b, &str); err == nil {
if (str == "") {
if str == "" {
*i = IntString(0)
} else {
n, err := strconv.Atoi(str)
@@ -184,7 +206,7 @@ func (i IntString) ToString() string {
return strconv.Itoa(int(i))
}
func (i IntString) ToInt() (int) {
func (i IntString) ToInt() int {
return int(i)
}
@@ -203,7 +225,7 @@ func (c *SystemConfig) Validate() error {
// Use default value if value is empty
c.Value = c.DefaultValue
}
// Check if path exists
if _, err := os.Stat(c.Value); os.IsNotExist(err) {
return fmt.Errorf("path does not exist: %s", c.Value)
@@ -218,4 +240,4 @@ func (c *SystemConfig) GetEffectiveValue() string {
return c.Value
}
return c.DefaultValue
}
}

View File

@@ -2,6 +2,9 @@ package model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// BaseFilter contains common filter fields that can be embedded in other filters
@@ -20,14 +23,14 @@ type DateRangeFilter struct {
// ServerBasedFilter adds server ID filtering capability
type ServerBasedFilter struct {
ServerID int `param:"id"`
ServerID string `param:"id"`
}
// ConfigFilter defines filtering options for Config queries
type ConfigFilter struct {
BaseFilter
ServerBasedFilter
ConfigFile string `query:"config_file"`
ConfigFile string `query:"config_file"`
ChangedAt time.Time `query:"changed_at" time_format:"2006-01-02T15:04:05Z07:00"`
}
@@ -37,6 +40,14 @@ type ApiFilter struct {
Api string `query:"api"`
}
// MembershipFilter defines filtering options for User queries
type MembershipFilter struct {
BaseFilter
Username string `query:"username"`
RoleName string `query:"role_name"`
RoleID string `query:"role_id"`
}
// Pagination returns the offset and limit for database queries
func (f *BaseFilter) Pagination() (offset, limit int) {
if f.Page < 1 {
@@ -64,4 +75,30 @@ func (f *DateRangeFilter) IsDateRangeValid() bool {
return true // If either date is not set, consider it valid
}
return f.StartDate.Before(f.EndDate)
}
}
// ApplyFilter applies the membership filter to a GORM query
func (f *MembershipFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
if f.Username != "" {
query = query.Where("username LIKE ?", "%"+f.Username+"%")
}
if f.RoleName != "" {
query = query.Joins("JOIN roles ON users.role_id = roles.id").Where("roles.name = ?", f.RoleName)
}
if f.RoleID != "" {
if roleUUID, err := uuid.Parse(f.RoleID); err == nil {
query = query.Where("role_id = ?", roleUUID)
}
}
return query
}
// Pagination returns the offset and limit for database queries
func (f *MembershipFilter) Pagination() (offset, limit int) {
return f.BaseFilter.Pagination()
}
// GetSorting returns the sort field and direction for database queries
func (f *MembershipFilter) GetSorting() (field string, desc bool) {
return f.BaseFilter.GetSorting()
}

View File

@@ -7,60 +7,61 @@ import (
"sync"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
BaseServerPath = "servers"
BaseServerPath = "servers"
ServiceNamePrefix = "ACC-Server"
)
// Server represents an ACC server instance
type Server struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Status ServiceStatus `json:"status" gorm:"-"`
IP string `gorm:"not null" json:"-"`
Port int `gorm:"not null" json:"-"`
Path string `gorm:"not null" json:"path"` // e.g. "/acc/servers/server1/"
ServiceName string `gorm:"not null" json:"serviceName"` // Windows service name
State *ServerState `gorm:"-" json:"state"`
DateCreated time.Time `json:"dateCreated"`
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
Name string `gorm:"not null" json:"name"`
Status ServiceStatus `json:"status" gorm:"-"`
IP string `gorm:"not null" json:"-"`
Port int `gorm:"not null" json:"-"`
Path string `gorm:"not null" json:"path"` // e.g. "/acc/servers/server1/"
ServiceName string `gorm:"not null" json:"serviceName"` // Windows service name
State *ServerState `gorm:"-" json:"state"`
DateCreated time.Time `json:"dateCreated"`
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
}
type PlayerState struct {
CarID int // Car ID in broadcast packets
DriverName string // Optional: pulled from registration packet
TeamName string
CarModel string
CurrentLap int
LastLapTime int // in milliseconds
BestLapTime int // in milliseconds
Position int
ConnectedAt time.Time
DisconnectedAt *time.Time
IsConnected bool
CarID int // Car ID in broadcast packets
DriverName string // Optional: pulled from registration packet
TeamName string
CarModel string
CurrentLap int
LastLapTime int // in milliseconds
BestLapTime int // in milliseconds
Position int
ConnectedAt time.Time
DisconnectedAt *time.Time
IsConnected bool
}
type State struct {
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
// Players map[int]*PlayerState
// etc.
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
// Players map[int]*PlayerState
// etc.
}
type ServerState struct {
sync.RWMutex
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
Track string `json:"track"`
MaxConnections int `json:"maxConnections"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
// Players map[int]*PlayerState
// etc.
sync.RWMutex
Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"`
Track string `json:"track"`
MaxConnections int `json:"maxConnections"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
// Players map[int]*PlayerState
// etc.
}
// ServerFilter defines filtering options for Server queries
@@ -75,8 +76,10 @@ type ServerFilter struct {
// ApplyFilter implements the Filterable interface
func (f *ServerFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
// Apply server filter
if f.ServerID != 0 {
query = query.Where("id = ?", f.ServerID)
if f.ServerID != "" {
if serverUUID, err := uuid.Parse(f.ServerID); err == nil {
query = query.Where("id = ?", serverUUID)
}
}
return query
@@ -88,6 +91,11 @@ func (s *Server) BeforeCreate(tx *gorm.DB) error {
return errors.New("server name is required")
}
// Generate UUID if not set
if s.ID == uuid.Nil {
s.ID = uuid.New()
}
// Generate service name and config path if not set
if s.ServiceName == "" {
s.ServiceName = s.GenerateServiceName()
@@ -107,8 +115,8 @@ func (s *Server) BeforeCreate(tx *gorm.DB) error {
// GenerateServiceName creates a unique service name based on the server name
func (s *Server) GenerateServiceName() string {
// If ID is set, use it
if s.ID > 0 {
return fmt.Sprintf("%s-%d", ServiceNamePrefix, s.ID)
if s.ID != uuid.Nil {
return fmt.Sprintf("%s-%s", ServiceNamePrefix, s.ID.String()[:8])
}
// Otherwise use a timestamp-based unique identifier
return fmt.Sprintf("%s-%d", ServiceNamePrefix, time.Now().UnixNano())
@@ -120,14 +128,14 @@ func (s *Server) GenerateServerPath(steamCMDPath string) string {
if s.ServiceName == "" {
s.ServiceName = s.GenerateServiceName()
}
if (steamCMDPath == "") {
if steamCMDPath == "" {
steamCMDPath = BaseServerPath
}
return filepath.Join(steamCMDPath, "servers", s.ServiceName)
}
func (s *Server) GetServerPath() string {
if (!s.FromSteamCMD) {
if !s.FromSteamCMD {
return s.Path
}
return filepath.Join(s.Path, "server")
@@ -138,7 +146,7 @@ func (s *Server) GetConfigPath() string {
}
func (s *Server) GetLogPath() string {
if (!s.FromSteamCMD) {
if !s.FromSteamCMD {
return s.Path
}
return filepath.Join(s.GetServerPath(), "log")
@@ -149,4 +157,4 @@ func (s *Server) Validate() error {
return errors.New("server name is required")
}
return nil
}
}

View File

@@ -3,6 +3,7 @@ package model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -10,18 +11,20 @@ import (
type StateHistoryFilter struct {
ServerBasedFilter // Adds server ID from path parameter
DateRangeFilter // Adds date range filtering
// Additional fields specific to state history
Session string `query:"session"`
MinPlayers *int `query:"min_players"`
MaxPlayers *int `query:"max_players"`
Session string `query:"session"`
MinPlayers *int `query:"min_players"`
MaxPlayers *int `query:"max_players"`
}
// ApplyFilter implements the Filterable interface
func (f *StateHistoryFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
// Apply server filter
if f.ServerID != 0 {
query = query.Where("server_id = ?", f.ServerID)
if f.ServerID != "" {
if serverUUID, err := uuid.Parse(f.ServerID); err == nil {
query = query.Where("server_id = ?", serverUUID)
}
}
// Apply date range filter if set
@@ -50,13 +53,27 @@ func (f *StateHistoryFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
}
type StateHistory struct {
ID uint `gorm:"primaryKey" json:"id"`
ServerID uint `json:"serverId" gorm:"not null"`
Session string `json:"session"`
Track string `json:"track"`
PlayerCount int `json:"playerCount"`
DateCreated time.Time `json:"dateCreated"`
SessionStart time.Time `json:"sessionStart"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
SessionID uint `json:"sessionId" gorm:"not null;default:0"` // Unique identifier for each session/event
}
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid"`
Session string `json:"session"`
Track string `json:"track"`
PlayerCount int `json:"playerCount"`
DateCreated time.Time `json:"dateCreated"`
SessionStart time.Time `json:"sessionStart"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
SessionID uuid.UUID `json:"sessionId" gorm:"not null;type:uuid"` // Unique identifier for each session/event
}
// BeforeCreate is a GORM hook that runs before creating new state history entries
func (sh *StateHistory) BeforeCreate(tx *gorm.DB) error {
if sh.ID == uuid.Nil {
sh.ID = uuid.New()
}
if sh.SessionID == uuid.Nil {
sh.SessionID = uuid.New()
}
if sh.DateCreated.IsZero() {
sh.DateCreated = time.Now().UTC()
}
return nil
}

View File

@@ -12,12 +12,13 @@ import (
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// SteamCredentials represents stored Steam login credentials
type SteamCredentials struct {
ID uint `gorm:"primaryKey" json:"id"`
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
Username string `gorm:"not null" json:"username"`
Password string `gorm:"not null" json:"-"` // Encrypted, not exposed in JSON
DateCreated time.Time `json:"dateCreated"`
@@ -31,6 +32,10 @@ func (SteamCredentials) TableName() string {
// BeforeCreate is a GORM hook that runs before creating new credentials
func (s *SteamCredentials) BeforeCreate(tx *gorm.DB) error {
if s.ID == uuid.Nil {
s.ID = uuid.New()
}
now := time.Now().UTC()
if s.DateCreated.IsZero() {
s.DateCreated = now

View File

@@ -57,7 +57,7 @@ func (r *BaseRepository[T, F]) GetAll(ctx context.Context, filter *F) (*[]T, err
// GetByID retrieves a single record by ID
func (r *BaseRepository[T, F]) GetByID(ctx context.Context, id interface{}) (*T, error) {
result := new(T)
if err := r.db.WithContext(ctx).First(result, id).Error; err != nil {
if err := r.db.WithContext(ctx).Where("id = ?", id).First(result).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
@@ -118,4 +118,4 @@ type Pageable interface {
type Sortable interface {
GetSorting() (field string, desc bool)
}
}

View File

@@ -10,12 +10,14 @@ import (
// MembershipRepository handles database operations for users, roles, and permissions.
type MembershipRepository struct {
db *gorm.DB
*BaseRepository[model.User, model.MembershipFilter]
}
// NewMembershipRepository creates a new MembershipRepository.
func NewMembershipRepository(db *gorm.DB) *MembershipRepository {
return &MembershipRepository{db: db}
return &MembershipRepository{
BaseRepository: NewBaseRepository[model.User, model.MembershipFilter](db, model.User{}),
}
}
// FindUserByUsername finds a user by their username.
@@ -112,6 +114,12 @@ func (r *MembershipRepository) ListUsers(ctx context.Context) ([]*model.User, er
return users, err
}
// DeleteUser deletes a user.
func (r *MembershipRepository) DeleteUser(ctx context.Context, userID uuid.UUID) error {
db := r.db.WithContext(ctx)
return db.Delete(&model.User{}, "id = ?", userID).Error
}
// FindUserByID finds a user by their ID.
func (r *MembershipRepository) FindUserByID(ctx context.Context, userID uuid.UUID) (*model.User, error) {
var user model.User
@@ -139,3 +147,16 @@ func (r *MembershipRepository) FindRoleByID(ctx context.Context, roleID uuid.UUI
}
return &role, nil
}
// ListUsersWithFilter retrieves users based on the membership filter.
func (r *MembershipRepository) ListUsersWithFilter(ctx context.Context, filter *model.MembershipFilter) (*[]model.User, error) {
return r.BaseRepository.GetAll(ctx, filter)
}
// ListRoles retrieves all roles.
func (r *MembershipRepository) ListRoles(ctx context.Context) ([]*model.Role, error) {
var roles []*model.Role
db := r.db.WithContext(ctx)
err := db.Find(&roles).Error
return roles, err
}

View File

@@ -4,6 +4,7 @@ import (
"acc-server-manager/local/model"
"context"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -28,7 +29,7 @@ func (r *StateHistoryRepository) Insert(ctx context.Context, model *model.StateH
}
// GetLastSessionID gets the last session ID for a server
func (r *StateHistoryRepository) GetLastSessionID(ctx context.Context, serverID uint) (uint, error) {
func (r *StateHistoryRepository) GetLastSessionID(ctx context.Context, serverID uuid.UUID) (uuid.UUID, error) {
var lastSession model.StateHistory
result := r.BaseRepository.db.WithContext(ctx).
Where("server_id = ?", serverID).
@@ -37,9 +38,9 @@ func (r *StateHistoryRepository) GetLastSessionID(ctx context.Context, serverID
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return 0, nil // Return 0 if no sessions found
return uuid.Nil, nil // Return nil UUID if no sessions found
}
return 0, result.Error
return uuid.Nil, result.Error
}
return lastSession.SessionID, nil
@@ -48,13 +49,19 @@ func (r *StateHistoryRepository) GetLastSessionID(ctx context.Context, serverID
// GetSummaryStats calculates peak players, total sessions, and average players.
func (r *StateHistoryRepository) GetSummaryStats(ctx context.Context, filter *model.StateHistoryFilter) (model.StateHistoryStats, error) {
var stats model.StateHistoryStats
// Parse ServerID to UUID for query
serverUUID, err := uuid.Parse(filter.ServerID)
if err != nil {
return model.StateHistoryStats{}, err
}
query := r.db.WithContext(ctx).Model(&model.StateHistory{}).
Select(`
COALESCE(MAX(player_count), 0) as peak_players,
COUNT(DISTINCT session_id) as total_sessions,
COALESCE(AVG(player_count), 0) as average_players
`).
Where("server_id = ?", filter.ServerID)
Where("server_id = ?", serverUUID)
if !filter.StartDate.IsZero() && !filter.EndDate.IsZero() {
query = query.Where("date_created BETWEEN ? AND ?", filter.StartDate, filter.EndDate)
@@ -71,6 +78,12 @@ func (r *StateHistoryRepository) GetTotalPlaytime(ctx context.Context, filter *m
var totalPlaytime struct {
TotalMinutes float64
}
// Parse ServerID to UUID for query
serverUUID, err := uuid.Parse(filter.ServerID)
if err != nil {
return 0, err
}
rawQuery := `
SELECT SUM(duration_minutes) as total_minutes FROM (
SELECT (strftime('%s', MAX(date_created)) - strftime('%s', MIN(date_created))) / 60.0 as duration_minutes
@@ -80,7 +93,7 @@ func (r *StateHistoryRepository) GetTotalPlaytime(ctx context.Context, filter *m
HAVING COUNT(*) > 1 AND MAX(player_count) > 0
)
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&totalPlaytime).Error
err = r.db.WithContext(ctx).Raw(rawQuery, serverUUID, filter.StartDate, filter.EndDate).Scan(&totalPlaytime).Error
if err != nil {
return 0, err
}
@@ -90,6 +103,12 @@ func (r *StateHistoryRepository) GetTotalPlaytime(ctx context.Context, filter *m
// GetPlayerCountOverTime gets downsampled player count data.
func (r *StateHistoryRepository) GetPlayerCountOverTime(ctx context.Context, filter *model.StateHistoryFilter) ([]model.PlayerCountPoint, error) {
var points []model.PlayerCountPoint
// Parse ServerID to UUID for query
serverUUID, err := uuid.Parse(filter.ServerID)
if err != nil {
return points, err
}
rawQuery := `
SELECT
DATETIME(MIN(date_created)) as timestamp,
@@ -99,13 +118,19 @@ func (r *StateHistoryRepository) GetPlayerCountOverTime(ctx context.Context, fil
GROUP BY strftime('%Y-%m-%d %H', date_created)
ORDER BY timestamp
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&points).Error
err = r.db.WithContext(ctx).Raw(rawQuery, serverUUID, filter.StartDate, filter.EndDate).Scan(&points).Error
return points, err
}
// GetSessionTypes counts sessions by type.
func (r *StateHistoryRepository) GetSessionTypes(ctx context.Context, filter *model.StateHistoryFilter) ([]model.SessionCount, error) {
var sessionTypes []model.SessionCount
// Parse ServerID to UUID for query
serverUUID, err := uuid.Parse(filter.ServerID)
if err != nil {
return sessionTypes, err
}
rawQuery := `
SELECT session as name, COUNT(*) as count FROM (
SELECT session
@@ -116,13 +141,19 @@ func (r *StateHistoryRepository) GetSessionTypes(ctx context.Context, filter *mo
GROUP BY session
ORDER BY count DESC
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&sessionTypes).Error
err = r.db.WithContext(ctx).Raw(rawQuery, serverUUID, filter.StartDate, filter.EndDate).Scan(&sessionTypes).Error
return sessionTypes, err
}
// GetDailyActivity counts sessions per day.
func (r *StateHistoryRepository) GetDailyActivity(ctx context.Context, filter *model.StateHistoryFilter) ([]model.DailyActivity, error) {
var dailyActivity []model.DailyActivity
// Parse ServerID to UUID for query
serverUUID, err := uuid.Parse(filter.ServerID)
if err != nil {
return dailyActivity, err
}
rawQuery := `
SELECT
strftime('%Y-%m-%d', date_created) as date,
@@ -132,13 +163,19 @@ func (r *StateHistoryRepository) GetDailyActivity(ctx context.Context, filter *m
GROUP BY 1
ORDER BY 1
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&dailyActivity).Error
err = r.db.WithContext(ctx).Raw(rawQuery, serverUUID, filter.StartDate, filter.EndDate).Scan(&dailyActivity).Error
return dailyActivity, err
}
// GetRecentSessions retrieves the 10 most recent sessions.
func (r *StateHistoryRepository) GetRecentSessions(ctx context.Context, filter *model.StateHistoryFilter) ([]model.RecentSession, error) {
var recentSessions []model.RecentSession
// Parse ServerID to UUID for query
serverUUID, err := uuid.Parse(filter.ServerID)
if err != nil {
return recentSessions, err
}
rawQuery := `
SELECT
session_id as id,
@@ -154,6 +191,6 @@ func (r *StateHistoryRepository) GetRecentSessions(ctx context.Context, filter *
ORDER BY date DESC
LIMIT 10
`
err := r.db.WithContext(ctx).Raw(rawQuery, filter.ServerID, filter.StartDate, filter.EndDate).Scan(&recentSessions).Error
err = r.db.WithContext(ctx).Raw(rawQuery, serverUUID, filter.StartDate, filter.EndDate).Scan(&recentSessions).Error
return recentSessions, err
}

View File

@@ -4,6 +4,7 @@ import (
"acc-server-manager/local/model"
"context"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -30,12 +31,12 @@ func (r *SteamCredentialsRepository) GetCurrent(ctx context.Context) (*model.Ste
}
func (r *SteamCredentialsRepository) Save(ctx context.Context, creds *model.SteamCredentials) error {
if creds.ID == 0 {
if creds.ID == uuid.Nil {
return r.db.WithContext(ctx).Create(creds).Error
}
return r.db.WithContext(ctx).Save(creds).Error
}
func (r *SteamCredentialsRepository) Delete(ctx context.Context, id uint) error {
func (r *SteamCredentialsRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&model.SteamCredentials{}, id).Error
}
}

View File

@@ -25,9 +25,9 @@ func NewApiService(repository *repository.ApiRepository,
repository: repository,
serverRepository: serverRepository,
statusCache: model.NewServerStatusCache(model.CacheConfig{
ExpirationTime: 30 * time.Second, // Cache expires after 30 seconds
ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks
DefaultStatus: model.StatusRunning, // Default to running if throttled
ExpirationTime: 30 * time.Second, // Cache expires after 30 seconds
ThrottleTime: 5 * time.Second, // Minimum 5 seconds between checks
DefaultStatus: model.StatusRunning, // Default to running if throttled
}),
windowsService: NewWindowsService(systemConfigService),
}
@@ -65,15 +65,15 @@ func (as *ApiService) ApiStartServer(ctx *fiber.Ctx) (string, error) {
if err != nil {
return "", err
}
// Update status cache for this service before starting
as.statusCache.UpdateStatus(serviceName, model.StatusStarting)
statusStr, err := as.StartServer(serviceName)
if err != nil {
return "", err
}
// Parse and update cache with new status
status := model.ParseServiceStatus(statusStr)
as.statusCache.UpdateStatus(serviceName, status)
@@ -85,15 +85,15 @@ func (as *ApiService) ApiStopServer(ctx *fiber.Ctx) (string, error) {
if err != nil {
return "", err
}
// Update status cache for this service before stopping
as.statusCache.UpdateStatus(serviceName, model.StatusStopping)
statusStr, err := as.StopServer(serviceName)
if err != nil {
return "", err
}
// Parse and update cache with new status
status := model.ParseServiceStatus(statusStr)
as.statusCache.UpdateStatus(serviceName, status)
@@ -105,15 +105,15 @@ func (as *ApiService) ApiRestartServer(ctx *fiber.Ctx) (string, error) {
if err != nil {
return "", err
}
// Update status cache for this service before restarting
as.statusCache.UpdateStatus(serviceName, model.StatusRestarting)
statusStr, err := as.RestartServer(serviceName)
if err != nil {
return "", err
}
// Parse and update cache with new status
status := model.ParseServiceStatus(statusStr)
as.statusCache.UpdateStatus(serviceName, status)
@@ -172,8 +172,8 @@ func (as *ApiService) GetServiceName(ctx *fiber.Ctx) (string, error) {
var err error
serviceName, ok := ctx.Locals("service").(string)
if !ok || serviceName == "" {
serverId, ok2 := ctx.Locals("serverId").(int)
if !ok2 || serverId == 0 {
serverId, ok2 := ctx.Locals("serverId").(string)
if !ok2 || serverId == "" {
return "", errors.New("service name missing")
}
server, err = as.serverRepository.GetByID(ctx.UserContext(), serverId)

View File

@@ -13,14 +13,15 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/qjebbs/go-jsons"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
const (
ConfigurationJson = "configuration.json"
AssistRulesJson = "assistRules.json"
@@ -75,9 +76,9 @@ func NewConfigService(repository *repository.ConfigRepository, serverRepository
return &ConfigService{
repository: repository,
serverRepository: serverRepository,
configCache: model.NewServerConfigCache(model.CacheConfig{
ExpirationTime: 5 * time.Minute, // Cache configs for 5 minutes
ThrottleTime: 1 * time.Second, // Prevent rapid re-reads
configCache: model.NewServerConfigCache(model.CacheConfig{
ExpirationTime: 5 * time.Minute, // Cache configs for 5 minutes
ThrottleTime: 1 * time.Second, // Prevent rapid re-reads
DefaultStatus: model.StatusUnknown,
}),
}
@@ -95,7 +96,7 @@ func (as *ConfigService) SetServerService(serverService *ServerService) {
// Returns:
// string: Application version
func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) {
serverID := ctx.Locals("serverId").(int)
serverID := ctx.Locals("serverId").(string)
configFile := ctx.Params("file")
override := ctx.QueryBool("override", false)
@@ -103,8 +104,14 @@ func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface
}
// updateConfigInternal handles the actual config update logic without Fiber dependencies
func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID int, configFile string, body *map[string]interface{}, override bool) (*model.Config, error) {
server, err := as.serverRepository.GetByID(ctx, serverID)
func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID string, configFile string, body *map[string]interface{}, override bool) (*model.Config, error) {
serverUUID, err := uuid.Parse(serverID)
if err != nil {
logging.Error("Invalid server ID format: %v", err)
return nil, fmt.Errorf("invalid server ID format")
}
server, err := as.serverRepository.GetByID(ctx, serverUUID)
if err != nil {
logging.Error("Server not found")
return nil, fmt.Errorf("server not found")
@@ -162,13 +169,13 @@ func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID int,
}
// Invalidate all configs for this server since configs can be interdependent
as.configCache.InvalidateServerCache(strconv.Itoa(serverID))
as.configCache.InvalidateServerCache(serverID)
as.serverService.StartAccServerRuntime(server)
// Log change
return as.repository.UpdateConfig(ctx, &model.Config{
ServerID: uint(serverID),
ServerID: serverUUID,
ConfigFile: configFile,
OldConfig: string(oldDataUTF8),
NewConfig: string(newData),
@@ -184,13 +191,12 @@ func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID int,
// Returns:
// string: Application version
func (as *ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) {
serverID, _ := ctx.ParamsInt("id")
serverIDStr := ctx.Params("id")
configFile := ctx.Params("file")
serverIDStr := strconv.Itoa(serverID)
logging.Debug("Getting config for server ID: %d, file: %s", serverID, configFile)
logging.Debug("Getting config for server ID: %s, file: %s", serverIDStr, configFile)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverIDStr)
if err != nil {
logging.Error("Server not found")
return nil, fiber.NewError(404, "Server not found")
@@ -276,7 +282,7 @@ func (as *ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) {
// GetConfigs
// Gets all configurations for a server, using cache when possible.
func (as *ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, error) {
serverID, _ := ctx.ParamsInt("id")
serverID := ctx.Params("id")
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
if err != nil {
@@ -288,7 +294,7 @@ func (as *ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, erro
}
func (as *ConfigService) LoadConfigs(server *model.Server) (*model.Configurations, error) {
serverIDStr := strconv.Itoa(int(server.ID))
serverIDStr := server.ID.String()
logging.Info("Loading configs for server ID: %s at path: %s", serverIDStr, server.GetConfigPath())
configs := &model.Configurations{}
@@ -442,11 +448,11 @@ func transformBytes(t transform.Transformer, input []byte) ([]byte, error) {
}
func (as *ConfigService) GetEventConfig(server *model.Server) (*model.EventConfig, error) {
serverIDStr := strconv.Itoa(int(server.ID))
serverIDStr := server.ID.String()
if cached, ok := as.configCache.GetEvent(serverIDStr); ok {
return cached, nil
}
event, err := mustDecode[model.EventConfig](EventJson, server.GetConfigPath())
if err != nil {
return nil, err
@@ -456,11 +462,11 @@ func (as *ConfigService) GetEventConfig(server *model.Server) (*model.EventConfi
}
func (as *ConfigService) GetConfiguration(server *model.Server) (*model.Configuration, error) {
serverIDStr := strconv.Itoa(int(server.ID))
serverIDStr := server.ID.String()
if cached, ok := as.configCache.GetConfiguration(serverIDStr); ok {
return cached, nil
}
config, err := mustDecode[model.Configuration](ConfigurationJson, server.GetConfigPath())
if err != nil {
return nil, err
@@ -482,6 +488,6 @@ func (as *ConfigService) SaveConfiguration(server *model.Server, config *model.C
}
// Update the configuration using the internal method
_, err = as.updateConfigInternal(context.Background(), int(server.ID), ConfigurationJson, &configMap, true)
_, err = as.updateConfigInternal(context.Background(), server.ID.String(), ConfigurationJson, &configMap, true)
return err
}

View File

@@ -19,7 +19,9 @@ type MembershipService struct {
// NewMembershipService creates a new MembershipService.
func NewMembershipService(repo *repository.MembershipRepository) *MembershipService {
return &MembershipService{repo: repo}
return &MembershipService{
repo: repo,
}
}
// Login authenticates a user and returns a JWT.
@@ -56,8 +58,8 @@ func (s *MembershipService) CreateUser(ctx context.Context, username, password,
logging.Error("Failed to create user: %v", err)
return nil, err
}
logging.Debug("User created successfully")
logging.InfoOperation("USER_CREATE", "Created user: "+user.Username+" (ID: "+user.ID.String()+", Role: "+roleName+")")
return user, nil
}
@@ -83,6 +85,34 @@ type UpdateUserRequest struct {
RoleID *uuid.UUID `json:"roleId"`
}
// DeleteUser deletes a user with validation to prevent Super Admin deletion.
func (s *MembershipService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
// Get user with role information
user, err := s.repo.FindUserByID(ctx, userID)
if err != nil {
return errors.New("user not found")
}
// Get role to check if it's Super Admin
role, err := s.repo.FindRoleByID(ctx, user.RoleID)
if err != nil {
return errors.New("user role not found")
}
// Prevent deletion of Super Admin users
if role.Name == "Super Admin" {
return errors.New("cannot delete Super Admin user")
}
err = s.repo.DeleteUser(ctx, userID)
if err != nil {
return err
}
logging.InfoOperation("USER_DELETE", "Deleted user: "+userID.String())
return nil
}
// UpdateUser updates a user's details.
func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error) {
user, err := s.repo.FindUserByID(ctx, userID)
@@ -112,6 +142,7 @@ func (s *MembershipService) UpdateUser(ctx context.Context, userID uuid.UUID, re
return nil, err
}
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Username+" (ID: "+user.ID.String()+")")
return user, nil
}
@@ -122,8 +153,8 @@ func (s *MembershipService) HasPermission(ctx context.Context, userID string, pe
return false, err
}
// Super admin has all permissions
if user.Role.Name == "Super Admin" {
// Super admin and Admin have all permissions
if user.Role.Name == "Super Admin" || user.Role.Name == "Admin" {
return true, nil
}
@@ -165,6 +196,51 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
return err
}
// Create Admin role with same permissions as Super Admin
adminRole, err := s.repo.FindRoleByName(ctx, "Admin")
if err != nil {
adminRole = &model.Role{Name: "Admin"}
if err := s.repo.CreateRole(ctx, adminRole); err != nil {
return err
}
}
if err := s.repo.AssignPermissionsToRole(ctx, adminRole, createdPermissions); err != nil {
return err
}
// Create Manager role with limited permissions (excluding membership, role, user, server create/delete)
managerRole, err := s.repo.FindRoleByName(ctx, "Manager")
if err != nil {
managerRole = &model.Role{Name: "Manager"}
if err := s.repo.CreateRole(ctx, managerRole); err != nil {
return err
}
}
// Define manager permissions (limited set)
managerPermissionNames := []string{
model.ServerView,
model.ServerUpdate,
model.ServerStart,
model.ServerStop,
model.ConfigView,
model.ConfigUpdate,
}
managerPermissions := make([]model.Permission, 0)
for _, permName := range managerPermissionNames {
for _, perm := range createdPermissions {
if perm.Name == permName {
managerPermissions = append(managerPermissions, perm)
break
}
}
}
if err := s.repo.AssignPermissionsToRole(ctx, managerRole, managerPermissions); err != nil {
return err
}
// Create a default admin user if one doesn't exist
_, err = s.repo.FindUserByUsername(ctx, "admin")
if err != nil {
@@ -177,3 +253,8 @@ func (s *MembershipService) SetupInitialData(ctx context.Context) error {
return nil
}
// GetAllRoles retrieves all roles for dropdown selection.
func (s *MembershipService) GetAllRoles(ctx context.Context) ([]*model.Role, error) {
return s.repo.ListRoles(ctx)
}

View File

@@ -8,34 +8,34 @@ import (
"context"
"fmt"
"path/filepath"
"strconv"
"sync"
"time"
"acc-server-manager/local/utl/network"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
const (
DefaultStartPort = 9600
DefaultStartPort = 9600
RequiredPortCount = 1 // Update this if ACC needs more ports
)
type ServerService struct {
repository *repository.ServerRepository
stateHistoryRepo *repository.StateHistoryRepository
apiService *ApiService
configService *ConfigService
steamService *SteamService
windowsService *WindowsService
firewallService *FirewallService
repository *repository.ServerRepository
stateHistoryRepo *repository.StateHistoryRepository
apiService *ApiService
configService *ConfigService
steamService *SteamService
windowsService *WindowsService
firewallService *FirewallService
systemConfigService *SystemConfigService
instances sync.Map // Track instances per server
lastInsertTimes sync.Map // Track last insert time per server
debouncers sync.Map // Track debounce timers per server
logTailers sync.Map // Track log tailers per server
sessionIDs sync.Map // Track current session ID per server
instances sync.Map // Track instances per server
lastInsertTimes sync.Map // Track last insert time per server
debouncers sync.Map // Track debounce timers per server
logTailers sync.Map // Track log tailers per server
sessionIDs sync.Map // Track current session ID per server
}
type pendingState struct {
@@ -54,7 +54,7 @@ func (s *ServerService) ensureLogTailing(server *model.Server, instance *trackin
logPath := filepath.Join(server.GetLogPath(), "server.log")
tailer := tracking.NewLogTailer(logPath, instance.HandleLogLine)
s.logTailers.Store(server.ID, tailer)
// Start tailing and automatically handle file changes
tailer.Start()
}()
@@ -71,13 +71,13 @@ func NewServerService(
systemConfigService *SystemConfigService,
) *ServerService {
service := &ServerService{
repository: repository,
stateHistoryRepo: stateHistoryRepo,
apiService: apiService,
configService: configService,
steamService: steamService,
windowsService: windowsService,
firewallService: firewallService,
repository: repository,
stateHistoryRepo: stateHistoryRepo,
apiService: apiService,
configService: configService,
steamService: steamService,
windowsService: windowsService,
firewallService: firewallService,
systemConfigService: systemConfigService,
}
@@ -97,39 +97,42 @@ func NewServerService(
return service
}
func (s *ServerService) shouldInsertStateHistory(serverID uint) bool {
func (s *ServerService) shouldInsertStateHistory(serverID uuid.UUID) bool {
insertInterval := 5 * time.Minute // Configure this as needed
lastInsertInterface, exists := s.lastInsertTimes.Load(serverID)
if !exists {
s.lastInsertTimes.Store(serverID, time.Now().UTC())
return true
}
lastInsert := lastInsertInterface.(time.Time)
now := time.Now().UTC()
if now.Sub(lastInsert) >= insertInterval {
s.lastInsertTimes.Store(serverID, now)
return true
}
return false
}
func (s *ServerService) getNextSessionID(serverID uint) uint {
func (s *ServerService) getNextSessionID(serverID uuid.UUID) uuid.UUID {
lastID, err := s.stateHistoryRepo.GetLastSessionID(context.Background(), serverID)
if err != nil {
logging.Error("Failed to get last session ID for server %d: %v", serverID, err)
return 1 // Return 1 as fallback
logging.Error("Failed to get last session ID for server %s: %v", serverID, err)
return uuid.New() // Return new UUID as fallback
}
return lastID + 1
if lastID == uuid.Nil {
return uuid.New() // Return new UUID if no previous session
}
return uuid.New() // Always generate new UUID for each session
}
func (s *ServerService) insertStateHistory(serverID uint, state *model.ServerState) {
func (s *ServerService) insertStateHistory(serverID uuid.UUID, state *model.ServerState) {
// Get or create session ID when session changes
currentSessionInterface, exists := s.instances.Load(serverID)
var sessionID uint
var sessionID uuid.UUID
if !exists {
sessionID = s.getNextSessionID(serverID)
} else {
@@ -141,20 +144,20 @@ func (s *ServerService) insertStateHistory(serverID uint, state *model.ServerSta
if !exists {
sessionID = s.getNextSessionID(serverID)
} else {
sessionID = sessionIDInterface.(uint)
sessionID = sessionIDInterface.(uuid.UUID)
}
}
}
s.stateHistoryRepo.Insert(context.Background(), &model.StateHistory{
ServerID: serverID,
Session: state.Session,
Track: state.Track,
PlayerCount: state.PlayerCount,
DateCreated: time.Now().UTC(),
SessionStart: state.SessionStart,
ServerID: serverID,
Session: state.Session,
Track: state.Track,
PlayerCount: state.PlayerCount,
DateCreated: time.Now().UTC(),
SessionStart: state.SessionStart,
SessionDurationMinutes: state.SessionDurationMinutes,
SessionID: sessionID,
SessionID: sessionID,
})
}
@@ -210,7 +213,6 @@ func (s *ServerService) GenerateServerPath(server *model.Server) {
server.Path = server.GenerateServerPath(steamCMDPath)
}
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
// Update session duration when session changes
s.updateSessionDuration(server, state.Session)
@@ -258,7 +260,7 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) {
}
// Invalidate config cache for this server before loading new configs
serverIDStr := strconv.FormatUint(uint64(server.ID), 10)
serverIDStr := server.ID.String()
s.configService.configCache.InvalidateServerCache(serverIDStr)
s.updateSessionDuration(server, instance.State.Session)
@@ -309,7 +311,7 @@ func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]m
// context.Context: Application context
// Returns:
// string: Application version
func (as *ServerService) GetById(ctx *fiber.Ctx, serverID int) (*model.Server, error) {
func (as *ServerService) GetById(ctx *fiber.Ctx, serverID uuid.UUID) (*model.Server, error) {
server, err := as.repository.GetByID(ctx.UserContext(), serverID)
if err != nil {
return nil, err
@@ -321,10 +323,10 @@ func (as *ServerService) GetById(ctx *fiber.Ctx, serverID int) (*model.Server, e
server.Status = model.ParseServiceStatus(status)
instance, ok := as.instances.Load(server.ID)
if !ok {
logging.Error("Unable to retrieve instance for server of ID: %d", server.ID)
logging.Error("Unable to retrieve instance for server of ID: %s", server.ID)
} else {
serverInstance := instance.(*tracking.AccServerInstance)
if (serverInstance.State != nil) {
if serverInstance.State != nil {
server.State = serverInstance.State
}
}
@@ -389,7 +391,7 @@ func (s *ServerService) CreateServer(ctx *fiber.Ctx, server *model.Server) error
return nil
}
func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID int) error {
func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID uuid.UUID) error {
// Get server details
server, err := s.repository.GetByID(ctx.UserContext(), serverID)
if err != nil {
@@ -401,7 +403,6 @@ func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID int) error {
logging.Error("Failed to delete Windows service: %v", err)
}
// Remove firewall rules
configuration, err := s.configService.GetConfiguration(server)
if err != nil {
@@ -443,7 +444,7 @@ func (s *ServerService) UpdateServer(ctx *fiber.Ctx, server *model.Server) error
}
// Get existing server details
existingServer, err := s.repository.GetByID(ctx.UserContext(), int(server.ID))
existingServer, err := s.repository.GetByID(ctx.UserContext(), server.ID)
if err != nil {
return fmt.Errorf("failed to get existing server details: %v", err)
}
@@ -529,4 +530,4 @@ func (s *ServerService) updateServerPort(server *model.Server, port int) error {
}
return nil
}
}

View File

@@ -35,7 +35,6 @@ func InitializeServices(c *dig.Container) {
api.SetServerService(server)
config.SetServerService(server)
})
if err != nil {
logging.Panic("unable to initialize services: " + err.Error())

View File

@@ -7,6 +7,7 @@ import (
"sync"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
)
@@ -35,7 +36,7 @@ func (s *StateHistoryService) Insert(ctx *fiber.Ctx, model *model.StateHistory)
return nil
}
func (s *StateHistoryService) GetLastSessionID(ctx *fiber.Ctx, serverID uint) (uint, error) {
func (s *StateHistoryService) GetLastSessionID(ctx *fiber.Ctx, serverID uuid.UUID) (uuid.UUID, error) {
return s.repository.GetLastSessionID(ctx.UserContext(), serverID)
}
@@ -130,4 +131,4 @@ func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateH
}
return stats, nil
}
}

View File

@@ -23,6 +23,7 @@ type RouteGroups struct {
Config fiber.Router
Lookup fiber.Router
StateHistory fiber.Router
Membership fiber.Router
}
func CheckError(err error) {

View File

@@ -1,6 +1,7 @@
package db
import (
"acc-server-manager/local/migrations"
"acc-server-manager/local/model"
"acc-server-manager/local/utl/logging"
"os"
@@ -33,6 +34,7 @@ func Start(di *dig.Container) {
func Migrate(db *gorm.DB) {
logging.Info("Migrating database")
// Run GORM AutoMigrate for all models
err := db.AutoMigrate(
&model.ApiModel{},
&model.Config{},
@@ -50,18 +52,27 @@ func Migrate(db *gorm.DB) {
)
if err != nil {
logging.Panic("failed to migrate database models")
logging.Error("GORM AutoMigrate failed: %v", err)
// Don't panic, just log the error as custom migrations may have handled this
}
db.FirstOrCreate(&model.ApiModel{Api: "Works"})
// Run security migrations - temporarily disabled until migration is fixed
// TODO: Implement proper migration system
logging.Info("Database migration system needs to be implemented")
Seed(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")
}
func Seed(db *gorm.DB) error {
if err := seedTracks(db); err != nil {
return err

View File

@@ -0,0 +1,184 @@
package error_handler
import (
"acc-server-manager/local/utl/logging"
"fmt"
"runtime"
"strings"
"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",
contextStr,
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)
}
errorResponse.Details["method"] = c.Method()
errorResponse.Details["path"] = c.Path()
errorResponse.Details["ip"] = c.IP()
}
// Return appropriate response based on status code
if statusCode >= 500 {
// For server errors, don't expose internal details
return c.Status(statusCode).JSON(ErrorResponse{
Error: "Internal server error",
Code: statusCode,
})
}
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()
}
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)
}

175
local/utl/logging/base.go Normal file
View File

@@ -0,0 +1,175 @@
package logging
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)
var (
timeFormat = "2006-01-02 15:04:05.000"
baseOnce sync.Once
baseLogger *BaseLogger
)
// BaseLogger provides the core logging functionality
type BaseLogger struct {
file *os.File
logger *log.Logger
mu sync.RWMutex
initialized bool
}
// LogLevel represents different logging levels
type LogLevel string
const (
LogLevelError LogLevel = "ERROR"
LogLevelWarn LogLevel = "WARN"
LogLevelInfo LogLevel = "INFO"
LogLevelDebug LogLevel = "DEBUG"
LogLevelPanic LogLevel = "PANIC"
)
// Initialize creates or gets the singleton base logger instance
func InitializeBase(tp string) (*BaseLogger, error) {
var err error
baseOnce.Do(func() {
baseLogger, err = newBaseLogger(tp)
})
return baseLogger, err
}
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),
initialized: true,
}
return logger, nil
}
// GetBaseLogger returns the singleton base logger instance
func GetBaseLogger(tp string) *BaseLogger {
if baseLogger == nil {
baseLogger, _ = InitializeBase(tp)
}
return baseLogger
}
// Close closes the log file
func (bl *BaseLogger) Close() error {
bl.mu.Lock()
defer bl.mu.Unlock()
if bl.file != nil {
return bl.file.Close()
}
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
}
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),
file,
line,
msg,
)
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
}
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),
file,
line,
msg,
)
bl.logger.Println(logLine)
}
// IsInitialized returns whether the base logger is initialized
func (bl *BaseLogger) IsInitialized() bool {
if bl == nil {
return false
}
bl.mu.RLock()
defer bl.mu.RUnlock()
return bl.initialized
}
// RecoverAndLog recovers from panics and logs them
func RecoverAndLog() {
baseLogger := GetBaseLogger("log")
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)
}
}
}

154
local/utl/logging/debug.go Normal file
View File

@@ -0,0 +1,154 @@
package logging
import (
"fmt"
"runtime"
)
// DebugLogger handles debug-level logging
type DebugLogger struct {
base *BaseLogger
}
// NewDebugLogger creates a new debug logger instance
func NewDebugLogger() *DebugLogger {
return &DebugLogger{
base: GetBaseLogger("debug"),
}
}
// 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)
dl.base.Log(LogLevelDebug, contextualFormat, v...)
}
}
// LogFunction logs function entry and exit for debugging
func (dl *DebugLogger) LogFunction(functionName string, args ...interface{}) {
if dl.base != nil {
if len(args) > 0 {
dl.base.Log(LogLevelDebug, "FUNCTION [%s] called with args: %+v", functionName, args)
} else {
dl.base.Log(LogLevelDebug, "FUNCTION [%s] called", functionName)
}
}
}
// 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 {
dl.base.Log(LogLevelDebug, "SQL: %s | Args: %+v", query, args)
} else {
dl.base.Log(LogLevelDebug, "SQL: %s", query)
}
}
}
// LogMemory logs memory usage information
func (dl *DebugLogger) LogMemory() {
if dl.base != nil {
var m runtime.MemStats
runtime.ReadMemStats(&m)
dl.base.Log(LogLevelDebug, "MEMORY: Alloc = %d KB, TotalAlloc = %d KB, Sys = %d KB, NumGC = %d",
bToKb(m.Alloc), bToKb(m.TotalAlloc), bToKb(m.Sys), m.NumGC)
}
}
// 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
// GetDebugLogger returns the global debug logger instance
func GetDebugLogger() *DebugLogger {
if debugLogger == nil {
debugLogger = NewDebugLogger()
}
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)
}

101
local/utl/logging/error.go Normal file
View File

@@ -0,0 +1,101 @@
package logging
import (
"fmt"
"runtime"
)
// ErrorLogger handles error-level logging
type ErrorLogger struct {
base *BaseLogger
}
// NewErrorLogger creates a new error logger instance
func NewErrorLogger() *ErrorLogger {
return &ErrorLogger{
base: GetBaseLogger("error"),
}
}
// 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)
el.base.Log(LogLevelError, contextualFormat, v...)
}
}
// 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 {
el.base.Log(LogLevelError, "%s: %v", message[0], err)
} else {
el.base.Log(LogLevelError, "Error: %v", err)
}
}
}
// LogWithStackTrace logs an error with stack trace
func (el *ErrorLogger) LogWithStackTrace(format string, v ...interface{}) {
if el.base != nil {
// Get stack trace
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
stackTrace := string(buf[:n])
msg := fmt.Sprintf(format, v...)
el.base.Log(LogLevelError, "%s\nStack Trace:\n%s", msg, stackTrace)
}
}
// 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...)
panic(fmt.Sprintf(format, v...))
}
}
// Global error logger instance
var errorLogger *ErrorLogger
// GetErrorLogger returns the global error logger instance
func GetErrorLogger() *ErrorLogger {
if errorLogger == nil {
errorLogger = NewErrorLogger()
}
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...)
}

125
local/utl/logging/info.go Normal file
View File

@@ -0,0 +1,125 @@
package logging
import (
"fmt"
)
// InfoLogger handles info-level logging
type InfoLogger struct {
base *BaseLogger
}
// NewInfoLogger creates a new info logger instance
func NewInfoLogger() *InfoLogger {
return &InfoLogger{
base: GetBaseLogger("info"),
}
}
// 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)
il.base.Log(LogLevelInfo, contextualFormat, v...)
}
}
// 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
// GetInfoLogger returns the global info logger instance
func GetInfoLogger() *InfoLogger {
if infoLogger == nil {
infoLogger = NewInfoLogger()
}
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)
}

View File

@@ -2,27 +2,26 @@ package logging
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)
var (
logger *Logger
once sync.Once
timeFormat = "2006-01-02 15:04:05.000"
// Legacy logger for backward compatibility
logger *Logger
once sync.Once
)
// Logger maintains backward compatibility with existing code
type Logger struct {
file *os.File
logger *log.Logger
base *BaseLogger
errorLogger *ErrorLogger
warnLogger *WarnLogger
infoLogger *InfoLogger
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() {
@@ -32,119 +31,183 @@ func Initialize() (*Logger, error) {
}
func newLogger() (*Logger, 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.log", time.Now().Format("2006-01-02")))
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
// Initialize the base logger
baseLogger, err := InitializeBase("log")
if err != nil {
return nil, fmt.Errorf("failed to open log file: %v", err)
return nil, err
}
// Create multi-writer for both file and console
multiWriter := io.MultiWriter(file, os.Stdout)
// Create logger with custom prefix
// Create the legacy logger wrapper
logger := &Logger{
file: file,
logger: log.New(multiWriter, "", 0),
base: baseLogger,
errorLogger: NewErrorLogger(),
warnLogger: NewWarnLogger(),
infoLogger: NewInfoLogger(),
debugLogger: NewDebugLogger(),
}
return logger, nil
}
// Close closes the logger
func (l *Logger) Close() error {
if l.file != nil {
return l.file.Close()
if l.base != nil {
return l.base.Close()
}
return nil
}
// Legacy methods for backward compatibility
func (l *Logger) log(level, format string, v ...interface{}) {
// Get caller info
_, 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),
level,
file,
line,
msg,
)
l.logger.Println(logLine)
if l.base != nil {
l.base.LogWithCaller(LogLevel(level), 3, format, v...)
}
}
func (l *Logger) Info(format string, v ...interface{}) {
l.log("INFO", format, v...)
if l.infoLogger != nil {
l.infoLogger.Log(format, v...)
}
}
func (l *Logger) Error(format string, v ...interface{}) {
l.log("ERROR", format, v...)
if l.errorLogger != nil {
l.errorLogger.Log(format, v...)
}
}
func (l *Logger) Warn(format string, v ...interface{}) {
l.log("WARN", format, v...)
if l.warnLogger != nil {
l.warnLogger.Log(format, v...)
}
}
func (l *Logger) Debug(format string, v ...interface{}) {
l.log("DEBUG", format, v...)
if l.debugLogger != nil {
l.debugLogger.Log(format, v...)
}
}
func (l *Logger) Panic(format string) {
l.Panic("PANIC " + format)
if l.errorLogger != nil {
l.errorLogger.LogFatal(format)
}
}
// Global convenience functions
func Info(format string, v ...interface{}) {
// 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...)
}
}
func Error(format string, v ...interface{}) {
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...)
}
}
func Warn(format string, v ...interface{}) {
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...)
}
}
func Debug(format string, v ...interface{}) {
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...)
}
}
func Panic(format string) {
if logger != nil {
logger.Panic(format)
} else {
// Fallback to direct logger if legacy logger not initialized
GetErrorLogger().LogFatal(format)
}
}
// RecoverAndLog recovers from panics and logs them
func RecoverAndLog() {
if logger != nil {
logger.Info("Recovering from panic")
if r := recover(); r != nil {
// Get stack trace
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
stackTrace := string(buf[:n])
// Enhanced logging convenience functions
// These provide direct access to specialized logging functions
logger.log("PANIC", "Recovered from panic: %v\nStack Trace:\n%s", r, stackTrace)
}
// 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()
}
}
return logger
}
// InitializeLogging initializes all logging components
func InitializeLogging() error {
// Initialize base logger
_, err := InitializeBase("log")
if err != nil {
return fmt.Errorf("failed to initialize base logger: %v", err)
}
// Initialize legacy logger for backward compatibility
_, err = Initialize()
if err != nil {
return fmt.Errorf("failed to initialize legacy logger: %v", err)
}
// Log successful initialization
Info("Logging system initialized successfully")
return nil
}

93
local/utl/logging/warn.go Normal file
View File

@@ -0,0 +1,93 @@
package logging
import (
"fmt"
)
// WarnLogger handles warn-level logging
type WarnLogger struct {
base *BaseLogger
}
// NewWarnLogger creates a new warn logger instance
func NewWarnLogger() *WarnLogger {
return &WarnLogger{
base: GetBaseLogger("warn"),
}
}
// 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)
wl.base.Log(LogLevelWarn, contextualFormat, v...)
}
}
// LogDeprecation logs a deprecation warning
func (wl *WarnLogger) LogDeprecation(feature string, alternative string) {
if wl.base != nil {
if alternative != "" {
wl.base.Log(LogLevelWarn, "DEPRECATED: %s is deprecated, use %s instead", feature, alternative)
} else {
wl.base.Log(LogLevelWarn, "DEPRECATED: %s is deprecated", feature)
}
}
}
// 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
// GetWarnLogger returns the global warn logger instance
func GetWarnLogger() *WarnLogger {
if warnLogger == nil {
warnLogger = NewWarnLogger()
}
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)
}