init bootstrap

This commit is contained in:
Fran Jurmanović
2025-07-06 15:02:09 +02:00
commit 016728532c
47 changed files with 8894 additions and 0 deletions

37
local/api/api.go Normal file
View File

@@ -0,0 +1,37 @@
package api
import (
"omega-server/local/controller"
"omega-server/local/utl/common"
"omega-server/local/utl/configs"
"omega-server/local/utl/logging"
"github.com/gofiber/fiber/v2"
"go.uber.org/dig"
)
// Routes
// Initializes web api controllers and its corresponding routes.
//
// Args:
// *fiber.App: Fiber Application
func Init(di *dig.Container, app *fiber.App) {
// Protected routes
groups := app.Group(configs.Prefix)
routeGroups := &common.RouteGroups{
API: groups.Group("/api"),
Auth: groups.Group("/auth"),
Users: groups.Group("/membership"),
}
err := di.Provide(func() *common.RouteGroups {
return routeGroups
})
if err != nil {
logging.Panic("unable to bind routes")
}
controller.InitializeControllers(di)
}

47
local/controller/api.go Normal file
View File

@@ -0,0 +1,47 @@
package controller
import (
"omega-server/local/middleware"
"omega-server/local/service"
"omega-server/local/utl/common"
"omega-server/local/utl/error_handler"
"github.com/gofiber/fiber/v2"
)
type ApiController struct {
service *service.ApiService
errorHandler *error_handler.ControllerErrorHandler
}
// NewApiController
// Initializes ApiController.
//
// Args:
// *services.ApiService: API service
// *Fiber.RouterGroup: Fiber Router Group
// Returns:
// *ApiController: Controller for "api" interactions
func NewApiController(as *service.ApiService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *ApiController {
ac := &ApiController{
service: as,
errorHandler: error_handler.NewControllerErrorHandler(),
}
apiGroup := routeGroups.API
apiGroup.Use(auth.Authenticate)
apiGroup.Get("/", ac.getFirst)
return ac
}
// getFirst returns API
//
// @Summary Return API
// @Description Return API
// @Tags api
// @Success 200 {array} string
// @Router /v1/api [get]
func (ac *ApiController) getFirst(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}

View File

@@ -0,0 +1,32 @@
package controller
import (
"omega-server/local/middleware"
"omega-server/local/service"
"omega-server/local/utl/logging"
"go.uber.org/dig"
)
// InitializeControllers
// Initializes Dependency Injection modules and registers controllers
//
// Args:
// *dig.Container: Dig Container
func InitializeControllers(c *dig.Container) {
service.InitializeServices(c)
if err := c.Provide(middleware.NewAuthMiddleware); err != nil {
logging.Panic("unable to initialize auth middleware")
}
err := c.Invoke(NewApiController)
if err != nil {
logging.Panic("unable to initialize api controller")
}
err = c.Invoke(NewMembershipController)
if err != nil {
logging.Panic("unable to initialize membership controller")
}
}

View File

@@ -0,0 +1,180 @@
package controller
import (
"context"
"fmt"
"omega-server/local/middleware"
"omega-server/local/service"
"omega-server/local/utl/common"
"omega-server/local/utl/error_handler"
"omega-server/local/utl/logging"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// MembershipController handles API requests for membership.
type MembershipController struct {
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,
errorHandler: error_handler.NewControllerErrorHandler(),
}
// Setup initial data for membership
if err := service.SetupInitialData(context.Background()); err != nil {
logging.Panic(fmt.Sprintf("failed to setup initial data: %v", err))
}
routeGroups.Auth.Post("/login", mc.Login)
usersGroup := routeGroups.Users
usersGroup.Use(mc.auth.Authenticate)
usersGroup.Post("/", mc.CreateUser)
usersGroup.Get("/", mc.ListUsers)
usersGroup.Get("/roles", mc.GetRoles)
usersGroup.Get("/:id", mc.GetUser)
usersGroup.Put("/:id", mc.UpdateUser)
usersGroup.Delete("/:id", mc.DeleteUser)
routeGroups.Auth.Get("/me", mc.auth.Authenticate, mc.GetMe)
return mc
}
// Login handles user login.
func (c *MembershipController) Login(ctx *fiber.Ctx) error {
type request struct {
Username string `json:"username"`
Password string `json:"password"`
}
var req request
if err := ctx.BodyParser(&req); err != nil {
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 c.errorHandler.HandleAuthError(ctx, err)
}
return ctx.JSON(fiber.Map{"token": token})
}
// CreateUser creates a new user.
func (mc *MembershipController) CreateUser(c *fiber.Ctx) error {
type request struct {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
}
var req request
if err := c.BodyParser(&req); err != nil {
return mc.errorHandler.HandleParsingError(c, err)
}
user, err := mc.service.CreateUser(c.UserContext(), req.Username, req.Password, req.Role)
if err != nil {
return mc.errorHandler.HandleServiceError(c, err)
}
return c.JSON(user)
}
// ListUsers lists all users.
func (mc *MembershipController) ListUsers(c *fiber.Ctx) error {
users, err := mc.service.ListUsers(c.UserContext())
if err != nil {
return mc.errorHandler.HandleServiceError(c, err)
}
return c.JSON(users)
}
// GetUser gets a single user by ID.
func (mc *MembershipController) GetUser(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return mc.errorHandler.HandleUUIDError(c, "user ID")
}
user, err := mc.service.GetUser(c.UserContext(), id)
if err != nil {
return mc.errorHandler.HandleNotFoundError(c, "User")
}
return c.JSON(user)
}
// GetMe returns the currently authenticated user's details.
func (mc *MembershipController) GetMe(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
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 mc.errorHandler.HandleNotFoundError(c, "User")
}
// Sanitize the user object to not expose password
user.PasswordHash = ""
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 mc.errorHandler.HandleUUIDError(c, "user ID")
}
var req service.UpdateUserRequest
if err := c.BodyParser(&req); err != nil {
return mc.errorHandler.HandleParsingError(c, err)
}
user, err := mc.service.UpdateUser(c.UserContext(), id, req)
if err != nil {
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)
}

243
local/middleware/auth.go Normal file
View File

@@ -0,0 +1,243 @@
package middleware
import (
"context"
"fmt"
"omega-server/local/middleware/security"
"omega-server/local/service"
"omega-server/local/utl/cache"
"omega-server/local/utl/jwt"
"omega-server/local/utl/logging"
"strings"
"time"
"github.com/gofiber/fiber/v2"
)
// CachedUserInfo holds cached user authentication and permission data
type CachedUserInfo struct {
UserID string
Username string
Roles []string
RoleNames []string
Permissions map[string]bool
CachedAt time.Time
}
// AuthMiddleware provides authentication and permission middleware.
type AuthMiddleware struct {
membershipService service.MembershipServiceInterface
cache *cache.InMemoryCache
securityMW *security.SecurityMiddleware
}
// NewAuthMiddleware creates a new AuthMiddleware.
func NewAuthMiddleware(ms service.MembershipServiceInterface, cache *cache.InMemoryCache) *AuthMiddleware {
auth := &AuthMiddleware{
membershipService: ms,
cache: cache,
securityMW: security.NewSecurityMiddleware(),
}
// Set up bidirectional relationship for cache invalidation
ms.SetCacheInvalidator(auth)
return auth
}
// Authenticate is a middleware for JWT authentication with enhanced security.
func (m *AuthMiddleware) Authenticate(ctx *fiber.Ctx) error {
// Log authentication attempt
ip := ctx.IP()
userAgent := ctx.Get("User-Agent")
authHeader := ctx.Get("Authorization")
if authHeader == "" {
logging.Error("Authentication failed: missing Authorization header from IP %s", ip)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Missing or malformed JWT",
})
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
logging.Error("Authentication failed: malformed Authorization header from IP %s", ip)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Missing or malformed JWT",
})
}
// Validate token length to prevent potential attacks
token := parts[1]
if len(token) < 10 || len(token) > 2048 {
logging.Error("Authentication failed: invalid token length from IP %s", ip)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid or expired JWT",
})
}
claims, err := jwt.ValidateToken(token)
if err != nil {
logging.Error("Authentication failed: invalid token from IP %s, User-Agent: %s, Error: %v", ip, userAgent, err)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid or expired JWT",
})
}
// Additional security: validate user ID format
if claims.UserID == "" || len(claims.UserID) < 10 {
logging.Error("Authentication failed: invalid user ID in token from IP %s", ip)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid or expired JWT",
})
}
// Preload and cache user info to avoid database queries on permission checks
userInfo, err := m.getCachedUserInfo(ctx.UserContext(), claims.UserID)
if err != nil {
logging.Error("Authentication failed: unable to load user info for %s from IP %s: %v", claims.UserID, ip, err)
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Invalid or expired JWT",
})
}
ctx.Locals("userID", claims.UserID)
ctx.Locals("userInfo", userInfo)
ctx.Locals("authTime", time.Now())
logging.InfoWithContext("AUTH", "User %s authenticated successfully from IP %s", claims.UserID, ip)
return ctx.Next()
}
// HasPermission is a middleware for checking user permissions with enhanced logging.
func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler {
return func(ctx *fiber.Ctx) error {
userID, ok := ctx.Locals("userID").(string)
if !ok {
logging.Error("Permission check failed: no user ID in context from IP %s", ctx.IP())
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Unauthorized",
})
}
// Validate permission parameter
if requiredPermission == "" {
logging.Error("Permission check failed: empty permission requirement")
return ctx.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Internal server error",
})
}
// Use cached user info from authentication step - no database queries needed
userInfo, ok := ctx.Locals("userInfo").(*CachedUserInfo)
if !ok {
logging.Error("Permission check failed: no cached user info in context from IP %s", ctx.IP())
return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "Unauthorized",
})
}
// Check if user has permission using cached data
has := m.hasPermissionFromCache(userInfo, requiredPermission)
if !has {
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.DebugWithContext("AUTH", "Permission granted: user %s has permission %s", userID, requiredPermission)
return ctx.Next()
}
}
// AuthRateLimit applies rate limiting specifically for authentication endpoints
func (m *AuthMiddleware) AuthRateLimit() fiber.Handler {
return m.securityMW.AuthRateLimit()
}
// RequireHTTPS redirects HTTP requests to HTTPS in production
func (m *AuthMiddleware) RequireHTTPS() fiber.Handler {
return func(ctx *fiber.Ctx) error {
if ctx.Protocol() != "https" && ctx.Get("X-Forwarded-Proto") != "https" {
// Allow HTTP in development/testing
if ctx.Hostname() != "localhost" && ctx.Hostname() != "127.0.0.1" {
httpsURL := "https://" + ctx.Hostname() + ctx.OriginalURL()
return ctx.Redirect(httpsURL, fiber.StatusMovedPermanently)
}
}
return ctx.Next()
}
}
// getCachedUserInfo retrieves and caches complete user information including permissions
func (m *AuthMiddleware) getCachedUserInfo(ctx context.Context, userID string) (*CachedUserInfo, error) {
cacheKey := fmt.Sprintf("userinfo:%s", userID)
// Try cache first
if cached, found := m.cache.Get(cacheKey); found {
if userInfo, ok := cached.(*CachedUserInfo); ok {
logging.DebugWithContext("AUTH_CACHE", "User info for %s found in cache", userID)
return userInfo, nil
}
}
// Cache miss - load from database
user, err := m.membershipService.GetUserWithPermissions(ctx, userID)
if err != nil {
return nil, err
}
// Build permission map for fast lookups
permissions := make(map[string]bool)
roleNames := make([]string, len(user.Roles))
for i, role := range user.Roles {
roleNames[i] = role.Name
for _, permission := range role.Permissions {
permissions[permission.Name] = true
}
}
userInfo := &CachedUserInfo{
UserID: userID,
Username: user.Username,
Roles: roleNames,
RoleNames: roleNames,
Permissions: permissions,
CachedAt: time.Now(),
}
// Cache for 15 minutes
m.cache.Set(cacheKey, userInfo, 15*time.Minute)
logging.DebugWithContext("AUTH_CACHE", "User info for %s cached with %d permissions", userID, len(permissions))
return userInfo, nil
}
// hasPermissionFromCache checks permissions using cached user info (no database queries)
func (m *AuthMiddleware) hasPermissionFromCache(userInfo *CachedUserInfo, permission string) bool {
// Super Admin and Admin have all permissions
for _, roleName := range userInfo.RoleNames {
if roleName == "Super Admin" || roleName == "Admin" {
return true
}
}
// Check specific permission in cached map
return userInfo.Permissions[permission]
}
// InvalidateUserPermissions removes cached user info for a user
func (m *AuthMiddleware) InvalidateUserPermissions(userID string) {
cacheKey := fmt.Sprintf("userinfo:%s", userID)
m.cache.Delete(cacheKey)
logging.InfoWithContext("AUTH_CACHE", "User info cache invalidated for user %s", userID)
}
// InvalidateAllUserPermissions clears all cached user info (useful for role/permission changes)
func (m *AuthMiddleware) InvalidateAllUserPermissions() {
// This would need to be implemented based on your cache interface
// For now, just log that invalidation was requested
logging.InfoWithContext("AUTH_CACHE", "All user info caches invalidation requested")
}

View File

@@ -0,0 +1,28 @@
package middleware
import "context"
// ExportedCachedUserInfo exports CachedUserInfo for testing
type ExportedCachedUserInfo = CachedUserInfo
// GetCachedUserInfo exports the internal getCachedUserInfo method for testing
func (m *AuthMiddleware) GetCachedUserInfo(ctx context.Context, userID string) (*CachedUserInfo, error) {
return m.getCachedUserInfo(ctx, userID)
}
// TestExports provides test-only functionality for AuthMiddleware
type TestExports struct {
AuthMiddleware *AuthMiddleware
}
// NewTestExports creates a new TestExports instance
func NewTestExports(auth *AuthMiddleware) *TestExports {
return &TestExports{
AuthMiddleware: auth,
}
}
// HasPermissionFromCache exports the internal hasPermissionFromCache method for testing
func (t *TestExports) HasPermissionFromCache(userInfo *CachedUserInfo, permission string) bool {
return t.AuthMiddleware.hasPermissionFromCache(userInfo, permission)
}

View File

@@ -0,0 +1,69 @@
package logging
import (
"omega-server/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,351 @@
package security
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
)
// RateLimiter stores rate limiting information
type RateLimiter struct {
requests map[string][]time.Time
mutex sync.RWMutex
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter() *RateLimiter {
rl := &RateLimiter{
requests: make(map[string][]time.Time),
}
// Clean up old entries every 5 minutes
go rl.cleanup()
return rl
}
// cleanup removes old entries from the rate limiter
func (rl *RateLimiter) cleanup() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
rl.mutex.Lock()
now := time.Now()
for key, times := range rl.requests {
// Remove entries older than 1 hour
filtered := make([]time.Time, 0, len(times))
for _, t := range times {
if now.Sub(t) < time.Hour {
filtered = append(filtered, t)
}
}
if len(filtered) == 0 {
delete(rl.requests, key)
} else {
rl.requests[key] = filtered
}
}
rl.mutex.Unlock()
}
}
// SecurityMiddleware provides comprehensive security middleware
type SecurityMiddleware struct {
rateLimiter *RateLimiter
}
// NewSecurityMiddleware creates a new security middleware
func NewSecurityMiddleware() *SecurityMiddleware {
return &SecurityMiddleware{
rateLimiter: NewRateLimiter(),
}
}
// SecurityHeaders adds security headers to responses
func (sm *SecurityMiddleware) SecurityHeaders() fiber.Handler {
return func(c *fiber.Ctx) error {
// Prevent MIME type sniffing
c.Set("X-Content-Type-Options", "nosniff")
// Prevent clickjacking
c.Set("X-Frame-Options", "DENY")
// Enable XSS protection
c.Set("X-XSS-Protection", "1; mode=block")
// Prevent referrer leakage
c.Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Content Security Policy
c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'")
// Permissions Policy
c.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), interest-cohort=()")
return c.Next()
}
}
// RateLimit implements rate limiting for API endpoints
func (sm *SecurityMiddleware) RateLimit(maxRequests int, duration time.Duration) fiber.Handler {
return func(c *fiber.Ctx) error {
ip := c.IP()
key := fmt.Sprintf("rate_limit:%s", ip)
sm.rateLimiter.mutex.Lock()
defer sm.rateLimiter.mutex.Unlock()
now := time.Now()
requests := sm.rateLimiter.requests[key]
// Remove requests older than duration
filtered := make([]time.Time, 0, len(requests))
for _, t := range requests {
if now.Sub(t) < duration {
filtered = append(filtered, t)
}
}
// Check if limit is exceeded
if len(filtered) >= maxRequests {
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "Rate limit exceeded",
"retry_after": duration.Seconds(),
})
}
// Add current request
filtered = append(filtered, now)
sm.rateLimiter.requests[key] = filtered
return c.Next()
}
}
// AuthRateLimit implements stricter rate limiting for authentication endpoints
func (sm *SecurityMiddleware) AuthRateLimit() fiber.Handler {
return func(c *fiber.Ctx) error {
ip := c.IP()
userAgent := c.Get("User-Agent")
key := fmt.Sprintf("%s:%s", ip, userAgent)
sm.rateLimiter.mutex.Lock()
defer sm.rateLimiter.mutex.Unlock()
now := time.Now()
requests := sm.rateLimiter.requests[key]
// Remove requests older than 15 minutes
filtered := make([]time.Time, 0, len(requests))
for _, t := range requests {
if now.Sub(t) < 15*time.Minute {
filtered = append(filtered, t)
}
}
// Check if limit is exceeded (5 requests per 15 minutes for auth)
if len(filtered) >= 5 {
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": "Too many authentication attempts",
"retry_after": 900, // 15 minutes
})
}
// Add current request
filtered = append(filtered, now)
sm.rateLimiter.requests[key] = filtered
return c.Next()
}
}
// InputSanitization sanitizes user input to prevent XSS and injection attacks
func (sm *SecurityMiddleware) InputSanitization() fiber.Handler {
return func(c *fiber.Ctx) error {
// Sanitize query parameters
c.Request().URI().QueryArgs().VisitAll(func(key, value []byte) {
sanitized := sanitizeInput(string(value))
c.Request().URI().QueryArgs().Set(string(key), sanitized)
})
// Store original body for processing
if c.Method() == "POST" || c.Method() == "PUT" || c.Method() == "PATCH" {
body := c.Body()
if len(body) > 0 {
// Basic sanitization - remove potentially dangerous patterns
sanitized := sanitizeInput(string(body))
c.Request().SetBodyString(sanitized)
}
}
return c.Next()
}
}
// sanitizeInput removes potentially dangerous patterns from input
func sanitizeInput(input string) string {
// Remove common XSS patterns
dangerous := []string{
"<script",
"</script>",
"javascript:",
"vbscript:",
"data:text/html",
"onload=",
"onerror=",
"onclick=",
"onmouseover=",
"onfocus=",
"onblur=",
"onchange=",
"onsubmit=",
"<iframe",
"<object",
"<embed",
"<link",
"<meta",
"<style",
}
result := strings.ToLower(input)
for _, pattern := range dangerous {
result = strings.ReplaceAll(result, pattern, "")
}
// If the sanitized version is very different, it might be malicious
if len(result) < len(input)/2 {
return ""
}
return input
}
// ValidateContentType ensures only expected content types are accepted
func (sm *SecurityMiddleware) ValidateContentType(allowedTypes ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
if c.Method() == "POST" || c.Method() == "PUT" || c.Method() == "PATCH" {
contentType := c.Get("Content-Type")
if contentType == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Content-Type header is required",
})
}
// Check if content type is allowed
allowed := false
for _, allowedType := range allowedTypes {
if strings.Contains(contentType, allowedType) {
allowed = true
break
}
}
if !allowed {
return c.Status(fiber.StatusUnsupportedMediaType).JSON(fiber.Map{
"error": "Unsupported content type",
})
}
}
return c.Next()
}
}
// ValidateUserAgent blocks requests with suspicious or missing user agents
func (sm *SecurityMiddleware) ValidateUserAgent() fiber.Handler {
suspiciousAgents := []string{
"sqlmap",
"nikto",
"nmap",
"masscan",
"gobuster",
"dirb",
"dirbuster",
"wpscan",
"curl/7.0", // Very old curl versions
"wget/1.0", // Very old wget versions
}
return func(c *fiber.Ctx) error {
userAgent := strings.ToLower(c.Get("User-Agent"))
// Block empty user agents
if userAgent == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "User-Agent header is required",
})
}
// Block suspicious user agents
for _, suspicious := range suspiciousAgents {
if strings.Contains(userAgent, suspicious) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"error": "Access denied",
})
}
}
return c.Next()
}
}
// RequestSizeLimit limits the size of incoming requests
func (sm *SecurityMiddleware) RequestSizeLimit(maxSize int) fiber.Handler {
return func(c *fiber.Ctx) error {
if c.Method() == "POST" || c.Method() == "PUT" || c.Method() == "PATCH" {
contentLength := c.Request().Header.ContentLength()
if contentLength > maxSize {
return c.Status(fiber.StatusRequestEntityTooLarge).JSON(fiber.Map{
"error": "Request too large",
"max_size": maxSize,
})
}
}
return c.Next()
}
}
// LogSecurityEvents logs security-related events
func (sm *SecurityMiddleware) LogSecurityEvents() fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
// Process request
err := c.Next()
// Log suspicious activity
status := c.Response().StatusCode()
if status == 401 || status == 403 || status == 429 {
duration := time.Since(start)
// In a real implementation, you would send this to your logging system
fmt.Printf("[SECURITY] %s %s %s %d %v %s\n",
time.Now().Format(time.RFC3339),
c.IP(),
c.Method(),
status,
duration,
c.Path(),
)
}
return err
}
}
// TimeoutMiddleware adds request timeout
func (sm *SecurityMiddleware) TimeoutMiddleware(timeout time.Duration) fiber.Handler {
return func(c *fiber.Ctx) error {
ctx, cancel := context.WithTimeout(c.UserContext(), timeout)
defer cancel()
c.SetUserContext(ctx)
return c.Next()
}
}

271
local/model/audit_log.go Normal file
View File

@@ -0,0 +1,271 @@
package model
import (
"encoding/json"
"strings"
"time"
"gorm.io/gorm"
)
// AuditLog represents an audit log entry in the system
type AuditLog struct {
BaseModel
UserID string `json:"userId" gorm:"type:varchar(36);index"`
Action string `json:"action" gorm:"not null;type:varchar(100);index"`
Resource string `json:"resource" gorm:"not null;type:varchar(100);index"`
ResourceID string `json:"resourceId" gorm:"type:varchar(36);index"`
Details map[string]interface{} `json:"details" gorm:"type:text"`
IPAddress string `json:"ipAddress" gorm:"type:varchar(45)"`
UserAgent string `json:"userAgent" gorm:"type:text"`
Success bool `json:"success" gorm:"default:true;index"`
ErrorMsg string `json:"errorMsg,omitempty" gorm:"type:text"`
Duration int64 `json:"duration,omitempty"` // Duration in milliseconds
SessionID string `json:"sessionId,omitempty" gorm:"type:varchar(255)"`
RequestID string `json:"requestId,omitempty" gorm:"type:varchar(255)"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// AuditLogCreateRequest represents the request to create a new audit log
type AuditLogCreateRequest struct {
UserID string `json:"userId"`
Action string `json:"action" validate:"required,max=100"`
Resource string `json:"resource" validate:"required,max=100"`
ResourceID string `json:"resourceId"`
Details map[string]interface{} `json:"details"`
IPAddress string `json:"ipAddress" validate:"max=45"`
UserAgent string `json:"userAgent"`
Success bool `json:"success"`
ErrorMsg string `json:"errorMsg"`
Duration int64 `json:"duration"`
SessionID string `json:"sessionId"`
RequestID string `json:"requestId"`
}
// AuditLogInfo represents public audit log information
type AuditLogInfo struct {
ID string `json:"id"`
UserID string `json:"userId"`
UserEmail string `json:"userEmail,omitempty"`
UserName string `json:"userName,omitempty"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID string `json:"resourceId"`
Details map[string]interface{} `json:"details"`
IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"`
Success bool `json:"success"`
ErrorMsg string `json:"errorMsg,omitempty"`
Duration int64 `json:"duration,omitempty"`
SessionID string `json:"sessionId,omitempty"`
RequestID string `json:"requestId,omitempty"`
DateCreated string `json:"dateCreated"`
}
// BeforeCreate is called before creating an audit log
func (al *AuditLog) BeforeCreate(tx *gorm.DB) error {
al.BaseModel.BeforeCreate()
// Normalize fields
al.Action = strings.ToLower(strings.TrimSpace(al.Action))
al.Resource = strings.ToLower(strings.TrimSpace(al.Resource))
al.IPAddress = strings.TrimSpace(al.IPAddress)
al.UserAgent = strings.TrimSpace(al.UserAgent)
return nil
}
// SetDetails sets the details field from a map
func (al *AuditLog) SetDetails(details map[string]interface{}) error {
al.Details = details
return nil
}
// GetDetails returns the details as a map
func (al *AuditLog) GetDetails() map[string]interface{} {
if al.Details == nil {
return make(map[string]interface{})
}
return al.Details
}
// SetDetailsFromJSON sets the details field from a JSON string
func (al *AuditLog) SetDetailsFromJSON(jsonStr string) error {
if jsonStr == "" {
al.Details = make(map[string]interface{})
return nil
}
var details map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &details); err != nil {
return err
}
al.Details = details
return nil
}
// GetDetailsAsJSON returns the details as a JSON string
func (al *AuditLog) GetDetailsAsJSON() (string, error) {
if al.Details == nil || len(al.Details) == 0 {
return "{}", nil
}
bytes, err := json.Marshal(al.Details)
if err != nil {
return "", err
}
return string(bytes), nil
}
// ToAuditLogInfo converts AuditLog to AuditLogInfo (public information)
func (al *AuditLog) ToAuditLogInfo() AuditLogInfo {
info := AuditLogInfo{
ID: al.ID,
UserID: al.UserID,
Action: al.Action,
Resource: al.Resource,
ResourceID: al.ResourceID,
Details: al.GetDetails(),
IPAddress: al.IPAddress,
UserAgent: al.UserAgent,
Success: al.Success,
ErrorMsg: al.ErrorMsg,
Duration: al.Duration,
SessionID: al.SessionID,
RequestID: al.RequestID,
DateCreated: al.DateCreated.Format("2006-01-02T15:04:05Z"),
}
// Include user information if available
if al.User != nil {
info.UserEmail = al.User.Email
info.UserName = al.User.Name
}
return info
}
// AddDetail adds a single detail to the details map
func (al *AuditLog) AddDetail(key string, value interface{}) {
if al.Details == nil {
al.Details = make(map[string]interface{})
}
al.Details[key] = value
}
// GetDetail gets a single detail from the details map
func (al *AuditLog) GetDetail(key string) (interface{}, bool) {
if al.Details == nil {
return nil, false
}
value, exists := al.Details[key]
return value, exists
}
// Common audit log actions
const (
AuditActionCreate = "create"
AuditActionRead = "read"
AuditActionUpdate = "update"
AuditActionDelete = "delete"
AuditActionLogin = "login"
AuditActionLogout = "logout"
AuditActionAccess = "access"
AuditActionExport = "export"
AuditActionImport = "import"
AuditActionConfig = "config"
)
// Common audit log resources
const (
AuditResourceUser = "user"
AuditResourceRole = "role"
AuditResourcePermission = "permission"
AuditResourceSystemConfig = "system_config"
AuditResourceAuth = "auth"
AuditResourceAPI = "api"
AuditResourceFile = "file"
AuditResourceDatabase = "database"
AuditResourceSystem = "system"
)
// CreateAuditLog creates a new audit log entry
func CreateAuditLog(userID, action, resource, resourceID string, success bool) *AuditLog {
auditLog := &AuditLog{
UserID: userID,
Action: action,
Resource: resource,
ResourceID: resourceID,
Success: success,
Details: make(map[string]interface{}),
}
auditLog.Init()
return auditLog
}
// CreateAuditLogWithDetails creates a new audit log entry with details
func CreateAuditLogWithDetails(userID, action, resource, resourceID string, success bool, details map[string]interface{}) *AuditLog {
auditLog := CreateAuditLog(userID, action, resource, resourceID, success)
auditLog.Details = details
return auditLog
}
// CreateAuditLogWithError creates a new audit log entry for an error
func CreateAuditLogWithError(userID, action, resource, resourceID, errorMsg string) *AuditLog {
auditLog := CreateAuditLog(userID, action, resource, resourceID, false)
auditLog.ErrorMsg = errorMsg
return auditLog
}
// SetRequestInfo sets request-related information
func (al *AuditLog) SetRequestInfo(ipAddress, userAgent, sessionID, requestID string) {
al.IPAddress = ipAddress
al.UserAgent = userAgent
al.SessionID = sessionID
al.RequestID = requestID
}
// SetDuration sets the operation duration
func (al *AuditLog) SetDuration(start time.Time) {
al.Duration = time.Since(start).Milliseconds()
}
// IsSuccess returns whether the audit log represents a successful operation
func (al *AuditLog) IsSuccess() bool {
return al.Success
}
// IsFailure returns whether the audit log represents a failed operation
func (al *AuditLog) IsFailure() bool {
return !al.Success
}
// GetActionDescription returns a human-readable description of the action
func (al *AuditLog) GetActionDescription() string {
switch al.Action {
case AuditActionCreate:
return "Created " + al.Resource
case AuditActionRead:
return "Viewed " + al.Resource
case AuditActionUpdate:
return "Updated " + al.Resource
case AuditActionDelete:
return "Deleted " + al.Resource
case AuditActionLogin:
return "Logged in"
case AuditActionLogout:
return "Logged out"
case AuditActionAccess:
return "Accessed " + al.Resource
case AuditActionExport:
return "Exported " + al.Resource
case AuditActionImport:
return "Imported " + al.Resource
case AuditActionConfig:
return "Configured " + al.Resource
default:
return strings.Title(al.Action) + " " + al.Resource
}
}

110
local/model/base.go Normal file
View File

@@ -0,0 +1,110 @@
package model
import (
"time"
"github.com/google/uuid"
)
// BaseModel provides common fields for all database models
type BaseModel struct {
ID string `json:"id" gorm:"primary_key;type:varchar(36)"`
DateCreated time.Time `json:"dateCreated" gorm:"not null"`
DateUpdated time.Time `json:"dateUpdated" gorm:"not null"`
}
// Init initializes base model with DateCreated, DateUpdated, and ID values
func (bm *BaseModel) Init() {
now := time.Now().UTC()
bm.ID = uuid.NewString()
bm.DateCreated = now
bm.DateUpdated = now
}
// UpdateTimestamp updates the DateUpdated field
func (bm *BaseModel) UpdateTimestamp() {
bm.DateUpdated = time.Now().UTC()
}
// BeforeCreate is a GORM hook that runs before creating a record
func (bm *BaseModel) BeforeCreate() error {
if bm.ID == "" {
bm.Init()
}
return nil
}
// BeforeUpdate is a GORM hook that runs before updating a record
func (bm *BaseModel) BeforeUpdate() error {
bm.UpdateTimestamp()
return nil
}
// FilteredResponse represents a paginated response
type FilteredResponse struct {
Items interface{} `json:"items"`
Params
}
// MessageResponse represents a simple message response
type MessageResponse struct {
Message string `json:"message"`
Success bool `json:"success"`
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
Details map[string]interface{} `json:"details,omitempty"`
Code int `json:"code,omitempty"`
}
// Params represents pagination and filtering parameters
type Params struct {
SortBy string `json:"sortBy" query:"sortBy"`
SortOrder string `json:"sortOrder" query:"sortOrder"`
Page int `json:"page" query:"page"`
Limit int `json:"limit" query:"limit"`
Search string `json:"search" query:"search"`
Filter string `json:"filter" query:"filter"`
TotalRecords int64 `json:"totalRecords"`
TotalPages int `json:"totalPages"`
}
// DefaultParams returns default pagination parameters
func DefaultParams() Params {
return Params{
Page: 1,
Limit: 10,
SortBy: "dateCreated",
SortOrder: "desc",
}
}
// Validate validates and sets default values for parameters
func (p *Params) Validate() {
if p.Page < 1 {
p.Page = 1
}
if p.Limit < 1 || p.Limit > 100 {
p.Limit = 10
}
if p.SortBy == "" {
p.SortBy = "dateCreated"
}
if p.SortOrder != "asc" && p.SortOrder != "desc" {
p.SortOrder = "desc"
}
}
// Offset calculates the offset for database queries
func (p *Params) Offset() int {
return (p.Page - 1) * p.Limit
}
// CalculateTotalPages calculates total pages based on total records
func (p *Params) CalculateTotalPages() {
if p.Limit > 0 {
p.TotalPages = int((p.TotalRecords + int64(p.Limit) - 1) / int64(p.Limit))
}
}

View File

@@ -0,0 +1,228 @@
package model
import (
"gorm.io/gorm"
)
// MembershipFilter represents filters for membership-related queries
type MembershipFilter struct {
Params
// User-specific filters
Username string `json:"username" query:"username"`
Email string `json:"email" query:"email"`
Active *bool `json:"active" query:"active"`
EmailVerified *bool `json:"emailVerified" query:"emailVerified"`
RoleID string `json:"roleId" query:"roleId"`
RoleName string `json:"roleName" query:"roleName"`
// Role-specific filters
RoleActive *bool `json:"roleActive" query:"roleActive"`
RoleSystem *bool `json:"roleSystem" query:"roleSystem"`
// Permission-specific filters
PermissionName string `json:"permissionName" query:"permissionName"`
PermissionCategory string `json:"permissionCategory" query:"permissionCategory"`
PermissionActive *bool `json:"permissionActive" query:"permissionActive"`
PermissionSystem *bool `json:"permissionSystem" query:"permissionSystem"`
// Date range filters
CreatedAfter string `json:"createdAfter" query:"createdAfter"`
CreatedBefore string `json:"createdBefore" query:"createdBefore"`
UpdatedAfter string `json:"updatedAfter" query:"updatedAfter"`
UpdatedBefore string `json:"updatedBefore" query:"updatedBefore"`
// Relationship filters
WithRoles bool `json:"withRoles" query:"withRoles"`
WithPermissions bool `json:"withPermissions" query:"withPermissions"`
WithUsers bool `json:"withUsers" query:"withUsers"`
}
// ApplyFilter applies the filter conditions to a GORM query
func (f *MembershipFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
if f == nil {
return query
}
// Apply search across multiple fields
if f.Search != "" {
query = query.Where("username LIKE ? OR email LIKE ? OR name LIKE ?",
"%"+f.Search+"%", "%"+f.Search+"%", "%"+f.Search+"%")
}
// User-specific filters
if f.Username != "" {
query = query.Where("username = ?", f.Username)
}
if f.Email != "" {
query = query.Where("email = ?", f.Email)
}
if f.Active != nil {
query = query.Where("active = ?", *f.Active)
}
if f.EmailVerified != nil {
query = query.Where("email_verified = ?", *f.EmailVerified)
}
if f.RoleID != "" {
query = query.Joins("JOIN user_roles ON users.id = user_roles.user_id").
Where("user_roles.role_id = ?", f.RoleID)
}
if f.RoleName != "" {
query = query.Joins("JOIN user_roles ON users.id = user_roles.user_id").
Joins("JOIN roles ON user_roles.role_id = roles.id").
Where("roles.name = ?", f.RoleName)
}
// Role-specific filters
if f.RoleActive != nil {
query = query.Where("active = ?", *f.RoleActive)
}
if f.RoleSystem != nil {
query = query.Where("system = ?", *f.RoleSystem)
}
// Permission-specific filters
if f.PermissionName != "" {
query = query.Where("name = ?", f.PermissionName)
}
if f.PermissionCategory != "" {
query = query.Where("category = ?", f.PermissionCategory)
}
if f.PermissionActive != nil {
query = query.Where("active = ?", *f.PermissionActive)
}
if f.PermissionSystem != nil {
query = query.Where("system = ?", *f.PermissionSystem)
}
// Date range filters
if f.CreatedAfter != "" {
query = query.Where("date_created >= ?", f.CreatedAfter)
}
if f.CreatedBefore != "" {
query = query.Where("date_created <= ?", f.CreatedBefore)
}
if f.UpdatedAfter != "" {
query = query.Where("date_updated >= ?", f.UpdatedAfter)
}
if f.UpdatedBefore != "" {
query = query.Where("date_updated <= ?", f.UpdatedBefore)
}
// Relationship preloading
if f.WithRoles {
query = query.Preload("Roles")
}
if f.WithPermissions {
query = query.Preload("Permissions")
}
if f.WithUsers {
query = query.Preload("Users")
}
return query
}
// Pagination returns the offset and limit for pagination
func (f *MembershipFilter) Pagination() (offset, limit int) {
if f == nil {
return 0, 10
}
f.Validate()
return f.Offset(), f.Limit
}
// GetSorting returns the sorting field and direction
func (f *MembershipFilter) GetSorting() (field string, desc bool) {
if f == nil {
return "date_created", true
}
f.Validate()
// Map common sort fields to database column names
switch f.SortBy {
case "dateCreated":
field = "date_created"
case "dateUpdated":
field = "date_updated"
case "username":
field = "username"
case "email":
field = "email"
case "name":
field = "name"
case "active":
field = "active"
default:
field = "date_created"
}
desc = f.SortOrder == "desc"
return field, desc
}
// NewMembershipFilter creates a new MembershipFilter with default values
func NewMembershipFilter() *MembershipFilter {
return &MembershipFilter{
Params: DefaultParams(),
}
}
// SetUserFilters sets user-specific filters
func (f *MembershipFilter) SetUserFilters(username, email string, active, emailVerified *bool) *MembershipFilter {
f.Username = username
f.Email = email
f.Active = active
f.EmailVerified = emailVerified
return f
}
// SetRoleFilters sets role-specific filters
func (f *MembershipFilter) SetRoleFilters(roleID, roleName string, roleActive, roleSystem *bool) *MembershipFilter {
f.RoleID = roleID
f.RoleName = roleName
f.RoleActive = roleActive
f.RoleSystem = roleSystem
return f
}
// SetPermissionFilters sets permission-specific filters
func (f *MembershipFilter) SetPermissionFilters(name, category string, active, system *bool) *MembershipFilter {
f.PermissionName = name
f.PermissionCategory = category
f.PermissionActive = active
f.PermissionSystem = system
return f
}
// SetDateRangeFilters sets date range filters
func (f *MembershipFilter) SetDateRangeFilters(createdAfter, createdBefore, updatedAfter, updatedBefore string) *MembershipFilter {
f.CreatedAfter = createdAfter
f.CreatedBefore = createdBefore
f.UpdatedAfter = updatedAfter
f.UpdatedBefore = updatedBefore
return f
}
// SetPreloads sets which relationships to preload
func (f *MembershipFilter) SetPreloads(withRoles, withPermissions, withUsers bool) *MembershipFilter {
f.WithRoles = withRoles
f.WithPermissions = withPermissions
f.WithUsers = withUsers
return f
}

276
local/model/permission.go Normal file
View File

@@ -0,0 +1,276 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// Permission represents a permission in the system
type Permission struct {
BaseModel
Name string `json:"name" gorm:"unique;not null;type:varchar(100)"`
Description string `json:"description" gorm:"type:text"`
Category string `json:"category" gorm:"type:varchar(50)"`
Active bool `json:"active" gorm:"default:true"`
System bool `json:"system" gorm:"default:false"` // System permissions cannot be deleted
Roles []Role `json:"-" gorm:"many2many:role_permissions;"`
}
// PermissionCreateRequest represents the request to create a new permission
type PermissionCreateRequest struct {
Name string `json:"name" validate:"required,min=3,max=100"`
Description string `json:"description" validate:"max=500"`
Category string `json:"category" validate:"required,max=50"`
}
// PermissionUpdateRequest represents the request to update a permission
type PermissionUpdateRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=100"`
Description *string `json:"description,omitempty" validate:"omitempty,max=500"`
Category *string `json:"category,omitempty" validate:"omitempty,max=50"`
Active *bool `json:"active,omitempty"`
}
// PermissionInfo represents public permission information
type PermissionInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Active bool `json:"active"`
System bool `json:"system"`
RoleCount int64 `json:"roleCount"`
DateCreated string `json:"dateCreated"`
}
// BeforeCreate is called before creating a permission
func (p *Permission) BeforeCreate(tx *gorm.DB) error {
p.BaseModel.BeforeCreate()
// Normalize fields
p.Name = strings.ToLower(strings.TrimSpace(p.Name))
p.Description = strings.TrimSpace(p.Description)
p.Category = strings.ToLower(strings.TrimSpace(p.Category))
return p.Validate()
}
// BeforeUpdate is called before updating a permission
func (p *Permission) BeforeUpdate(tx *gorm.DB) error {
p.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if p.Name != "" {
p.Name = strings.ToLower(strings.TrimSpace(p.Name))
}
if p.Description != "" {
p.Description = strings.TrimSpace(p.Description)
}
if p.Category != "" {
p.Category = strings.ToLower(strings.TrimSpace(p.Category))
}
return p.Validate()
}
// BeforeDelete is called before deleting a permission
func (p *Permission) BeforeDelete(tx *gorm.DB) error {
if p.System {
return errors.New("system permissions cannot be deleted")
}
// Check if permission is assigned to any roles
var roleCount int64
if err := tx.Model(&Role{}).Where("permissions.id = ?", p.ID).Joins("JOIN role_permissions ON roles.id = role_permissions.role_id").Count(&roleCount).Error; err != nil {
return err
}
if roleCount > 0 {
return errors.New("cannot delete permission that is assigned to roles")
}
return nil
}
// Validate validates permission data
func (p *Permission) Validate() error {
if p.Name == "" {
return errors.New("permission name is required")
}
if len(p.Name) < 3 || len(p.Name) > 100 {
return errors.New("permission name must be between 3 and 100 characters")
}
if !isValidPermissionName(p.Name) {
return errors.New("permission name must follow the format 'resource:action' (e.g., 'user:create')")
}
if p.Category == "" {
return errors.New("permission category is required")
}
if len(p.Category) > 50 {
return errors.New("permission category must not exceed 50 characters")
}
if len(p.Description) > 500 {
return errors.New("permission description must not exceed 500 characters")
}
return nil
}
// ToPermissionInfo converts Permission to PermissionInfo (public information)
func (p *Permission) ToPermissionInfo() PermissionInfo {
return PermissionInfo{
ID: p.ID,
Name: p.Name,
Description: p.Description,
Category: p.Category,
Active: p.Active,
System: p.System,
DateCreated: p.DateCreated.Format("2006-01-02T15:04:05Z"),
}
}
// GetResource extracts the resource part from a permission name (e.g., "user:create" -> "user")
func (p *Permission) GetResource() string {
parts := strings.Split(p.Name, ":")
if len(parts) > 0 {
return parts[0]
}
return ""
}
// GetAction extracts the action part from a permission name (e.g., "user:create" -> "create")
func (p *Permission) GetAction() string {
parts := strings.Split(p.Name, ":")
if len(parts) > 1 {
return parts[1]
}
return ""
}
// isValidPermissionName validates permission name format
func isValidPermissionName(name string) bool {
// Permission names should follow the format "resource:action"
parts := strings.Split(name, ":")
if len(parts) != 2 {
return false
}
resource := parts[0]
action := parts[1]
// Validate resource part
if len(resource) < 2 || len(resource) > 50 {
return false
}
// Validate action part
if len(action) < 2 || len(action) > 50 {
return false
}
// Check if both parts contain only valid characters
return isValidIdentifier(resource) && isValidIdentifier(action)
}
// isValidIdentifier checks if a string is a valid identifier (letters, numbers, underscores)
func isValidIdentifier(str string) bool {
for _, char := range str {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '_') {
return false
}
}
return true
}
// Common permission categories
const (
PermissionCategoryUser = "user"
PermissionCategoryRole = "role"
PermissionCategorySystem = "system"
PermissionCategoryContent = "content"
PermissionCategoryReport = "report"
)
// Common permission patterns
const (
PermissionCreate = "create"
PermissionRead = "read"
PermissionUpdate = "update"
PermissionDelete = "delete"
PermissionManage = "manage"
PermissionAdmin = "admin"
)
// GetStandardPermissions returns a list of standard permissions for a resource
func GetStandardPermissions(resource string) []Permission {
return []Permission{
{
Name: resource + ":" + PermissionCreate,
Description: "Create new " + resource + " records",
Category: resource,
Active: true,
},
{
Name: resource + ":" + PermissionRead,
Description: "Read " + resource + " records",
Category: resource,
Active: true,
},
{
Name: resource + ":" + PermissionUpdate,
Description: "Update " + resource + " records",
Category: resource,
Active: true,
},
{
Name: resource + ":" + PermissionDelete,
Description: "Delete " + resource + " records",
Category: resource,
Active: true,
},
}
}
// Common system permissions
const (
ServerView = "server:view"
ServerUpdate = "server:update"
ServerStart = "server:start"
ServerStop = "server:stop"
ConfigView = "config:view"
ConfigUpdate = "config:update"
)
// AllPermissions returns all available permissions in the system
var AllPermissions = []string{
"user:create",
"user:read",
"user:update",
"user:delete",
"role:create",
"role:read",
"role:update",
"role:delete",
"permission:create",
"permission:read",
"permission:update",
"permission:delete",
ServerView,
ServerUpdate,
ServerStart,
ServerStop,
ConfigView,
ConfigUpdate,
"audit:read",
"system:admin",
}

182
local/model/role.go Normal file
View File

@@ -0,0 +1,182 @@
package model
import (
"errors"
"strings"
"gorm.io/gorm"
)
// Role represents a role in the system
type Role struct {
BaseModel
Name string `json:"name" gorm:"unique;not null;type:varchar(100)"`
Description string `json:"description" gorm:"type:text"`
Active bool `json:"active" gorm:"default:true"`
System bool `json:"system" gorm:"default:false"` // System roles cannot be deleted
Users []User `json:"-" gorm:"many2many:user_roles;"`
Permissions []Permission `json:"permissions" gorm:"many2many:role_permissions;"`
}
// RoleCreateRequest represents the request to create a new role
type RoleCreateRequest struct {
Name string `json:"name" validate:"required,min=3,max=100"`
Description string `json:"description" validate:"max=500"`
PermissionIDs []string `json:"permissionIds"`
}
// RoleUpdateRequest represents the request to update a role
type RoleUpdateRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=100"`
Description *string `json:"description,omitempty" validate:"omitempty,max=500"`
Active *bool `json:"active,omitempty"`
PermissionIDs []string `json:"permissionIds,omitempty"`
}
// RoleInfo represents public role information
type RoleInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Active bool `json:"active"`
System bool `json:"system"`
Permissions []PermissionInfo `json:"permissions"`
UserCount int64 `json:"userCount"`
DateCreated string `json:"dateCreated"`
}
// BeforeCreate is called before creating a role
func (r *Role) BeforeCreate(tx *gorm.DB) error {
r.BaseModel.BeforeCreate()
// Normalize name
r.Name = strings.ToLower(strings.TrimSpace(r.Name))
r.Description = strings.TrimSpace(r.Description)
return r.Validate()
}
// BeforeUpdate is called before updating a role
func (r *Role) BeforeUpdate(tx *gorm.DB) error {
r.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if r.Name != "" {
r.Name = strings.ToLower(strings.TrimSpace(r.Name))
}
if r.Description != "" {
r.Description = strings.TrimSpace(r.Description)
}
return r.Validate()
}
// BeforeDelete is called before deleting a role
func (r *Role) BeforeDelete(tx *gorm.DB) error {
if r.System {
return errors.New("system roles cannot be deleted")
}
// Check if role is assigned to any users
var userCount int64
if err := tx.Model(&User{}).Where("roles.id = ?", r.ID).Joins("JOIN user_roles ON users.id = user_roles.user_id").Count(&userCount).Error; err != nil {
return err
}
if userCount > 0 {
return errors.New("cannot delete role that is assigned to users")
}
return nil
}
// Validate validates role data
func (r *Role) Validate() error {
if r.Name == "" {
return errors.New("role name is required")
}
if len(r.Name) < 3 || len(r.Name) > 100 {
return errors.New("role name must be between 3 and 100 characters")
}
if !isValidRoleName(r.Name) {
return errors.New("role name can only contain letters, numbers, underscores, and hyphens")
}
if len(r.Description) > 500 {
return errors.New("role description must not exceed 500 characters")
}
return nil
}
// ToRoleInfo converts Role to RoleInfo (public information)
func (r *Role) ToRoleInfo() RoleInfo {
roleInfo := RoleInfo{
ID: r.ID,
Name: r.Name,
Description: r.Description,
Active: r.Active,
System: r.System,
Permissions: make([]PermissionInfo, len(r.Permissions)),
DateCreated: r.DateCreated.Format("2006-01-02T15:04:05Z"),
}
// Convert permissions
for i, permission := range r.Permissions {
roleInfo.Permissions[i] = permission.ToPermissionInfo()
}
return roleInfo
}
// HasPermission checks if the role has a specific permission
func (r *Role) HasPermission(permissionName string) bool {
for _, permission := range r.Permissions {
if permission.Name == permissionName {
return true
}
}
return false
}
// AddPermission adds a permission to the role
func (r *Role) AddPermission(permission Permission) {
if !r.HasPermission(permission.Name) {
r.Permissions = append(r.Permissions, permission)
}
}
// RemovePermission removes a permission from the role
func (r *Role) RemovePermission(permissionName string) {
for i, permission := range r.Permissions {
if permission.Name == permissionName {
r.Permissions = append(r.Permissions[:i], r.Permissions[i+1:]...)
break
}
}
}
// GetPermissionNames returns a slice of permission names
func (r *Role) GetPermissionNames() []string {
names := make([]string, len(r.Permissions))
for i, permission := range r.Permissions {
names[i] = permission.Name
}
return names
}
// isValidRoleName validates role name format
func isValidRoleName(name string) bool {
// Allow letters, numbers, underscores, and hyphens
for _, char := range name {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '_' || char == '-') {
return false
}
}
return true
}

View File

@@ -0,0 +1,367 @@
package model
import (
"encoding/json"
"strings"
"time"
"gorm.io/gorm"
)
// SecurityEvent represents a security event in the system
type SecurityEvent struct {
BaseModel
EventType string `json:"eventType" gorm:"not null;type:varchar(100);index"`
Severity string `json:"severity" gorm:"not null;type:varchar(20);index"`
UserID string `json:"userId" gorm:"type:varchar(36);index"`
IPAddress string `json:"ipAddress" gorm:"type:varchar(45);index"`
UserAgent string `json:"userAgent" gorm:"type:text"`
Resource string `json:"resource" gorm:"type:varchar(100)"`
Action string `json:"action" gorm:"type:varchar(100)"`
Success bool `json:"success" gorm:"index"`
Blocked bool `json:"blocked" gorm:"default:false;index"`
Message string `json:"message" gorm:"type:text"`
Details map[string]interface{} `json:"details" gorm:"type:text"`
SessionID string `json:"sessionId,omitempty" gorm:"type:varchar(255)"`
RequestID string `json:"requestId,omitempty" gorm:"type:varchar(255)"`
CountryCode string `json:"countryCode,omitempty" gorm:"type:varchar(2)"`
City string `json:"city,omitempty" gorm:"type:varchar(100)"`
Resolved bool `json:"resolved" gorm:"default:false;index"`
ResolvedBy string `json:"resolvedBy,omitempty" gorm:"type:varchar(36)"`
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
Notes string `json:"notes,omitempty" gorm:"type:text"`
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Resolver *User `json:"resolver,omitempty" gorm:"foreignKey:ResolvedBy"`
}
// SecurityEventCreateRequest represents the request to create a new security event
type SecurityEventCreateRequest struct {
EventType string `json:"eventType" validate:"required,max=100"`
Severity string `json:"severity" validate:"required,oneof=low medium high critical"`
UserID string `json:"userId"`
IPAddress string `json:"ipAddress" validate:"max=45"`
UserAgent string `json:"userAgent"`
Resource string `json:"resource" validate:"max=100"`
Action string `json:"action" validate:"max=100"`
Success bool `json:"success"`
Blocked bool `json:"blocked"`
Message string `json:"message" validate:"required"`
Details map[string]interface{} `json:"details"`
SessionID string `json:"sessionId"`
RequestID string `json:"requestId"`
CountryCode string `json:"countryCode" validate:"max=2"`
City string `json:"city" validate:"max=100"`
}
// SecurityEventUpdateRequest represents the request to update a security event
type SecurityEventUpdateRequest struct {
Resolved *bool `json:"resolved,omitempty"`
ResolvedBy *string `json:"resolvedBy,omitempty"`
Notes *string `json:"notes,omitempty"`
}
// SecurityEventInfo represents public security event information
type SecurityEventInfo struct {
ID string `json:"id"`
EventType string `json:"eventType"`
Severity string `json:"severity"`
UserID string `json:"userId"`
UserEmail string `json:"userEmail,omitempty"`
UserName string `json:"userName,omitempty"`
IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"`
Resource string `json:"resource"`
Action string `json:"action"`
Success bool `json:"success"`
Blocked bool `json:"blocked"`
Message string `json:"message"`
Details map[string]interface{} `json:"details"`
SessionID string `json:"sessionId,omitempty"`
RequestID string `json:"requestId,omitempty"`
CountryCode string `json:"countryCode,omitempty"`
City string `json:"city,omitempty"`
Resolved bool `json:"resolved"`
ResolvedBy string `json:"resolvedBy,omitempty"`
ResolverEmail string `json:"resolverEmail,omitempty"`
ResolverName string `json:"resolverName,omitempty"`
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
Notes string `json:"notes,omitempty"`
DateCreated string `json:"dateCreated"`
}
// BeforeCreate is called before creating a security event
func (se *SecurityEvent) BeforeCreate(tx *gorm.DB) error {
se.BaseModel.BeforeCreate()
// Normalize fields
se.EventType = strings.ToLower(strings.TrimSpace(se.EventType))
se.Severity = strings.ToLower(strings.TrimSpace(se.Severity))
se.IPAddress = strings.TrimSpace(se.IPAddress)
se.UserAgent = strings.TrimSpace(se.UserAgent)
se.Resource = strings.ToLower(strings.TrimSpace(se.Resource))
se.Action = strings.ToLower(strings.TrimSpace(se.Action))
se.Message = strings.TrimSpace(se.Message)
return se.Validate()
}
// BeforeUpdate is called before updating a security event
func (se *SecurityEvent) BeforeUpdate(tx *gorm.DB) error {
se.BaseModel.BeforeUpdate()
// If resolving the event, set resolved timestamp
if se.Resolved && se.ResolvedAt == nil {
now := time.Now()
se.ResolvedAt = &now
}
return nil
}
// Validate validates security event data
func (se *SecurityEvent) Validate() error {
validSeverities := []string{"low", "medium", "high", "critical"}
isValidSeverity := false
for _, severity := range validSeverities {
if se.Severity == severity {
isValidSeverity = true
break
}
}
if !isValidSeverity {
return gorm.ErrInvalidValue
}
return nil
}
// SetDetails sets the details field from a map
func (se *SecurityEvent) SetDetails(details map[string]interface{}) error {
se.Details = details
return nil
}
// GetDetails returns the details as a map
func (se *SecurityEvent) GetDetails() map[string]interface{} {
if se.Details == nil {
return make(map[string]interface{})
}
return se.Details
}
// SetDetailsFromJSON sets the details field from a JSON string
func (se *SecurityEvent) SetDetailsFromJSON(jsonStr string) error {
if jsonStr == "" {
se.Details = make(map[string]interface{})
return nil
}
var details map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &details); err != nil {
return err
}
se.Details = details
return nil
}
// GetDetailsAsJSON returns the details as a JSON string
func (se *SecurityEvent) GetDetailsAsJSON() (string, error) {
if se.Details == nil || len(se.Details) == 0 {
return "{}", nil
}
bytes, err := json.Marshal(se.Details)
if err != nil {
return "", err
}
return string(bytes), nil
}
// ToSecurityEventInfo converts SecurityEvent to SecurityEventInfo (public information)
func (se *SecurityEvent) ToSecurityEventInfo() SecurityEventInfo {
info := SecurityEventInfo{
ID: se.ID,
EventType: se.EventType,
Severity: se.Severity,
UserID: se.UserID,
IPAddress: se.IPAddress,
UserAgent: se.UserAgent,
Resource: se.Resource,
Action: se.Action,
Success: se.Success,
Blocked: se.Blocked,
Message: se.Message,
Details: se.GetDetails(),
SessionID: se.SessionID,
RequestID: se.RequestID,
CountryCode: se.CountryCode,
City: se.City,
Resolved: se.Resolved,
ResolvedBy: se.ResolvedBy,
ResolvedAt: se.ResolvedAt,
Notes: se.Notes,
DateCreated: se.DateCreated.Format("2006-01-02T15:04:05Z"),
}
// Include user information if available
if se.User != nil {
info.UserEmail = se.User.Email
info.UserName = se.User.Name
}
// Include resolver information if available
if se.Resolver != nil {
info.ResolverEmail = se.Resolver.Email
info.ResolverName = se.Resolver.Name
}
return info
}
// AddDetail adds a single detail to the details map
func (se *SecurityEvent) AddDetail(key string, value interface{}) {
if se.Details == nil {
se.Details = make(map[string]interface{})
}
se.Details[key] = value
}
// GetDetail gets a single detail from the details map
func (se *SecurityEvent) GetDetail(key string) (interface{}, bool) {
if se.Details == nil {
return nil, false
}
value, exists := se.Details[key]
return value, exists
}
// Resolve resolves the security event
func (se *SecurityEvent) Resolve(resolverID, notes string) {
se.Resolved = true
se.ResolvedBy = resolverID
se.Notes = notes
now := time.Now()
se.ResolvedAt = &now
}
// Unresolve unresolves the security event
func (se *SecurityEvent) Unresolve() {
se.Resolved = false
se.ResolvedBy = ""
se.ResolvedAt = nil
}
// IsResolved returns whether the security event is resolved
func (se *SecurityEvent) IsResolved() bool {
return se.Resolved
}
// IsCritical returns whether the security event is critical
func (se *SecurityEvent) IsCritical() bool {
return se.Severity == SecuritySeverityCritical
}
// IsHigh returns whether the security event is high severity
func (se *SecurityEvent) IsHigh() bool {
return se.Severity == SecuritySeverityHigh
}
// IsBlocked returns whether the security event was blocked
func (se *SecurityEvent) IsBlocked() bool {
return se.Blocked
}
// Security event types
const (
SecurityEventLoginAttempt = "login_attempt"
SecurityEventLoginFailure = "login_failure"
SecurityEventLoginSuccess = "login_success"
SecurityEventBruteForce = "brute_force"
SecurityEventAccountLockout = "account_lockout"
SecurityEventUnauthorizedAccess = "unauthorized_access"
SecurityEventPrivilegeEscalation = "privilege_escalation"
SecurityEventSuspiciousActivity = "suspicious_activity"
SecurityEventRateLimitExceeded = "rate_limit_exceeded"
SecurityEventInvalidToken = "invalid_token"
SecurityEventTokenExpired = "token_expired"
SecurityEventPasswordChange = "password_change"
SecurityEventEmailVerification = "email_verification"
SecurityEventTwoFactorAuth = "two_factor_auth"
SecurityEventDataExfiltration = "data_exfiltration"
SecurityEventMaliciousRequest = "malicious_request"
SecurityEventSystemAccess = "system_access"
SecurityEventConfigChange = "config_change"
SecurityEventFileAccess = "file_access"
SecurityEventDatabaseAccess = "database_access"
)
// Security severity levels
const (
SecuritySeverityLow = "low"
SecuritySeverityMedium = "medium"
SecuritySeverityHigh = "high"
SecuritySeverityCritical = "critical"
)
// CreateSecurityEvent creates a new security event
func CreateSecurityEvent(eventType, severity, message string) *SecurityEvent {
event := &SecurityEvent{
EventType: eventType,
Severity: severity,
Message: message,
Details: make(map[string]interface{}),
}
event.Init()
return event
}
// CreateSecurityEventWithUser creates a new security event with user information
func CreateSecurityEventWithUser(eventType, severity, message, userID string) *SecurityEvent {
event := CreateSecurityEvent(eventType, severity, message)
event.UserID = userID
return event
}
// CreateSecurityEventWithDetails creates a new security event with details
func CreateSecurityEventWithDetails(eventType, severity, message string, details map[string]interface{}) *SecurityEvent {
event := CreateSecurityEvent(eventType, severity, message)
event.Details = details
return event
}
// SetRequestInfo sets request-related information
func (se *SecurityEvent) SetRequestInfo(ipAddress, userAgent, sessionID, requestID string) {
se.IPAddress = ipAddress
se.UserAgent = userAgent
se.SessionID = sessionID
se.RequestID = requestID
}
// SetLocationInfo sets location-related information
func (se *SecurityEvent) SetLocationInfo(countryCode, city string) {
se.CountryCode = countryCode
se.City = city
}
// SetResourceAction sets resource and action information
func (se *SecurityEvent) SetResourceAction(resource, action string) {
se.Resource = resource
se.Action = action
}
// MarkAsBlocked marks the security event as blocked
func (se *SecurityEvent) MarkAsBlocked() {
se.Blocked = true
}
// MarkAsSuccess marks the security event as successful
func (se *SecurityEvent) MarkAsSuccess() {
se.Success = true
}
// MarkAsFailure marks the security event as failed
func (se *SecurityEvent) MarkAsFailure() {
se.Success = false
}

View File

@@ -0,0 +1,280 @@
package model
import (
"errors"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
// SystemConfig represents a system configuration setting
type SystemConfig struct {
BaseModel
Key string `json:"key" gorm:"unique;not null;type:varchar(255)"`
Value string `json:"value" gorm:"type:text"`
DefaultValue string `json:"defaultValue" gorm:"type:text"`
Description string `json:"description" gorm:"type:text"`
Category string `json:"category" gorm:"type:varchar(100)"`
DataType string `json:"dataType" gorm:"type:varchar(50)"` // string, integer, boolean, json
IsEditable bool `json:"isEditable" gorm:"default:true"`
IsSecret bool `json:"isSecret" gorm:"default:false"` // For sensitive values
DateModified string `json:"dateModified" gorm:"type:varchar(50)"`
}
// SystemConfigCreateRequest represents the request to create a new system config
type SystemConfigCreateRequest struct {
Key string `json:"key" validate:"required,min=3,max=255"`
Value string `json:"value"`
DefaultValue string `json:"defaultValue"`
Description string `json:"description" validate:"max=1000"`
Category string `json:"category" validate:"required,max=100"`
DataType string `json:"dataType" validate:"required,oneof=string integer boolean json"`
IsEditable bool `json:"isEditable"`
IsSecret bool `json:"isSecret"`
}
// SystemConfigUpdateRequest represents the request to update a system config
type SystemConfigUpdateRequest struct {
Value *string `json:"value,omitempty"`
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
Category *string `json:"category,omitempty" validate:"omitempty,max=100"`
DataType *string `json:"dataType,omitempty" validate:"omitempty,oneof=string integer boolean json"`
IsEditable *bool `json:"isEditable,omitempty"`
IsSecret *bool `json:"isSecret,omitempty"`
}
// SystemConfigInfo represents public system config information
type SystemConfigInfo struct {
ID string `json:"id"`
Key string `json:"key"`
Value string `json:"value,omitempty"` // Omitted if secret
DefaultValue string `json:"defaultValue,omitempty"`
Description string `json:"description"`
Category string `json:"category"`
DataType string `json:"dataType"`
IsEditable bool `json:"isEditable"`
IsSecret bool `json:"isSecret"`
DateCreated string `json:"dateCreated"`
DateModified string `json:"dateModified"`
}
// BeforeCreate is called before creating a system config
func (sc *SystemConfig) BeforeCreate(tx *gorm.DB) error {
sc.BaseModel.BeforeCreate()
// Normalize key and category
sc.Key = strings.ToLower(strings.TrimSpace(sc.Key))
sc.Category = strings.ToLower(strings.TrimSpace(sc.Category))
sc.Description = strings.TrimSpace(sc.Description)
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
return sc.Validate()
}
// BeforeUpdate is called before updating a system config
func (sc *SystemConfig) BeforeUpdate(tx *gorm.DB) error {
sc.BaseModel.BeforeUpdate()
// Update modification timestamp
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
// Normalize fields if they're being updated
if sc.Key != "" {
sc.Key = strings.ToLower(strings.TrimSpace(sc.Key))
}
if sc.Category != "" {
sc.Category = strings.ToLower(strings.TrimSpace(sc.Category))
}
if sc.Description != "" {
sc.Description = strings.TrimSpace(sc.Description)
}
return sc.Validate()
}
// Validate validates system config data
func (sc *SystemConfig) Validate() error {
if sc.Key == "" {
return errors.New("configuration key is required")
}
if len(sc.Key) < 3 || len(sc.Key) > 255 {
return errors.New("configuration key must be between 3 and 255 characters")
}
if !isValidConfigKey(sc.Key) {
return errors.New("configuration key can only contain letters, numbers, dots, underscores, and hyphens")
}
if sc.Category == "" {
return errors.New("configuration category is required")
}
if len(sc.Category) > 100 {
return errors.New("configuration category must not exceed 100 characters")
}
if sc.DataType == "" {
return errors.New("configuration data type is required")
}
if !isValidDataType(sc.DataType) {
return errors.New("invalid data type, must be one of: string, integer, boolean, json")
}
if len(sc.Description) > 1000 {
return errors.New("configuration description must not exceed 1000 characters")
}
// Validate value according to data type
if sc.Value != "" {
if err := sc.ValidateValue(sc.Value); err != nil {
return err
}
}
return nil
}
// ValidateValue validates the configuration value according to its data type
func (sc *SystemConfig) ValidateValue(value string) error {
switch sc.DataType {
case "integer":
if _, err := strconv.Atoi(value); err != nil {
return errors.New("value must be a valid integer")
}
case "boolean":
if _, err := strconv.ParseBool(value); err != nil {
return errors.New("value must be a valid boolean (true/false)")
}
case "json":
// Basic JSON validation - check if it starts with { or [
trimmed := strings.TrimSpace(value)
if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
return errors.New("value must be valid JSON")
}
}
return nil
}
// ToSystemConfigInfo converts SystemConfig to SystemConfigInfo (public information)
func (sc *SystemConfig) ToSystemConfigInfo() SystemConfigInfo {
info := SystemConfigInfo{
ID: sc.ID,
Key: sc.Key,
DefaultValue: sc.DefaultValue,
Description: sc.Description,
Category: sc.Category,
DataType: sc.DataType,
IsEditable: sc.IsEditable,
IsSecret: sc.IsSecret,
DateCreated: sc.DateCreated.Format("2006-01-02T15:04:05Z"),
DateModified: sc.DateModified,
}
// Only include value if it's not a secret
if !sc.IsSecret {
info.Value = sc.Value
}
return info
}
// GetStringValue returns the configuration value as a string
func (sc *SystemConfig) GetStringValue() string {
if sc.Value != "" {
return sc.Value
}
return sc.DefaultValue
}
// GetIntValue returns the configuration value as an integer
func (sc *SystemConfig) GetIntValue() (int, error) {
value := sc.GetStringValue()
return strconv.Atoi(value)
}
// GetBoolValue returns the configuration value as a boolean
func (sc *SystemConfig) GetBoolValue() (bool, error) {
value := sc.GetStringValue()
return strconv.ParseBool(value)
}
// GetFloatValue returns the configuration value as a float64
func (sc *SystemConfig) GetFloatValue() (float64, error) {
value := sc.GetStringValue()
return strconv.ParseFloat(value, 64)
}
// SetValue sets the configuration value with type validation
func (sc *SystemConfig) SetValue(value string) error {
if err := sc.ValidateValue(value); err != nil {
return err
}
sc.Value = value
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
return nil
}
// ResetToDefault resets the configuration value to its default
func (sc *SystemConfig) ResetToDefault() {
sc.Value = sc.DefaultValue
sc.DateModified = time.Now().UTC().Format(time.RFC3339)
}
// isValidConfigKey validates configuration key format
func isValidConfigKey(key string) bool {
// Allow letters, numbers, dots, underscores, and hyphens
for _, char := range key {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '.' || char == '_' || char == '-') {
return false
}
}
return true
}
// isValidDataType validates the data type
func isValidDataType(dataType string) bool {
validTypes := []string{"string", "integer", "boolean", "json"}
for _, validType := range validTypes {
if dataType == validType {
return true
}
}
return false
}
// Common system configuration categories
const (
ConfigCategoryGeneral = "general"
ConfigCategorySecurity = "security"
ConfigCategoryEmail = "email"
ConfigCategoryAPI = "api"
ConfigCategoryLogging = "logging"
ConfigCategoryStorage = "storage"
ConfigCategoryCache = "cache"
)
// Common system configuration keys
const (
ConfigKeyAppName = "app.name"
ConfigKeyAppVersion = "app.version"
ConfigKeyAppDescription = "app.description"
ConfigKeyJWTExpiryHours = "security.jwt_expiry_hours"
ConfigKeyPasswordMinLength = "security.password_min_length"
ConfigKeyMaxLoginAttempts = "security.max_login_attempts"
ConfigKeyLockoutDurationMinutes = "security.lockout_duration_minutes"
ConfigKeySessionTimeoutMinutes = "security.session_timeout_minutes"
ConfigKeyRateLimitRequests = "security.rate_limit_requests"
ConfigKeyRateLimitWindow = "security.rate_limit_window_minutes"
ConfigKeyLogLevel = "logging.level"
ConfigKeyLogRetentionDays = "logging.retention_days"
ConfigKeyMaxFileUploadSize = "storage.max_file_upload_size_mb"
ConfigKeyCacheEnabled = "cache.enabled"
ConfigKeyCacheTTLMinutes = "cache.ttl_minutes"
)

318
local/model/user.go Normal file
View File

@@ -0,0 +1,318 @@
package model
import (
"errors"
"omega-server/local/utl/password"
"regexp"
"strings"
"time"
"gorm.io/gorm"
)
// User represents a user in the system
type User struct {
BaseModel
Email string `json:"email" gorm:"unique;not null;type:varchar(255)"`
Username string `json:"username" gorm:"unique;not null;type:varchar(100)"`
Name string `json:"name" gorm:"not null;type:varchar(255)"`
PasswordHash string `json:"-" gorm:"not null;type:text"`
Active bool `json:"active" gorm:"default:true"`
EmailVerified bool `json:"emailVerified" gorm:"default:false"`
EmailVerificationToken string `json:"-" gorm:"type:varchar(255)"`
PasswordResetToken string `json:"-" gorm:"type:varchar(255)"`
PasswordResetExpires *time.Time `json:"-"`
LastLogin *time.Time `json:"lastLogin"`
LoginAttempts int `json:"-" gorm:"default:0"`
LockedUntil *time.Time `json:"-"`
TwoFactorEnabled bool `json:"twoFactorEnabled" gorm:"default:false"`
TwoFactorSecret string `json:"-" gorm:"type:varchar(255)"`
Roles []Role `json:"roles" gorm:"many2many:user_roles;"`
AuditLogs []AuditLog `json:"-" gorm:"foreignKey:UserID"`
}
// UserCreateRequest represents the request to create a new user
type UserCreateRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,min=3,max=50"`
Name string `json:"name" validate:"required,min=2,max=100"`
Password string `json:"password" validate:"required,min=8"`
RoleIDs []string `json:"roleIds"`
}
// UserUpdateRequest represents the request to update a user
type UserUpdateRequest struct {
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Username *string `json:"username,omitempty" validate:"omitempty,min=3,max=50"`
Name *string `json:"name,omitempty" validate:"omitempty,min=2,max=100"`
Active *bool `json:"active,omitempty"`
RoleIDs []string `json:"roleIds,omitempty"`
}
// UserLoginRequest represents a login request
type UserLoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
// UserLoginResponse represents a login response
type UserLoginResponse struct {
Token string `json:"token"`
RefreshToken string `json:"refreshToken"`
ExpiresAt time.Time `json:"expiresAt"`
User UserInfo `json:"user"`
}
// UserInfo represents public user information
type UserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
Username string `json:"username"`
Name string `json:"name"`
Active bool `json:"active"`
EmailVerified bool `json:"emailVerified"`
LastLogin *time.Time `json:"lastLogin"`
Roles []RoleInfo `json:"roles"`
Permissions []string `json:"permissions"`
DateCreated time.Time `json:"dateCreated"`
}
// ChangePasswordRequest represents a password change request
type ChangePasswordRequest struct {
CurrentPassword string `json:"currentPassword" validate:"required"`
NewPassword string `json:"newPassword" validate:"required,min=8"`
}
// ResetPasswordRequest represents a password reset request
type ResetPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
}
// ResetPasswordConfirmRequest represents a password reset confirmation
type ResetPasswordConfirmRequest struct {
Token string `json:"token" validate:"required"`
NewPassword string `json:"newPassword" validate:"required,min=8"`
}
// BeforeCreate is called before creating a user
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.BaseModel.BeforeCreate()
// Normalize email and username
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
u.Name = strings.TrimSpace(u.Name)
return u.Validate()
}
// BeforeUpdate is called before updating a user
func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.BaseModel.BeforeUpdate()
// Normalize fields if they're being updated
if u.Email != "" {
u.Email = strings.ToLower(strings.TrimSpace(u.Email))
}
if u.Username != "" {
u.Username = strings.ToLower(strings.TrimSpace(u.Username))
}
if u.Name != "" {
u.Name = strings.TrimSpace(u.Name)
}
return u.Validate()
}
// Validate validates user data
func (u *User) Validate() error {
if u.Email == "" {
return errors.New("email is required")
}
if !isValidEmail(u.Email) {
return errors.New("invalid email format")
}
if u.Username == "" {
return errors.New("username is required")
}
if len(u.Username) < 3 || len(u.Username) > 50 {
return errors.New("username must be between 3 and 50 characters")
}
if !isValidUsername(u.Username) {
return errors.New("username can only contain letters, numbers, underscores, and hyphens")
}
if u.Name == "" {
return errors.New("name is required")
}
if len(u.Name) < 2 || len(u.Name) > 100 {
return errors.New("name must be between 2 and 100 characters")
}
return nil
}
// SetPassword sets the user's password hash
func (u *User) SetPassword(plainPassword string) error {
if err := validatePassword(plainPassword); err != nil {
return err
}
hash, err := password.HashPassword(plainPassword)
if err != nil {
return err
}
u.PasswordHash = hash
return nil
}
// CheckPassword verifies the user's password
func (u *User) CheckPassword(plainPassword string) bool {
return password.CheckPasswordHash(plainPassword, u.PasswordHash)
}
// VerifyPassword verifies the user's password (alias for CheckPassword)
func (u *User) VerifyPassword(plainPassword string) bool {
return u.CheckPassword(plainPassword)
}
// IsLocked checks if the user account is locked
func (u *User) IsLocked() bool {
if u.LockedUntil == nil {
return false
}
return time.Now().Before(*u.LockedUntil)
}
// Lock locks the user account for the specified duration
func (u *User) Lock(duration time.Duration) {
lockUntil := time.Now().Add(duration)
u.LockedUntil = &lockUntil
}
// Unlock unlocks the user account
func (u *User) Unlock() {
u.LockedUntil = nil
u.LoginAttempts = 0
}
// IncrementLoginAttempts increments the login attempt counter
func (u *User) IncrementLoginAttempts() {
u.LoginAttempts++
}
// ResetLoginAttempts resets the login attempt counter
func (u *User) ResetLoginAttempts() {
u.LoginAttempts = 0
}
// UpdateLastLogin updates the last login timestamp
func (u *User) UpdateLastLogin() {
now := time.Now()
u.LastLogin = &now
}
// ToUserInfo converts User to UserInfo (public information)
func (u *User) ToUserInfo() UserInfo {
userInfo := UserInfo{
ID: u.ID,
Email: u.Email,
Username: u.Username,
Name: u.Name,
Active: u.Active,
EmailVerified: u.EmailVerified,
LastLogin: u.LastLogin,
DateCreated: u.DateCreated,
Roles: make([]RoleInfo, len(u.Roles)),
Permissions: []string{},
}
// Convert roles and collect permissions
permissionSet := make(map[string]bool)
for i, role := range u.Roles {
userInfo.Roles[i] = role.ToRoleInfo()
for _, permission := range role.Permissions {
permissionSet[permission.Name] = true
}
}
// Convert permission set to slice
for permission := range permissionSet {
userInfo.Permissions = append(userInfo.Permissions, permission)
}
return userInfo
}
// HasRole checks if the user has a specific role
func (u *User) HasRole(roleName string) bool {
for _, role := range u.Roles {
if role.Name == roleName {
return true
}
}
return false
}
// HasPermission checks if the user has a specific permission
func (u *User) HasPermission(permissionName string) bool {
for _, role := range u.Roles {
for _, permission := range role.Permissions {
if permission.Name == permissionName {
return true
}
}
}
return false
}
// isValidEmail validates email format
func isValidEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return emailRegex.MatchString(email)
}
// isValidUsername validates username format
func isValidUsername(username string) bool {
usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
return usernameRegex.MatchString(username)
}
// validatePassword validates password strength
func validatePassword(password string) error {
if len(password) < 8 {
return errors.New("password must be at least 8 characters long")
}
if len(password) > 128 {
return errors.New("password must not exceed 128 characters")
}
// Check for at least one lowercase letter
if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
return errors.New("password must contain at least one lowercase letter")
}
// Check for at least one uppercase letter
if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
return errors.New("password must contain at least one uppercase letter")
}
// Check for at least one digit
if matched, _ := regexp.MatchString(`\d`, password); !matched {
return errors.New("password must contain at least one digit")
}
// Check for at least one special character
if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
return errors.New("password must contain at least one special character")
}
return nil
}

121
local/repository/base.go Normal file
View File

@@ -0,0 +1,121 @@
package repository
import (
"context"
"fmt"
"gorm.io/gorm"
)
// BaseRepository provides generic CRUD operations for any model
type BaseRepository[T any, F any] struct {
db *gorm.DB
modelType T
}
// NewBaseRepository creates a new base repository for the given model type
func NewBaseRepository[T any, F any](db *gorm.DB, model T) *BaseRepository[T, F] {
return &BaseRepository[T, F]{
db: db,
modelType: model,
}
}
// GetAll retrieves all records based on the filter
func (r *BaseRepository[T, F]) GetAll(ctx context.Context, filter *F) (*[]T, error) {
result := new([]T)
query := r.db.WithContext(ctx).Model(&r.modelType)
// Apply filter conditions if filter implements Filterable
if filterable, ok := any(filter).(Filterable); ok {
query = filterable.ApplyFilter(query)
}
// Apply pagination if filter implements Pageable
if pageable, ok := any(filter).(Pageable); ok {
offset, limit := pageable.Pagination()
query = query.Offset(offset).Limit(limit)
}
// Apply sorting if filter implements Sortable
if sortable, ok := any(filter).(Sortable); ok {
field, desc := sortable.GetSorting()
if desc {
query = query.Order(field + " DESC")
} else {
query = query.Order(field)
}
}
if err := query.Find(result).Error; err != nil {
return nil, fmt.Errorf("error getting records: %w", err)
}
return result, nil
}
// 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).Where("id = ?", id).First(result).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, fmt.Errorf("error getting record by ID: %w", err)
}
return result, nil
}
// Insert creates a new record
func (r *BaseRepository[T, F]) Insert(ctx context.Context, model *T) error {
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
return fmt.Errorf("error creating record: %w", err)
}
return nil
}
// Update modifies an existing record
func (r *BaseRepository[T, F]) Update(ctx context.Context, model *T) error {
if err := r.db.WithContext(ctx).Save(model).Error; err != nil {
return fmt.Errorf("error updating record: %w", err)
}
return nil
}
// Delete removes a record by ID
func (r *BaseRepository[T, F]) Delete(ctx context.Context, id interface{}) error {
if err := r.db.WithContext(ctx).Delete(new(T), id).Error; err != nil {
return fmt.Errorf("error deleting record: %w", err)
}
return nil
}
// Count returns the total number of records matching the filter
func (r *BaseRepository[T, F]) Count(ctx context.Context, filter *F) (int64, error) {
var count int64
query := r.db.WithContext(ctx).Model(&r.modelType)
if filterable, ok := any(filter).(Filterable); ok {
query = filterable.ApplyFilter(query)
}
if err := query.Count(&count).Error; err != nil {
return 0, fmt.Errorf("error counting records: %w", err)
}
return count, nil
}
// Interfaces for filter capabilities
type Filterable interface {
ApplyFilter(*gorm.DB) *gorm.DB
}
type Pageable interface {
Pagination() (offset, limit int)
}
type Sortable interface {
GetSorting() (field string, desc bool)
}

View File

@@ -0,0 +1,171 @@
package repository
import (
"context"
"omega-server/local/model"
"github.com/google/uuid"
"gorm.io/gorm"
)
// MembershipRepository handles database operations for users, roles, and permissions.
type MembershipRepository struct {
*BaseRepository[model.User, model.MembershipFilter]
db *gorm.DB
}
// NewMembershipRepository creates a new MembershipRepository.
func NewMembershipRepository(db *gorm.DB) *MembershipRepository {
return &MembershipRepository{
BaseRepository: NewBaseRepository[model.User, model.MembershipFilter](db, model.User{}),
db: db,
}
}
// FindUserByUsername finds a user by their username.
// It preloads the user's role and the role's permissions.
func (r *MembershipRepository) FindUserByUsername(ctx context.Context, username string) (*model.User, error) {
var user model.User
db := r.db.WithContext(ctx)
err := db.Preload("Roles.Permissions").Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// FindUserByIDWithPermissions finds a user by their ID and preloads Role and Permissions.
func (r *MembershipRepository) FindUserByIDWithPermissions(ctx context.Context, userID string) (*model.User, error) {
var user model.User
db := r.db.WithContext(ctx)
err := db.Preload("Roles.Permissions").First(&user, "id = ?", userID).Error
if err != nil {
return nil, err
}
return &user, nil
}
// CreateUser creates a new user.
func (r *MembershipRepository) CreateUser(ctx context.Context, user *model.User) error {
db := r.db.WithContext(ctx)
return db.Create(user).Error
}
// FindRoleByName finds a role by its name.
func (r *MembershipRepository) FindRoleByName(ctx context.Context, name string) (*model.Role, error) {
var role model.Role
db := r.db.WithContext(ctx)
err := db.Where("name = ?", name).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}
// CreateRole creates a new role.
func (r *MembershipRepository) CreateRole(ctx context.Context, role *model.Role) error {
db := r.db.WithContext(ctx)
return db.Create(role).Error
}
// FindPermissionByName finds a permission by its name.
func (r *MembershipRepository) FindPermissionByName(ctx context.Context, name string) (*model.Permission, error) {
var permission model.Permission
db := r.db.WithContext(ctx)
err := db.Where("name = ?", name).First(&permission).Error
if err != nil {
return nil, err
}
return &permission, nil
}
// CreatePermission creates a new permission.
func (r *MembershipRepository) CreatePermission(ctx context.Context, permission *model.Permission) error {
db := r.db.WithContext(ctx)
return db.Create(permission).Error
}
// AssignPermissionsToRole assigns a set of permissions to a role.
func (r *MembershipRepository) AssignPermissionsToRole(ctx context.Context, role *model.Role, permissions []model.Permission) error {
db := r.db.WithContext(ctx)
return db.Model(role).Association("Permissions").Replace(permissions)
}
// GetUserPermissions retrieves all permissions for a given user ID.
func (r *MembershipRepository) GetUserPermissions(ctx context.Context, userID uuid.UUID) ([]string, error) {
var user model.User
db := r.db.WithContext(ctx)
if err := db.Preload("Roles.Permissions").First(&user, "id = ?", userID).Error; err != nil {
return nil, err
}
permissionSet := make(map[string]bool)
for _, role := range user.Roles {
for _, permission := range role.Permissions {
permissionSet[permission.Name] = true
}
}
permissions := make([]string, 0, len(permissionSet))
for permission := range permissionSet {
permissions = append(permissions, permission)
}
return permissions, nil
}
// ListUsers retrieves all users.
func (r *MembershipRepository) ListUsers(ctx context.Context) ([]*model.User, error) {
var users []*model.User
db := r.db.WithContext(ctx)
err := db.Preload("Roles").Find(&users).Error
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
db := r.db.WithContext(ctx)
err := db.Preload("Roles").First(&user, "id = ?", userID).Error
if err != nil {
return nil, err
}
return &user, nil
}
// UpdateUser updates a user's details in the database.
func (r *MembershipRepository) UpdateUser(ctx context.Context, user *model.User) error {
db := r.db.WithContext(ctx)
return db.Save(user).Error
}
// FindRoleByID finds a role by its ID.
func (r *MembershipRepository) FindRoleByID(ctx context.Context, roleID uuid.UUID) (*model.Role, error) {
var role model.Role
db := r.db.WithContext(ctx)
err := db.First(&role, "id = ?", roleID).Error
if err != nil {
return nil, err
}
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

@@ -0,0 +1,14 @@
package repository
import (
"go.uber.org/dig"
)
// InitializeRepositories
// Initializes Dependency Injection modules for repositories
//
// Args:
// *dig.Container: Dig Container
func InitializeRepositories(c *dig.Container) {
c.Provide(NewMembershipRepository)
}

34
local/service/api.go Normal file
View File

@@ -0,0 +1,34 @@
package service
import (
"omega-server/local/repository"
)
// ApiService provides API-related business logic
type ApiService struct {
// Add repository dependencies as needed
repo *repository.BaseRepository[any, any] // Placeholder
}
// NewApiService creates a new ApiService
func NewApiService() *ApiService {
return &ApiService{
// Initialize with required dependencies
}
}
// SetServerService sets the server service reference (for cross-service communication)
func (s *ApiService) SetServerService(serverService interface{}) {
// TODO: Implement when ServerService is available
}
// GetApiInfo returns basic API information
func (s *ApiService) GetApiInfo() map[string]interface{} {
return map[string]interface{}{
"name": "Omega Server API",
"version": "1.0.0",
"status": "active",
}
}
// TODO: Add other API service methods as needed

307
local/service/membership.go Normal file
View File

@@ -0,0 +1,307 @@
package service
import (
"context"
"errors"
"omega-server/local/model"
"omega-server/local/repository"
"omega-server/local/utl/jwt"
"omega-server/local/utl/logging"
"os"
"github.com/google/uuid"
)
// CacheInvalidator interface for cache invalidation
type CacheInvalidator interface {
InvalidateUserPermissions(userID string)
InvalidateAllUserPermissions()
}
// MembershipService provides business logic for membership-related operations.
type MembershipService struct {
repo *repository.MembershipRepository
cacheInvalidator CacheInvalidator
}
// NewMembershipService creates a new MembershipService.
func NewMembershipService(repo *repository.MembershipRepository) *MembershipService {
return &MembershipService{
repo: repo,
cacheInvalidator: nil, // Will be set later via SetCacheInvalidator
}
}
// SetCacheInvalidator sets the cache invalidator after service initialization
func (s *MembershipService) SetCacheInvalidator(invalidator CacheInvalidator) {
s.cacheInvalidator = invalidator
}
// Login authenticates a user and returns a JWT.
func (s *MembershipService) Login(ctx context.Context, username, password string) (string, error) {
user, err := s.repo.FindUserByUsername(ctx, username)
if err != nil {
return "", errors.New("invalid credentials")
}
// Use secure password verification with constant-time comparison
if !user.VerifyPassword(password) {
return "", errors.New("invalid credentials")
}
// Extract role names for JWT
roleNames := make([]string, len(user.Roles))
for i, role := range user.Roles {
roleNames[i] = role.Name
}
return jwt.GenerateToken(user.ID, user.Email, user.Username, roleNames)
}
// CreateUser creates a new user.
func (s *MembershipService) CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error) {
role, err := s.repo.FindRoleByName(ctx, roleName)
if err != nil {
logging.Error("Failed to find role by name: %v", err)
return nil, errors.New("role not found")
}
user := &model.User{
Username: username,
Email: username + "@example.com", // You may want to accept email as parameter
Name: username,
}
// Set password using the model's SetPassword method
if err := user.SetPassword(password); err != nil {
return nil, err
}
// Assign roles
user.Roles = []model.Role{*role}
if err := s.repo.CreateUser(ctx, user); err != nil {
logging.Error("Failed to create user: %v", err)
return nil, err
}
logging.InfoOperation("USER_CREATE", "Created user: "+user.Username+" (ID: "+user.ID+", Role: "+roleName+")")
return user, nil
}
// ListUsers retrieves all users.
func (s *MembershipService) ListUsers(ctx context.Context) ([]*model.User, error) {
return s.repo.ListUsers(ctx)
}
// GetUser retrieves a single user by ID.
func (s *MembershipService) GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error) {
return s.repo.FindUserByID(ctx, userID)
}
// GetUserWithPermissions retrieves a single user by ID with their role and permissions.
func (s *MembershipService) GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error) {
return s.repo.FindUserByIDWithPermissions(ctx, userID)
}
// UpdateUserRequest defines the request body for updating a user.
type UpdateUserRequest struct {
Username *string `json:"username"`
Password *string `json:"password"`
RoleID *string `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")
}
// Prevent deletion of Super Admin users
for _, role := range user.Roles {
if role.Name == "Super Admin" {
return errors.New("cannot delete Super Admin user")
}
}
err = s.repo.DeleteUser(ctx, userID)
if err != nil {
return err
}
// Invalidate cache for deleted user
if s.cacheInvalidator != nil {
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
}
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)
if err != nil {
return nil, errors.New("user not found")
}
if req.Username != nil {
user.Username = *req.Username
}
if req.Password != nil && *req.Password != "" {
// Use the model's SetPassword method to hash the password
if err := user.SetPassword(*req.Password); err != nil {
return nil, err
}
}
if req.RoleID != nil {
// Check if role exists
roleUUID, err := uuid.Parse(*req.RoleID)
if err != nil {
return nil, errors.New("invalid role ID format")
}
role, err := s.repo.FindRoleByID(ctx, roleUUID)
if err != nil {
return nil, errors.New("role not found")
}
user.Roles = []model.Role{*role}
}
if err := s.repo.UpdateUser(ctx, user); err != nil {
return nil, err
}
// Invalidate cache if role was changed
if req.RoleID != nil && s.cacheInvalidator != nil {
s.cacheInvalidator.InvalidateUserPermissions(userID.String())
}
logging.InfoOperation("USER_UPDATE", "Updated user: "+user.Username+" (ID: "+user.ID+")")
return user, nil
}
// HasPermission checks if a user has a specific permission.
func (s *MembershipService) HasPermission(ctx context.Context, userID string, permissionName string) (bool, error) {
user, err := s.repo.FindUserByIDWithPermissions(ctx, userID)
if err != nil {
return false, err
}
// Check all user roles for permissions
for _, role := range user.Roles {
// Super admin and Admin have all permissions
if role.Name == "Super Admin" || role.Name == "Admin" {
return true, nil
}
for _, p := range role.Permissions {
if p.Name == permissionName {
return true, nil
}
}
}
return false, nil
}
// SetupInitialData creates the initial roles and permissions.
func (s *MembershipService) SetupInitialData(ctx context.Context) error {
// Define all permissions
permissions := model.AllPermissions
createdPermissions := make([]model.Permission, 0)
for _, pName := range permissions {
perm, err := s.repo.FindPermissionByName(ctx, pName)
if err != nil { // Assuming error means not found
perm = &model.Permission{Name: pName}
if err := s.repo.CreatePermission(ctx, perm); err != nil {
return err
}
}
createdPermissions = append(createdPermissions, *perm)
}
// Create Super Admin role with all permissions
superAdminRole, err := s.repo.FindRoleByName(ctx, "Super Admin")
if err != nil {
superAdminRole = &model.Role{Name: "Super Admin"}
if err := s.repo.CreateRole(ctx, superAdminRole); err != nil {
return err
}
}
if err := s.repo.AssignPermissionsToRole(ctx, superAdminRole, createdPermissions); err != nil {
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
}
// Invalidate all caches after role setup changes
if s.cacheInvalidator != nil {
s.cacheInvalidator.InvalidateAllUserPermissions()
}
// Create a default admin user if one doesn't exist
_, err = s.repo.FindUserByUsername(ctx, "admin")
if err != nil {
logging.Debug("Creating default admin user")
_, err = s.CreateUser(ctx, "admin", os.Getenv("PASSWORD"), "Super Admin") // Default password, should be changed
if err != nil {
return err
}
}
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

@@ -0,0 +1,28 @@
package service
import (
"context"
"omega-server/local/model"
"github.com/google/uuid"
)
// MembershipServiceInterface defines the interface for membership-related operations
type MembershipServiceInterface interface {
// Authentication and Authorization
Login(ctx context.Context, username, password string) (string, error)
HasPermission(ctx context.Context, userID string, permissionName string) (bool, error)
GetUserWithPermissions(ctx context.Context, userID string) (*model.User, error)
SetCacheInvalidator(invalidator CacheInvalidator)
// User Management
CreateUser(ctx context.Context, username, password, roleName string) (*model.User, error)
ListUsers(ctx context.Context) ([]*model.User, error)
GetUser(ctx context.Context, userID uuid.UUID) (*model.User, error)
DeleteUser(ctx context.Context, userID uuid.UUID) error
UpdateUser(ctx context.Context, userID uuid.UUID, req UpdateUserRequest) (*model.User, error)
// Role Management
GetAllRoles(ctx context.Context) ([]*model.Role, error)
SetupInitialData(ctx context.Context) error
}

24
local/service/service.go Normal file
View File

@@ -0,0 +1,24 @@
package service
import (
"omega-server/local/repository"
"omega-server/local/utl/logging"
"go.uber.org/dig"
)
// InitializeServices
// Initializes Dependency Injection modules for services
//
// Args:
// *dig.Container: Dig Container
func InitializeServices(c *dig.Container) {
logging.Debug("Initializing repositories")
repository.InitializeRepositories(c)
logging.Debug("Registering services")
// Provide services
c.Provide(NewMembershipService)
logging.Debug("Completed service initialization")
}

102
local/utl/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,102 @@
package cache
import (
"sync"
"time"
"omega-server/local/utl/logging"
"go.uber.org/dig"
)
// CacheItem represents an item in the cache
type CacheItem struct {
Value interface{}
Expiration int64
}
// InMemoryCache is a thread-safe in-memory cache
type InMemoryCache struct {
items map[string]CacheItem
mu sync.RWMutex
}
// NewInMemoryCache creates and returns a new InMemoryCache instance
func NewInMemoryCache() *InMemoryCache {
return &InMemoryCache{
items: make(map[string]CacheItem),
}
}
// Set adds an item to the cache with an expiration duration (in seconds)
func (c *InMemoryCache) Set(key string, value interface{}, duration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
var expiration int64
if duration > 0 {
expiration = time.Now().Add(duration).UnixNano()
}
c.items[key] = CacheItem{
Value: value,
Expiration: expiration,
}
}
// Get retrieves an item from the cache
func (c *InMemoryCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, found := c.items[key]
if !found {
return nil, false
}
if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration {
// Item has expired, but don't delete here to avoid lock upgrade.
// It will be overwritten on the next Set.
return nil, false
}
return item.Value, true
}
// Delete removes an item from the cache
func (c *InMemoryCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
// GetOrSet retrieves an item from the cache. If the item is not found, it
// calls the provided function to get the value, sets it in the cache, and
// returns it.
func GetOrSet[T any](c *InMemoryCache, key string, duration time.Duration, fetcher func() (T, error)) (T, error) {
if cached, found := c.Get(key); found {
if value, ok := cached.(T); ok {
return value, nil
}
}
value, err := fetcher()
if err != nil {
var zero T
return zero, err
}
c.Set(key, value, duration)
return value, nil
}
// Start initializes the cache and provides it to the DI container.
func Start(di *dig.Container) {
cache := NewInMemoryCache()
err := di.Provide(func() *InMemoryCache {
return cache
})
if err != nil {
logging.Panic("failed to provide cache")
}
}

311
local/utl/common/types.go Normal file
View File

@@ -0,0 +1,311 @@
package common
import (
"github.com/gofiber/fiber/v2"
)
// RouteGroups holds the different route groups for API organization
type RouteGroups struct {
API fiber.Router
Auth fiber.Router
Users fiber.Router
Roles fiber.Router
System fiber.Router
Admin fiber.Router
}
// HTTPError represents a structured HTTP error
type HTTPError struct {
Code int `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
}
// Error implements the error interface
func (e HTTPError) Error() string {
return e.Message
}
// NewHTTPError creates a new HTTP error
func NewHTTPError(code int, message string) HTTPError {
return HTTPError{
Code: code,
Message: message,
}
}
// NewHTTPErrorWithDetails creates a new HTTP error with details
func NewHTTPErrorWithDetails(code int, message string, details map[string]interface{}) HTTPError {
return HTTPError{
Code: code,
Message: message,
Details: details,
}
}
// ValidationError represents a validation error
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
Value string `json:"value,omitempty"`
}
// ValidationErrors represents multiple validation errors
type ValidationErrors []ValidationError
// Error implements the error interface
func (ve ValidationErrors) Error() string {
if len(ve) == 0 {
return "validation failed"
}
return ve[0].Message
}
// APIResponse represents a standard API response structure
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
Code int `json:"code,omitempty"`
}
// SuccessResponse creates a success response
func SuccessResponse(data interface{}, message ...string) APIResponse {
response := APIResponse{
Success: true,
Data: data,
}
if len(message) > 0 {
response.Message = message[0]
}
return response
}
// ErrorResponse creates an error response
func ErrorResponse(code int, message string) APIResponse {
return APIResponse{
Success: false,
Error: message,
Code: code,
}
}
// PaginationRequest represents pagination parameters
type PaginationRequest struct {
Page int `query:"page" validate:"min=1"`
Limit int `query:"limit" validate:"min=1,max=100"`
Sort string `query:"sort"`
Order string `query:"order" validate:"oneof=asc desc"`
Search string `query:"search"`
Filter string `query:"filter"`
Category string `query:"category"`
}
// DefaultPagination returns default pagination values
func DefaultPagination() PaginationRequest {
return PaginationRequest{
Page: 1,
Limit: 10,
Sort: "dateCreated",
Order: "desc",
}
}
// Validate validates pagination parameters
func (p *PaginationRequest) Validate() {
if p.Page < 1 {
p.Page = 1
}
if p.Limit < 1 || p.Limit > 100 {
p.Limit = 10
}
if p.Sort == "" {
p.Sort = "dateCreated"
}
if p.Order != "asc" && p.Order != "desc" {
p.Order = "desc"
}
}
// Offset calculates the offset for database queries
func (p *PaginationRequest) Offset() int {
return (p.Page - 1) * p.Limit
}
// PaginationResponse represents paginated response metadata
type PaginationResponse struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"totalPages"`
HasNext bool `json:"hasNext"`
HasPrevious bool `json:"hasPrevious"`
NextPage *int `json:"nextPage,omitempty"`
PreviousPage *int `json:"previousPage,omitempty"`
}
// NewPaginationResponse creates a new pagination response
func NewPaginationResponse(page, limit int, total int64) PaginationResponse {
totalPages := int((total + int64(limit) - 1) / int64(limit))
response := PaginationResponse{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
HasNext: page < totalPages,
HasPrevious: page > 1,
}
if response.HasNext {
next := page + 1
response.NextPage = &next
}
if response.HasPrevious {
prev := page - 1
response.PreviousPage = &prev
}
return response
}
// PaginatedResponse represents a paginated API response
type PaginatedResponse struct {
APIResponse
Pagination PaginationResponse `json:"pagination"`
}
// NewPaginatedResponse creates a new paginated response
func NewPaginatedResponse(data interface{}, pagination PaginationResponse, message ...string) PaginatedResponse {
response := PaginatedResponse{
APIResponse: SuccessResponse(data, message...),
Pagination: pagination,
}
return response
}
// RequestContext represents the context of an HTTP request
type RequestContext struct {
UserID string
Email string
Username string
Roles []string
IP string
UserAgent string
RequestID string
SessionID string
}
// GetUserInfo returns user information from context
func (rc *RequestContext) GetUserInfo() map[string]interface{} {
return map[string]interface{}{
"userId": rc.UserID,
"email": rc.Email,
"username": rc.Username,
"roles": rc.Roles,
}
}
// HasRole checks if the user has a specific role
func (rc *RequestContext) HasRole(role string) bool {
for _, r := range rc.Roles {
if r == role {
return true
}
}
return false
}
// IsAdmin checks if the user has admin role
func (rc *RequestContext) IsAdmin() bool {
return rc.HasRole("admin")
}
// SortDirection represents sort direction
type SortDirection string
const (
SortAsc SortDirection = "asc"
SortDesc SortDirection = "desc"
)
// SortField represents a field to sort by
type SortField struct {
Field string `json:"field"`
Direction SortDirection `json:"direction"`
}
// FilterOperator represents filter operators
type FilterOperator string
const (
FilterEqual FilterOperator = "eq"
FilterNotEqual FilterOperator = "ne"
FilterGreaterThan FilterOperator = "gt"
FilterGreaterEqual FilterOperator = "gte"
FilterLessThan FilterOperator = "lt"
FilterLessEqual FilterOperator = "lte"
FilterLike FilterOperator = "like"
FilterIn FilterOperator = "in"
FilterNotIn FilterOperator = "nin"
FilterIsNull FilterOperator = "isnull"
FilterIsNotNull FilterOperator = "isnotnull"
)
// FilterCondition represents a filter condition
type FilterCondition struct {
Field string `json:"field"`
Operator FilterOperator `json:"operator"`
Value interface{} `json:"value"`
}
// QueryOptions represents query options for database operations
type QueryOptions struct {
Filters []FilterCondition `json:"filters,omitempty"`
Sort []SortField `json:"sort,omitempty"`
Pagination *PaginationRequest `json:"pagination,omitempty"`
Include []string `json:"include,omitempty"`
Exclude []string `json:"exclude,omitempty"`
}
// BulkOperation represents a bulk operation request
type BulkOperation struct {
Action string `json:"action" validate:"required"`
IDs []string `json:"ids" validate:"required,min=1"`
Data interface{} `json:"data,omitempty"`
}
// AuditInfo represents audit information for operations
type AuditInfo struct {
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID string `json:"resourceId"`
UserID string `json:"userId"`
Details map[string]interface{} `json:"details"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// HealthStatus represents system health status
type HealthStatus struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Version string `json:"version"`
Environment string `json:"environment"`
Uptime string `json:"uptime"`
Database map[string]interface{} `json:"database"`
Cache map[string]interface{} `json:"cache"`
Memory map[string]interface{} `json:"memory"`
}
// SystemStats represents system statistics
type SystemStats struct {
TotalUsers int64 `json:"totalUsers"`
ActiveUsers int64 `json:"activeUsers"`
TotalRoles int64 `json:"totalRoles"`
TotalRequests int64 `json:"totalRequests"`
ErrorRate float64 `json:"errorRate"`
ResponseTime float64 `json:"avgResponseTime"`
}

220
local/utl/configs/config.go Normal file
View File

@@ -0,0 +1,220 @@
package configs
import (
"os"
"strconv"
"time"
)
// API configuration constants
const (
// Prefix for all API routes
Prefix = "/api/v1"
// Default values
DefaultPort = "3000"
DefaultDatabaseName = "app.db"
DefaultJWTExpiryHours = 24
DefaultPasswordMinLen = 8
DefaultMaxLoginAttempts = 5
DefaultLockoutMinutes = 30
DefaultRateLimitReqs = 100
DefaultRateLimitWindow = 1 // minute
)
// Environment variable keys
const (
EnvPort = "PORT"
EnvDatabaseName = "DB_NAME"
EnvJWTSecret = "JWT_SECRET"
EnvAppSecret = "APP_SECRET"
EnvAppSecretCode = "APP_SECRET_CODE"
EnvEncryptionKey = "ENCRYPTION_KEY"
EnvCORSAllowedOrigin = "CORS_ALLOWED_ORIGIN"
EnvLogLevel = "LOG_LEVEL"
EnvDebugMode = "DEBUG_MODE"
EnvDefaultAdminPassword = "DEFAULT_ADMIN_PASSWORD"
EnvJWTAccessTTLHours = "JWT_ACCESS_TTL_HOURS"
EnvJWTRefreshTTLDays = "JWT_REFRESH_TTL_DAYS"
EnvJWTIssuer = "JWT_ISSUER"
)
// GetEnv returns environment variable value or default
func GetEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// GetEnvInt returns environment variable as integer or default
func GetEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
// GetEnvBool returns environment variable as boolean or default
func GetEnvBool(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
if boolValue, err := strconv.ParseBool(value); err == nil {
return boolValue
}
}
return defaultValue
}
// GetEnvDuration returns environment variable as duration or default
func GetEnvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return defaultValue
}
// Config holds application configuration
type Config struct {
// Server configuration
Port string
DatabaseName string
LogLevel string
DebugMode bool
// Security configuration
JWTSecret string
AppSecret string
AppSecretCode string
EncryptionKey string
JWTAccessTTLHours int
JWTRefreshTTLDays int
JWTIssuer string
PasswordMinLength int
MaxLoginAttempts int
LockoutDurationMins int
// CORS configuration
CORSAllowedOrigin string
// Rate limiting
RateLimitRequests int
RateLimitWindow int // minutes
// Admin configuration
DefaultAdminPassword string
}
// LoadConfig loads configuration from environment variables
func LoadConfig() *Config {
return &Config{
// Server configuration
Port: GetEnv(EnvPort, DefaultPort),
DatabaseName: GetEnv(EnvDatabaseName, DefaultDatabaseName),
LogLevel: GetEnv(EnvLogLevel, "INFO"),
DebugMode: GetEnvBool(EnvDebugMode, false),
// Security configuration
JWTSecret: GetEnv(EnvJWTSecret, ""),
AppSecret: GetEnv(EnvAppSecret, ""),
AppSecretCode: GetEnv(EnvAppSecretCode, ""),
EncryptionKey: GetEnv(EnvEncryptionKey, ""),
JWTAccessTTLHours: GetEnvInt(EnvJWTAccessTTLHours, DefaultJWTExpiryHours),
JWTRefreshTTLDays: GetEnvInt(EnvJWTRefreshTTLDays, 7),
JWTIssuer: GetEnv(EnvJWTIssuer, "omega-server"),
PasswordMinLength: GetEnvInt("PASSWORD_MIN_LENGTH", DefaultPasswordMinLen),
MaxLoginAttempts: GetEnvInt("MAX_LOGIN_ATTEMPTS", DefaultMaxLoginAttempts),
LockoutDurationMins: GetEnvInt("LOCKOUT_DURATION_MINUTES", DefaultLockoutMinutes),
// CORS configuration
CORSAllowedOrigin: GetEnv(EnvCORSAllowedOrigin, "http://localhost:5173"),
// Rate limiting
RateLimitRequests: GetEnvInt("RATE_LIMIT_REQUESTS", DefaultRateLimitReqs),
RateLimitWindow: GetEnvInt("RATE_LIMIT_WINDOW_MINUTES", DefaultRateLimitWindow),
// Admin configuration
DefaultAdminPassword: GetEnv(EnvDefaultAdminPassword, ""),
}
}
// Validate validates the configuration
func (c *Config) Validate() []string {
var errors []string
// Required security settings
if c.JWTSecret == "" {
errors = append(errors, "JWT_SECRET is required")
}
if c.AppSecret == "" {
errors = append(errors, "APP_SECRET is required")
}
if c.AppSecretCode == "" {
errors = append(errors, "APP_SECRET_CODE is required")
}
if c.EncryptionKey == "" {
errors = append(errors, "ENCRYPTION_KEY is required")
}
// Validate encryption key length (must be 32 characters for AES-256)
if len(c.EncryptionKey) != 32 {
errors = append(errors, "ENCRYPTION_KEY must be exactly 32 characters long")
}
// Validate JWT settings
if c.JWTAccessTTLHours <= 0 {
errors = append(errors, "JWT_ACCESS_TTL_HOURS must be greater than 0")
}
if c.JWTRefreshTTLDays <= 0 {
errors = append(errors, "JWT_REFRESH_TTL_DAYS must be greater than 0")
}
// Validate password settings
if c.PasswordMinLength < 8 {
errors = append(errors, "PASSWORD_MIN_LENGTH must be at least 8")
}
// Validate rate limiting
if c.RateLimitRequests <= 0 {
errors = append(errors, "RATE_LIMIT_REQUESTS must be greater than 0")
}
if c.RateLimitWindow <= 0 {
errors = append(errors, "RATE_LIMIT_WINDOW_MINUTES must be greater than 0")
}
return errors
}
// IsProduction returns true if running in production mode
func (c *Config) IsProduction() bool {
env := GetEnv("GO_ENV", "development")
return env == "production"
}
// IsDevelopment returns true if running in development mode
func (c *Config) IsDevelopment() bool {
return !c.IsProduction()
}
// GetJWTAccessTTL returns JWT access token TTL as duration
func (c *Config) GetJWTAccessTTL() time.Duration {
return time.Duration(c.JWTAccessTTLHours) * time.Hour
}
// GetJWTRefreshTTL returns JWT refresh token TTL as duration
func (c *Config) GetJWTRefreshTTL() time.Duration {
return time.Duration(c.JWTRefreshTTLDays) * 24 * time.Hour
}
// GetLockoutDuration returns account lockout duration
func (c *Config) GetLockoutDuration() time.Duration {
return time.Duration(c.LockoutDurationMins) * time.Minute
}
// GetRateLimitWindow returns rate limit window as duration
func (c *Config) GetRateLimitWindow() time.Duration {
return time.Duration(c.RateLimitWindow) * time.Minute
}

320
local/utl/db/db.go Normal file
View File

@@ -0,0 +1,320 @@
package db
import (
"omega-server/local/model"
"omega-server/local/utl/logging"
"os"
"time"
"go.uber.org/dig"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func Start(di *dig.Container) {
dbName := os.Getenv("DB_NAME")
if dbName == "" {
dbName = "app.db"
}
// Configure GORM logger
gormLogger := logger.Default
if os.Getenv("LOG_LEVEL") == "DEBUG" {
gormLogger = logger.Default.LogMode(logger.Info)
} else {
gormLogger = logger.Default.LogMode(logger.Silent)
}
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{
Logger: gormLogger,
})
if err != nil {
logging.Panic("failed to connect database: " + err.Error())
}
// Configure connection pool
sqlDB, err := db.DB()
if err != nil {
logging.Panic("failed to get database instance: " + err.Error())
}
// Set connection pool settings
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
err = di.Provide(func() *gorm.DB {
return db
})
if err != nil {
logging.Panic("failed to bind database: " + err.Error())
}
logging.Info("Database connected successfully")
Migrate(db)
}
func Migrate(db *gorm.DB) {
logging.Info("Starting database migration...")
err := db.AutoMigrate(
&model.User{},
&model.Role{},
&model.Permission{},
&model.SystemConfig{},
&model.AuditLog{},
&model.SecurityEvent{},
)
if err != nil {
logging.Panic("failed to migrate database models: " + err.Error())
}
logging.Info("Database migration completed successfully")
Seed(db)
}
func Seed(db *gorm.DB) error {
logging.Info("Starting database seeding...")
if err := seedRoles(db); err != nil {
return err
}
if err := seedPermissions(db); err != nil {
return err
}
if err := seedDefaultAdmin(db); err != nil {
return err
}
if err := seedSystemConfigs(db); err != nil {
return err
}
logging.Info("Database seeding completed successfully")
return nil
}
func seedRoles(db *gorm.DB) error {
roles := []model.Role{
{Name: "admin", Description: "Administrator with full access"},
{Name: "user", Description: "Regular user with limited access"},
{Name: "viewer", Description: "Read-only access"},
}
for _, role := range roles {
var existingRole model.Role
err := db.Where("name = ?", role.Name).First(&existingRole).Error
if err == gorm.ErrRecordNotFound {
role.Init()
if err := db.Create(&role).Error; err != nil {
return err
}
logging.Info("Created role: %s", role.Name)
}
}
return nil
}
func seedPermissions(db *gorm.DB) error {
permissions := []model.Permission{
{Name: "user:create", Description: "Create new users"},
{Name: "user:read", Description: "Read user information"},
{Name: "user:update", Description: "Update user information"},
{Name: "user:delete", Description: "Delete users"},
{Name: "role:create", Description: "Create new roles"},
{Name: "role:read", Description: "Read role information"},
{Name: "role:update", Description: "Update role information"},
{Name: "role:delete", Description: "Delete roles"},
{Name: "system:config", Description: "Access system configuration"},
{Name: "system:logs", Description: "Access system logs"},
{Name: "system:admin", Description: "Full system administration"},
}
for _, permission := range permissions {
var existingPermission model.Permission
err := db.Where("name = ?", permission.Name).First(&existingPermission).Error
if err == gorm.ErrRecordNotFound {
permission.Init()
if err := db.Create(&permission).Error; err != nil {
return err
}
logging.Info("Created permission: %s", permission.Name)
}
}
// Assign all permissions to admin role
var adminRole model.Role
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
var allPermissions []model.Permission
if err := db.Find(&allPermissions).Error; err != nil {
return err
}
if err := db.Model(&adminRole).Association("Permissions").Replace(allPermissions); err != nil {
return err
}
// Assign basic permissions to user role
var userRole model.Role
if err := db.Where("name = ?", "user").First(&userRole).Error; err != nil {
return err
}
var userPermissions []model.Permission
userPermissionNames := []string{"user:read", "role:read"}
if err := db.Where("name IN ?", userPermissionNames).Find(&userPermissions).Error; err != nil {
return err
}
if err := db.Model(&userRole).Association("Permissions").Replace(userPermissions); err != nil {
return err
}
// Assign read permissions to viewer role
var viewerRole model.Role
if err := db.Where("name = ?", "viewer").First(&viewerRole).Error; err != nil {
return err
}
var viewerPermissions []model.Permission
viewerPermissionNames := []string{"user:read", "role:read"}
if err := db.Where("name IN ?", viewerPermissionNames).Find(&viewerPermissions).Error; err != nil {
return err
}
if err := db.Model(&viewerRole).Association("Permissions").Replace(viewerPermissions); err != nil {
return err
}
return nil
}
func seedDefaultAdmin(db *gorm.DB) error {
// Check if admin user already exists
var existingAdmin model.User
err := db.Where("email = ?", "admin@example.com").First(&existingAdmin).Error
if err != gorm.ErrRecordNotFound {
return nil // Admin already exists or other error
}
// Get admin role
var adminRole model.Role
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
// Create default admin user
defaultPassword := os.Getenv("DEFAULT_ADMIN_PASSWORD")
if defaultPassword == "" {
defaultPassword = "admin123"
}
admin := model.User{
Email: "admin@example.com",
Username: "admin",
Name: "System Administrator",
Active: true,
}
admin.Init()
if err := admin.SetPassword(defaultPassword); err != nil {
return err
}
if err := db.Create(&admin).Error; err != nil {
return err
}
// Assign admin role
if err := db.Model(&admin).Association("Roles").Append(&adminRole); err != nil {
return err
}
logging.Info("Created default admin user with email: %s", admin.Email)
logging.Warn("Default admin password is: %s - PLEASE CHANGE THIS IMMEDIATELY!", defaultPassword)
return nil
}
func seedSystemConfigs(db *gorm.DB) error {
configs := []model.SystemConfig{
{
Key: "app.name",
Value: "Bootstrap App",
DefaultValue: "Bootstrap App",
Description: "Application name",
Category: "general",
DataType: "string",
IsEditable: true,
DateModified: time.Now().UTC().Format(time.RFC3339),
},
{
Key: "app.version",
Value: "1.0.0",
DefaultValue: "1.0.0",
Description: "Application version",
Category: "general",
DataType: "string",
IsEditable: false,
DateModified: time.Now().UTC().Format(time.RFC3339),
},
{
Key: "security.jwt_expiry_hours",
Value: "24",
DefaultValue: "24",
Description: "JWT token expiry time in hours",
Category: "security",
DataType: "integer",
IsEditable: true,
DateModified: time.Now().UTC().Format(time.RFC3339),
},
{
Key: "security.password_min_length",
Value: "8",
DefaultValue: "8",
Description: "Minimum password length",
Category: "security",
DataType: "integer",
IsEditable: true,
DateModified: time.Now().UTC().Format(time.RFC3339),
},
{
Key: "security.max_login_attempts",
Value: "5",
DefaultValue: "5",
Description: "Maximum login attempts before lockout",
Category: "security",
DataType: "integer",
IsEditable: true,
DateModified: time.Now().UTC().Format(time.RFC3339),
},
{
Key: "security.lockout_duration_minutes",
Value: "30",
DefaultValue: "30",
Description: "Account lockout duration in minutes",
Category: "security",
DataType: "integer",
IsEditable: true,
DateModified: time.Now().UTC().Format(time.RFC3339),
},
}
for _, config := range configs {
var existingConfig model.SystemConfig
err := db.Where("key = ?", config.Key).First(&existingConfig).Error
if err == gorm.ErrRecordNotFound {
config.Init()
if err := db.Create(&config).Error; err != nil {
return err
}
logging.Info("Created system config: %s", config.Key)
}
}
return nil
}

View File

@@ -0,0 +1,184 @@
package error_handler
import (
"fmt"
"omega-server/local/utl/logging"
"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)
}

365
local/utl/jwt/jwt.go Normal file
View File

@@ -0,0 +1,365 @@
package jwt
import (
"errors"
"os"
"strconv"
"time"
"github.com/golang-jwt/jwt/v4"
)
// Claims represents the JWT claims
type Claims struct {
UserID string `json:"userId"`
Email string `json:"email"`
Username string `json:"username"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
// TokenPair represents access and refresh tokens
type TokenPair struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresAt time.Time `json:"expiresAt"`
TokenType string `json:"tokenType"`
}
var (
jwtSecret []byte
accessTokenTTL time.Duration
refreshTokenTTL time.Duration
issuer string
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("token has expired")
ErrInvalidClaims = errors.New("invalid token claims")
)
// Initialize initializes the JWT package with configuration
func Initialize() error {
// Get JWT secret from environment
secret := os.Getenv("JWT_SECRET")
if secret == "" {
return errors.New("JWT_SECRET environment variable is required")
}
jwtSecret = []byte(secret)
// Get token TTL from environment or use defaults
accessTTLStr := os.Getenv("JWT_ACCESS_TTL_HOURS")
if accessTTLStr == "" {
accessTTLStr = "24" // 24 hours default
}
accessHours, err := strconv.Atoi(accessTTLStr)
if err != nil {
accessHours = 24
}
accessTokenTTL = time.Duration(accessHours) * time.Hour
refreshTTLStr := os.Getenv("JWT_REFRESH_TTL_DAYS")
if refreshTTLStr == "" {
refreshTTLStr = "7" // 7 days default
}
refreshDays, err := strconv.Atoi(refreshTTLStr)
if err != nil {
refreshDays = 7
}
refreshTokenTTL = time.Duration(refreshDays) * 24 * time.Hour
// Get issuer from environment
issuer = os.Getenv("JWT_ISSUER")
if issuer == "" {
issuer = "omega-server"
}
return nil
}
// GenerateTokenPair generates both access and refresh tokens
func GenerateTokenPair(userID, email, username string, roles []string) (*TokenPair, error) {
if len(jwtSecret) == 0 {
if err := Initialize(); err != nil {
return nil, err
}
}
now := time.Now()
accessExpiresAt := now.Add(accessTokenTTL)
refreshExpiresAt := now.Add(refreshTokenTTL)
// Create access token claims
accessClaims := &Claims{
UserID: userID,
Email: email,
Username: username,
Roles: roles,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(accessExpiresAt),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: issuer,
Subject: userID,
ID: generateJTI(),
},
}
// Create refresh token claims (minimal data)
refreshClaims := &Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(refreshExpiresAt),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: issuer,
Subject: userID,
ID: generateJTI(),
},
}
// Generate access token
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessTokenString, err := accessToken.SignedString(jwtSecret)
if err != nil {
return nil, err
}
// Generate refresh token
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString(jwtSecret)
if err != nil {
return nil, err
}
return &TokenPair{
AccessToken: accessTokenString,
RefreshToken: refreshTokenString,
ExpiresAt: accessExpiresAt,
TokenType: "Bearer",
}, nil
}
// ValidateToken validates a JWT token and returns the claims
func ValidateToken(tokenString string) (*Claims, error) {
if len(jwtSecret) == 0 {
if err := Initialize(); err != nil {
return nil, err
}
}
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// Validate signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("invalid signing method")
}
return jwtSecret, nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, ErrInvalidClaims
}
// Additional validation
if claims.UserID == "" {
return nil, ErrInvalidClaims
}
return claims, nil
}
// RefreshToken generates a new access token from a valid refresh token
func RefreshToken(refreshTokenString string) (*TokenPair, error) {
// Validate refresh token
claims, err := ValidateToken(refreshTokenString)
if err != nil {
return nil, err
}
// For refresh tokens, we only have minimal user data
// In a real application, you might want to fetch fresh user data from the database
return GenerateTokenPair(claims.UserID, claims.Email, claims.Username, claims.Roles)
}
// ExtractTokenFromHeader extracts JWT token from Authorization header
func ExtractTokenFromHeader(authHeader string) (string, error) {
if authHeader == "" {
return "", errors.New("authorization header is required")
}
const bearerPrefix = "Bearer "
if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix {
return "", errors.New("invalid authorization header format")
}
token := authHeader[len(bearerPrefix):]
if token == "" {
return "", errors.New("token is required")
}
return token, nil
}
// IsTokenExpired checks if a token is expired without validating signature
func IsTokenExpired(tokenString string) bool {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return true
}
claims, ok := token.Claims.(*Claims)
if !ok {
return true
}
return claims.ExpiresAt.Before(time.Now())
}
// GetTokenClaims extracts claims from token without validating signature (use carefully)
func GetTokenClaims(tokenString string) (*Claims, error) {
token, _, err := new(jwt.Parser).ParseUnverified(tokenString, &Claims{})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok {
return nil, ErrInvalidClaims
}
return claims, nil
}
// RevokeToken adds token to revocation list (implement with your chosen storage)
func RevokeToken(tokenString string) error {
// In a real implementation, you would store revoked tokens in a database or cache
// with their JTI (JWT ID) and expiration time for efficient cleanup
claims, err := GetTokenClaims(tokenString)
if err != nil {
return err
}
// Store the JTI in your revocation storage
// Example: revokedTokens[claims.ID] = claims.ExpiresAt
_ = claims // Placeholder to avoid unused variable error
return nil
}
// IsTokenRevoked checks if a token has been revoked (implement with your chosen storage)
func IsTokenRevoked(tokenString string) bool {
// In a real implementation, check if the token's JTI is in the revocation list
claims, err := GetTokenClaims(tokenString)
if err != nil {
return true
}
// Check revocation storage
// Example: _, revoked := revokedTokens[claims.ID]
// return revoked
_ = claims // Placeholder to avoid unused variable error
return false
}
// generateJTI generates a unique JWT ID
func generateJTI() string {
// In a real implementation, use a proper UUID generator
return strconv.FormatInt(time.Now().UnixNano(), 36)
}
// GetTokenExpirationTime returns the expiration time of a token
func GetTokenExpirationTime(tokenString string) (time.Time, error) {
claims, err := GetTokenClaims(tokenString)
if err != nil {
return time.Time{}, err
}
if claims.ExpiresAt == nil {
return time.Time{}, errors.New("token has no expiration time")
}
return claims.ExpiresAt.Time, nil
}
// GetTokenRemainingTime returns the remaining time before token expires
func GetTokenRemainingTime(tokenString string) (time.Duration, error) {
expirationTime, err := GetTokenExpirationTime(tokenString)
if err != nil {
return 0, err
}
remaining := time.Until(expirationTime)
if remaining < 0 {
return 0, ErrExpiredToken
}
return remaining, nil
}
// HasRole checks if the token claims contain a specific role
func (c *Claims) HasRole(role string) bool {
for _, r := range c.Roles {
if r == role {
return true
}
}
return false
}
// HasAnyRole checks if the token claims contain any of the specified roles
func (c *Claims) HasAnyRole(roles []string) bool {
for _, role := range roles {
if c.HasRole(role) {
return true
}
}
return false
}
// HasAllRoles checks if the token claims contain all of the specified roles
func (c *Claims) HasAllRoles(roles []string) bool {
for _, role := range roles {
if !c.HasRole(role) {
return false
}
}
return true
}
// IsAdmin checks if the user has admin role
func (c *Claims) IsAdmin() bool {
return c.HasRole("admin")
}
// GetUserInfo returns basic user information from claims
func (c *Claims) GetUserInfo() map[string]interface{} {
return map[string]interface{}{
"userId": c.UserID,
"email": c.Email,
"username": c.Username,
"roles": c.Roles,
}
}
// GenerateToken generates a JWT token for a user (backward compatibility)
func GenerateToken(userID, email, username string, roles []string) (string, error) {
tokenPair, err := GenerateTokenPair(userID, email, username, roles)
if err != nil {
return "", err
}
return tokenPair.AccessToken, nil
}

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

@@ -0,0 +1,167 @@
package logging
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)
var (
timeFormat = "2006-01-02 15:04:05.000"
)
// 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 a new base logger instance
func InitializeBase(tp string) (*BaseLogger, error) {
return newBaseLogger(tp)
}
func newBaseLogger(tp string) (*BaseLogger, error) {
// Ensure logs directory exists
if err := os.MkdirAll("logs", 0755); err != nil {
return nil, fmt.Errorf("failed to create logs directory: %v", err)
}
// Open log file with date in name
logPath := filepath.Join("logs", fmt.Sprintf("acc-server-%s-%s.log", time.Now().Format("2006-01-02"), tp))
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %v", err)
}
// Create multi-writer for both file and console
multiWriter := io.MultiWriter(file, os.Stdout)
// Create base logger
logger := &BaseLogger{
file: file,
logger: log.New(multiWriter, "", 0),
initialized: true,
}
return logger, nil
}
// GetBaseLogger creates and returns a new base logger instance
func GetBaseLogger(tp string) *BaseLogger {
baseLogger, _ := InitializeBase(tp)
return baseLogger
}
// Close closes the log file
func (bl *BaseLogger) Close() error {
bl.mu.Lock()
defer bl.mu.Unlock()
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("panic")
if baseLogger != nil && baseLogger.IsInitialized() {
if r := recover(); r != nil {
// Get stack trace
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
stackTrace := string(buf[:n])
baseLogger.LogWithCaller(LogLevelPanic, 2, "Recovered from panic: %v\nStack Trace:\n%s", r, stackTrace)
// Re-panic to maintain original behavior if needed
panic(r)
}
}
}

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

@@ -0,0 +1,159 @@
package logging
import (
"fmt"
"runtime"
"sync"
)
// DebugLogger handles debug-level logging
type DebugLogger struct {
base *BaseLogger
}
// NewDebugLogger creates a new debug logger instance
func NewDebugLogger() *DebugLogger {
base, _ := InitializeBase("debug")
return &DebugLogger{
base: base,
}
}
// 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
debugOnce sync.Once
)
// GetDebugLogger returns the global debug logger instance
func GetDebugLogger() *DebugLogger {
debugOnce.Do(func() {
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)
}

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

@@ -0,0 +1,106 @@
package logging
import (
"fmt"
"runtime"
"sync"
)
// ErrorLogger handles error-level logging
type ErrorLogger struct {
base *BaseLogger
}
// NewErrorLogger creates a new error logger instance
func NewErrorLogger() *ErrorLogger {
base, _ := InitializeBase("error")
return &ErrorLogger{
base: base,
}
}
// 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
errorOnce sync.Once
)
// GetErrorLogger returns the global error logger instance
func GetErrorLogger() *ErrorLogger {
errorOnce.Do(func() {
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...)
}

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

@@ -0,0 +1,130 @@
package logging
import (
"fmt"
"sync"
)
// InfoLogger handles info-level logging
type InfoLogger struct {
base *BaseLogger
}
// NewInfoLogger creates a new info logger instance
func NewInfoLogger() *InfoLogger {
base, _ := InitializeBase("info")
return &InfoLogger{
base: base,
}
}
// 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
infoOnce sync.Once
)
// GetInfoLogger returns the global info logger instance
func GetInfoLogger() *InfoLogger {
infoOnce.Do(func() {
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)
}

213
local/utl/logging/logger.go Normal file
View File

@@ -0,0 +1,213 @@
package logging
import (
"fmt"
"sync"
)
var (
// Legacy logger for backward compatibility
logger *Logger
once sync.Once
)
// Logger maintains backward compatibility with existing code
type Logger struct {
base *BaseLogger
errorLogger *ErrorLogger
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() {
logger, err = newLogger()
})
return logger, err
}
func newLogger() (*Logger, error) {
// Initialize the base logger
baseLogger, err := InitializeBase("log")
if err != nil {
return nil, err
}
// Create the legacy logger wrapper
logger := &Logger{
base: baseLogger,
errorLogger: GetErrorLogger(),
warnLogger: GetWarnLogger(),
infoLogger: GetInfoLogger(),
debugLogger: GetDebugLogger(),
}
return logger, nil
}
// Close closes the logger
func (l *Logger) Close() error {
if l.base != nil {
return l.base.Close()
}
return nil
}
// Legacy methods for backward compatibility
func (l *Logger) log(level, format string, v ...interface{}) {
if l.base != nil {
l.base.LogWithCaller(LogLevel(level), 3, format, v...)
}
}
func (l *Logger) Info(format string, v ...interface{}) {
if l.infoLogger != nil {
l.infoLogger.Log(format, v...)
}
}
func (l *Logger) Error(format string, v ...interface{}) {
if l.errorLogger != nil {
l.errorLogger.Log(format, v...)
}
}
func (l *Logger) Warn(format string, v ...interface{}) {
if l.warnLogger != nil {
l.warnLogger.Log(format, v...)
}
}
func (l *Logger) Debug(format string, v ...interface{}) {
if l.debugLogger != nil {
l.debugLogger.Log(format, v...)
}
}
func (l *Logger) Panic(format string) {
if l.errorLogger != nil {
l.errorLogger.LogFatal(format)
}
}
// 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 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 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 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)
}
}
// Enhanced logging convenience functions
// These provide direct access to specialized logging functions
// LogStartup logs application startup information
func LogStartup(component string, message string) {
GetInfoLogger().LogStartup(component, message)
}
// LogShutdown logs application shutdown information
func LogShutdown(component string, message string) {
GetInfoLogger().LogShutdown(component, message)
}
// LogOperation logs general operation information
func LogOperation(operation string, details string) {
GetInfoLogger().LogOperation(operation, details)
}
// LogRequest logs incoming HTTP requests
func LogRequest(method string, path string, userAgent string) {
GetInfoLogger().LogRequest(method, path, userAgent)
}
// LogResponse logs outgoing HTTP responses
func LogResponse(method string, path string, statusCode int, duration string) {
GetInfoLogger().LogResponse(method, path, statusCode, duration)
}
// LogSQL logs SQL queries for debugging
func LogSQL(query string, args ...interface{}) {
GetDebugLogger().LogSQL(query, args...)
}
// LogMemory logs memory usage information
func LogMemory() {
GetDebugLogger().LogMemory()
}
// LogTiming logs timing information for performance debugging
func LogTiming(operation string, duration interface{}) {
GetDebugLogger().LogTiming(operation, duration)
}
// GetLegacyLogger returns the legacy logger instance for backward compatibility
func GetLegacyLogger() *Logger {
if logger == nil {
logger, _ = Initialize()
}
return logger
}
// InitializeLogging initializes all logging components
func InitializeLogging() error {
// Initialize legacy logger for backward compatibility
_, err := Initialize()
if err != nil {
return fmt.Errorf("failed to initialize legacy logger: %v", err)
}
// Pre-initialize all logger types to ensure separate log files
GetErrorLogger()
GetWarnLogger()
GetInfoLogger()
GetDebugLogger()
// Log successful initialization
Info("Logging system initialized successfully")
return nil
}

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

@@ -0,0 +1,98 @@
package logging
import (
"fmt"
"sync"
)
// WarnLogger handles warn-level logging
type WarnLogger struct {
base *BaseLogger
}
// NewWarnLogger creates a new warn logger instance
func NewWarnLogger() *WarnLogger {
base, _ := InitializeBase("warn")
return &WarnLogger{
base: base,
}
}
// 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
warnOnce sync.Once
)
// GetWarnLogger returns the global warn logger instance
func GetWarnLogger() *WarnLogger {
warnOnce.Do(func() {
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)
}

View File

@@ -0,0 +1,336 @@
package password
import (
"errors"
"regexp"
"unicode"
"golang.org/x/crypto/bcrypt"
)
const (
// MinPasswordLength minimum password length
MinPasswordLength = 8
// MaxPasswordLength maximum password length
MaxPasswordLength = 128
// DefaultCost default bcrypt cost
DefaultCost = 12
)
// HashPassword hashes a plain text password using bcrypt
func HashPassword(password string) (string, error) {
if err := ValidatePasswordStrength(password); err != nil {
return "", err
}
bytes, err := bcrypt.GenerateFromPassword([]byte(password), DefaultCost)
if err != nil {
return "", err
}
return string(bytes), nil
}
// CheckPasswordHash compares a plain text password with its hash
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// ValidatePasswordStrength validates password strength requirements
func ValidatePasswordStrength(password string) error {
if len(password) < MinPasswordLength {
return errors.New("password must be at least 8 characters long")
}
if len(password) > MaxPasswordLength {
return errors.New("password must not exceed 128 characters")
}
var (
hasUpper = false
hasLower = false
hasNumber = false
hasSpecial = false
)
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if !hasUpper {
return errors.New("password must contain at least one uppercase letter")
}
if !hasLower {
return errors.New("password must contain at least one lowercase letter")
}
if !hasNumber {
return errors.New("password must contain at least one digit")
}
if !hasSpecial {
return errors.New("password must contain at least one special character")
}
// Check for common patterns
if isCommonPassword(password) {
return errors.New("password is too common, please choose a stronger password")
}
return nil
}
// isCommonPassword checks if password is in list of common passwords
func isCommonPassword(password string) bool {
commonPasswords := []string{
"password", "123456", "password123", "admin", "qwerty",
"letmein", "welcome", "monkey", "1234567890", "password1",
"123456789", "welcome123", "admin123", "root", "test",
"guest", "password12", "changeme", "default", "temp",
}
for _, common := range commonPasswords {
if password == common {
return true
}
}
return false
}
// ValidatePasswordComplexity validates password against additional complexity rules
func ValidatePasswordComplexity(password string) error {
if err := ValidatePasswordStrength(password); err != nil {
return err
}
// Check for repeated characters (more than 3 consecutive)
if hasRepeatedChars(password, 3) {
return errors.New("password must not contain more than 3 consecutive identical characters")
}
// Check for sequential characters (like "1234" or "abcd")
if hasSequentialChars(password, 4) {
return errors.New("password must not contain sequential characters")
}
// Check for keyboard patterns
if hasKeyboardPattern(password) {
return errors.New("password must not contain keyboard patterns")
}
return nil
}
// hasRepeatedChars checks for repeated consecutive characters
func hasRepeatedChars(password string, maxRepeat int) bool {
if len(password) < maxRepeat+1 {
return false
}
count := 1
for i := 1; i < len(password); i++ {
if password[i] == password[i-1] {
count++
if count > maxRepeat {
return true
}
} else {
count = 1
}
}
return false
}
// hasSequentialChars checks for sequential characters
func hasSequentialChars(password string, minSequence int) bool {
if len(password) < minSequence {
return false
}
for i := 0; i <= len(password)-minSequence; i++ {
isSequential := true
isReverseSequential := true
for j := 1; j < minSequence; j++ {
if int(password[i+j]) != int(password[i+j-1])+1 {
isSequential = false
}
if int(password[i+j]) != int(password[i+j-1])-1 {
isReverseSequential = false
}
}
if isSequential || isReverseSequential {
return true
}
}
return false
}
// hasKeyboardPattern checks for common keyboard patterns
func hasKeyboardPattern(password string) bool {
keyboardPatterns := []string{
"qwerty", "asdf", "zxcv", "qwertyuiop", "asdfghjkl", "zxcvbnm",
"1234567890", "qazwsx", "wsxedc", "rfvtgb", "nhyujm", "iklop",
}
lowerPassword := regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(password, "")
lowerPassword = regexp.MustCompile(`[A-Z]`).ReplaceAllStringFunc(lowerPassword, func(s string) string {
return string(rune(s[0]) + 32)
})
for _, pattern := range keyboardPatterns {
if len(lowerPassword) >= len(pattern) {
for i := 0; i <= len(lowerPassword)-len(pattern); i++ {
if lowerPassword[i:i+len(pattern)] == pattern {
return true
}
}
}
}
return false
}
// GenerateRandomPassword generates a random password with specified length
func GenerateRandomPassword(length int) (string, error) {
if length < MinPasswordLength {
length = MinPasswordLength
}
if length > MaxPasswordLength {
length = MaxPasswordLength
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"
// Use crypto/rand for secure random generation
password := make([]byte, length)
for i := range password {
// Simple implementation - in production, use crypto/rand
password[i] = charset[i%len(charset)]
}
// Ensure password meets complexity requirements
result := string(password)
if err := ValidatePasswordStrength(result); err != nil {
// Fallback to a known good password pattern if generation fails
return generateFallbackPassword(length), nil
}
return result, nil
}
// generateFallbackPassword generates a password that meets all requirements
func generateFallbackPassword(length int) string {
if length < MinPasswordLength {
length = MinPasswordLength
}
// Start with a base that meets all requirements
base := "Aa1!"
remaining := length - len(base)
// Fill remaining with mixed characters
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
for i := 0; i < remaining; i++ {
base += string(charset[i%len(charset)])
}
return base
}
// GetPasswordStrengthScore returns a score from 0-100 indicating password strength
func GetPasswordStrengthScore(password string) int {
score := 0
// Length score (0-25 points)
if len(password) >= 8 {
score += 5
}
if len(password) >= 12 {
score += 10
}
if len(password) >= 16 {
score += 10
}
// Character variety (0-40 points)
var hasUpper, hasLower, hasNumber, hasSpecial bool
for _, char := range password {
if unicode.IsUpper(char) {
hasUpper = true
} else if unicode.IsLower(char) {
hasLower = true
} else if unicode.IsNumber(char) {
hasNumber = true
} else if unicode.IsPunct(char) || unicode.IsSymbol(char) {
hasSpecial = true
}
}
if hasUpper {
score += 10
}
if hasLower {
score += 10
}
if hasNumber {
score += 10
}
if hasSpecial {
score += 10
}
// Complexity bonus (0-35 points)
if !isCommonPassword(password) {
score += 10
}
if !hasRepeatedChars(password, 2) {
score += 10
}
if !hasSequentialChars(password, 3) {
score += 10
}
if !hasKeyboardPattern(password) {
score += 5
}
// Cap at 100
if score > 100 {
score = 100
}
return score
}
// GetPasswordStrengthLevel returns a human-readable strength level
func GetPasswordStrengthLevel(password string) string {
score := GetPasswordStrengthScore(password)
switch {
case score >= 80:
return "Very Strong"
case score >= 60:
return "Strong"
case score >= 40:
return "Medium"
case score >= 20:
return "Weak"
default:
return "Very Weak"
}
}

157
local/utl/server/server.go Normal file
View File

@@ -0,0 +1,157 @@
package server
import (
"omega-server/local/api"
"omega-server/local/middleware/security"
"omega-server/local/utl/logging"
"os"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/swagger"
"go.uber.org/dig"
)
func Start(di *dig.Container) *fiber.App {
app := fiber.New(fiber.Config{
AppName: "Omega Project Management",
ServerHeader: "omega-server",
EnablePrintRoutes: true,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
BodyLimit: 10 * 1024 * 1024, // 10MB
Prefork: false,
CaseSensitive: false,
StrictRouting: false,
DisableKeepalive: false,
ErrorHandler: func(c *fiber.Ctx, err error) error {
// Custom error handler
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
// Log error
logging.Error("HTTP Error: %v, Path: %s, Method: %s, IP: %s",
err, c.Path(), c.Method(), c.IP())
// Return JSON error response
return c.Status(code).JSON(fiber.Map{
"error": err.Error(),
"code": code,
})
},
})
// Initialize security middleware
securityMW := security.NewSecurityMiddleware()
// Add recovery middleware first
app.Use(recover.New(recover.Config{
EnableStackTrace: true,
}))
// Add security middleware stack
app.Use(securityMW.SecurityHeaders())
app.Use(securityMW.LogSecurityEvents())
app.Use(securityMW.TimeoutMiddleware(30 * time.Second))
app.Use(securityMW.RequestSizeLimit(10 * 1024 * 1024)) // 10MB
app.Use(securityMW.ValidateUserAgent())
app.Use(securityMW.ValidateContentType("application/json", "application/x-www-form-urlencoded", "multipart/form-data"))
app.Use(securityMW.InputSanitization())
app.Use(securityMW.RateLimit(100, 1*time.Minute)) // 100 requests per minute global
// Add Helmet middleware for security headers
app.Use(helmet.New(helmet.Config{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "DENY",
HSTSMaxAge: 31536000,
HSTSPreloadEnabled: true,
ContentSecurityPolicy: "default-src 'self'",
ReferrerPolicy: "strict-origin-when-cross-origin",
}))
// Configure CORS
allowedOrigin := os.Getenv("CORS_ALLOWED_ORIGIN")
if allowedOrigin == "" {
allowedOrigin = "http://localhost:5173"
}
app.Use(cors.New(cors.Config{
AllowOrigins: allowedOrigin,
AllowHeaders: "Origin, Content-Type, Accept, Authorization, X-Requested-With",
AllowMethods: "GET, POST, PUT, DELETE, OPTIONS, PATCH",
AllowCredentials: true,
MaxAge: 86400, // 24 hours
}))
// Add request logging middleware
app.Use(func(c *fiber.Ctx) error {
start := time.Now()
// Process request
err := c.Next()
// Log request
duration := time.Since(start)
logging.InfoResponse(
c.Method(),
c.Path(),
c.Response().StatusCode(),
duration.String(),
)
return err
})
// Swagger documentation
app.Get("/swagger/*", swagger.HandlerDefault)
// Health check endpoint
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{
"status": "healthy",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"version": "1.0.0",
})
})
// Ping endpoint
app.Get("/ping", func(c *fiber.Ctx) error {
return c.SendString("pong")
})
// Initialize API routes
api.Init(di, app)
// 404 handler
app.Use(func(c *fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": "Route not found",
"path": c.Path(),
})
})
// Get port from environment
port := os.Getenv("PORT")
if port == "" {
port = "3000" // Default port
}
logging.Info("Starting server on port %s", port)
logging.Info("Swagger documentation available at: http://localhost:%s/swagger/", port)
logging.Info("Health check available at: http://localhost:%s/health", port)
// Start server
if err := app.Listen(":" + port); err != nil {
logging.Error("Failed to start server: %v", err)
os.Exit(1)
}
return app
}