Files
tm_back/cmd/web/bootstrap/bootstrap.go
T
Mazyar 241e3b5f2d Implement audit logging functionality across services and middleware
- Introduced a new `auditlog` package to handle audit logging for user actions, including creation, updates, deletions, and authentication events.
- Enhanced existing services (customer, user) to log relevant actions using the new audit logger, capturing details such as actor ID, action type, target type, and success status.
- Added middleware to enrich request context with metadata for audit logging, ensuring comprehensive tracking of user interactions.
- Integrated Elasticsearch for persistent storage of audit logs, with fallback to file-only logging if Elasticsearch is unavailable.
- Updated API documentation to include new audit log endpoints for administrative access.

This update significantly improves the system's ability to track and audit user actions, enhancing security and accountability within the application.
2026-06-29 21:11:33 +03:30

439 lines
13 KiB
Go

package bootstrap
import (
"fmt"
"time"
ai_summarizer "tm/pkg/ai_summarizer"
"tm/pkg/audit"
"tm/pkg/authorization"
"tm/pkg/config"
"tm/pkg/elasticsearch"
"tm/pkg/filestore"
"tm/pkg/gorules"
"tm/pkg/hcaptcha"
"tm/pkg/logger"
"tm/pkg/mongo"
"tm/pkg/notification"
"tm/pkg/ollama"
"tm/pkg/redis"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
)
// Init Application Configuration
func InitConfig() Config {
conf, err := config.LoadConfig(".", &Config{})
if err != nil {
panic(fmt.Sprintf("Failed to load config: %v", err))
}
return *conf
}
// Init Logger Service
func InitLogger(conf config.LoggingConfig) logger.Logger {
return logger.NewLogger(&logger.Config{
Level: conf.Level,
Format: conf.Format,
Output: conf.Output,
File: logger.FileConfig{
Path: conf.File.Path,
MaxSize: conf.File.MaxSize,
MaxBackups: conf.File.MaxBackups,
MaxAge: conf.File.MaxAge,
Compress: conf.File.Compress,
},
})
}
// Init MongoDB Connection Manager
func InitMongoDB(conf config.MongoConfig, log logger.Logger) *mongo.ConnectionManager {
// Convert infra.MongoConfig to mongo.ConnectionConfig
connectionConfig := mongo.ConnectionConfig{
URI: conf.URI,
Database: conf.Name,
MaxPoolSize: uint64(conf.MaxPoolSize),
MinPoolSize: 5, // Default minimum pool size
MaxConnIdleTime: 30 * time.Minute, // Default idle time
ConnectTimeout: conf.Timeout,
ServerSelectionTimeout: 5 * time.Second, // Default server selection timeout
}
// Create MongoDB connection manager
connectionManager, err := mongo.NewConnectionManager(connectionConfig, log)
if err != nil {
log.Error("Failed to initialize MongoDB connection", map[string]interface{}{
"error": err.Error(),
"uri": conf.URI,
"db": conf.Name,
})
panic(fmt.Sprintf("Failed to initialize MongoDB connection: %v", err))
}
log.Info("MongoDB connection manager initialized successfully", map[string]interface{}{
"database": conf.Name,
"uri": conf.URI,
"pool_size": conf.MaxPoolSize,
})
return connectionManager
}
// initHTTPServer initializes the Echo HTTP server with middleware
func InitHTTPServer(conf Config, log logger.Logger) *echo.Echo {
e := echo.New()
// Configure Echo
// e.HideBanner = true
// e.HidePort = true
// Add middleware
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
e.Use(audit.RequestMetaMiddleware())
e.Use(middleware.Logger())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.DELETE, echo.OPTIONS, echo.PATCH},
AllowHeaders: []string{
echo.HeaderOrigin,
echo.HeaderContentType,
echo.HeaderAccept,
echo.HeaderAuthorization,
},
}))
// Add custom middleware for logging
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
// Process request
err := next(c)
// Log request details
duration := time.Since(start)
log.Info("HTTP request processed", map[string]interface{}{
"method": c.Request().Method,
"path": c.Request().URL.Path,
"status": c.Response().Status,
"duration": duration.String(),
"user_agent": c.Request().UserAgent(),
"remote_ip": c.RealIP(),
})
return err
}
})
// Add health check endpoint
e.GET("/admin/v1/health", healthHandler)
// Add Swagger documentation endpoint
e.GET("/swagger*", echoSwagger.WrapHandler)
log.Info("HTTP server initialized successfully", map[string]interface{}{
"middleware": []string{"recover", "request_id", "audit_meta", "logger", "cors", "custom_logging"},
})
return e
}
// Init Redis Connection Manager
func InitRedis(conf config.RedisConfig, log logger.Logger) redis.Client {
connectionConfig := &redis.Config{
Host: conf.Host,
Port: conf.Port,
Password: conf.Password,
DB: conf.DB,
PoolSize: conf.PoolSize,
}
redisClient, err := redis.NewClient(connectionConfig, log)
if err != nil {
log.Error("Failed to initialize Redis connection", map[string]interface{}{
"error": err.Error(),
"host": conf.Host,
"port": conf.Port,
})
panic(fmt.Sprintf("Failed to initialize Redis connection: %v", err))
}
log.Info("Redis connection manager initialized successfully", map[string]interface{}{
"host": conf.Host,
"port": conf.Port,
"pool_size": conf.PoolSize,
})
return redisClient
}
// Init Authorization Service
func InitAuthorizationService(conf JWTConfig, redisClient redis.Client, log logger.Logger) authorization.AuthorizationService {
authConfig := &authorization.AuthorizationConfig{
AccessTokenSecret: conf.AccessSecret,
RefreshTokenSecret: conf.RefreshSecret,
AccessTokenExpiry: time.Duration(conf.AccessExpiresIn) * time.Second,
RefreshTokenExpiry: time.Duration(conf.RefreshExpiresIn) * time.Second,
Issuer: "tender-management-system",
Audience: "tender-management-users",
}
authService := authorization.NewAuthorizationService(authConfig, log, redisClient)
log.Info("Authorization service initialized successfully", map[string]interface{}{
"access_expires_in": conf.AccessExpiresIn,
"refresh_expires_in": conf.RefreshExpiresIn,
})
return authService
}
// Init Notification Service
func InitNotificationService(conf config.NotificationConfig, log logger.Logger) notification.SDK {
notificationConfig := &notification.Config{
BaseURL: conf.BaseURL,
Timeout: conf.Timeout,
RetryAttempts: conf.RetryAttempts,
RetryDelay: conf.RetryDelay,
EnableLogging: conf.EnableLogging,
UserAgent: conf.UserAgent,
}
notificationSDK, err := notification.New(notificationConfig, log)
if err != nil {
log.Error("Failed to initialize Notification SDK", map[string]interface{}{
"error": err.Error(),
})
fmt.Printf("Failed to initialize Notification SDK: %v\n", err.Error())
}
log.Info("Notification SDK initialized successfully", map[string]interface{}{
"base_url": conf.BaseURL,
"timeout": conf.Timeout,
"retry_attempts": conf.RetryAttempts,
"retry_delay": conf.RetryDelay,
"enable_logging": conf.EnableLogging,
"user_agent": conf.UserAgent,
})
return *notificationSDK
}
// Init Ollama Service
func InitOllamaService(conf config.OllamaConfig, log logger.Logger) ollama.SDK {
ollamaConfig := &ollama.Config{
BaseURL: conf.BaseURL,
Timeout: conf.Timeout,
RetryAttempts: conf.RetryAttempts,
RetryDelay: conf.RetryDelay,
EnableLogging: conf.EnableLogging,
UserAgent: conf.UserAgent,
DefaultModel: conf.DefaultModel,
DefaultTemperature: conf.DefaultTemperature,
DefaultTopP: conf.DefaultTopP,
DefaultMaxTokens: conf.DefaultMaxTokens,
EnableStreaming: conf.EnableStreaming,
}
ollamaSDK, err := ollama.New(ollamaConfig, log)
if err != nil {
log.Error("Failed to initialize Ollama SDK", map[string]interface{}{
"error": err.Error(),
})
}
log.Info("Ollama SDK initialized successfully", map[string]interface{}{
"base_url": conf.BaseURL,
"timeout": conf.Timeout,
"retry_attempts": conf.RetryAttempts,
"retry_delay": conf.RetryDelay,
"enable_logging": conf.EnableLogging,
"user_agent": conf.UserAgent,
})
return *ollamaSDK
}
// InitHCaptchaService initializes the hCaptcha verifier
func InitHCaptchaService(conf config.HCaptchaConfig, log logger.Logger) hcaptcha.Verifier {
hcaptchaConfig := &hcaptcha.Config{
SecretKey: conf.SecretKey,
VerifyURL: conf.VerifyURL,
Timeout: conf.Timeout,
}
hcaptchaVerifier := hcaptcha.NewVerifier(hcaptchaConfig, log)
log.Info("hCaptcha verifier initialized successfully", map[string]interface{}{
"verify_url": conf.VerifyURL,
"timeout": conf.Timeout,
})
return hcaptchaVerifier
}
// InitFileStore initializes the GridFS file store service
func InitFileStore(mongoManager *mongo.ConnectionManager, log logger.Logger) (*filestore.GridFSService, error) {
fileStoreService, err := filestore.NewGridFSService(mongoManager, log)
if err != nil {
log.Error("Failed to initialize file store service", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
log.Info("File store service initialized successfully", map[string]interface{}{
"storage_type": "MongoDB GridFS",
})
return fileStoreService, nil
}
// InitAISummarizerClient initializes the AI summarizer HTTP client for on-demand summarization
func InitAISummarizerClient(conf AISummarizerConfig, mongoManager *mongo.ConnectionManager, log logger.Logger) *ai_summarizer.Client {
if conf.APIBaseURL == "" {
log.Warn("AI Summarizer API base URL not configured, on-demand summarization will be unavailable", map[string]interface{}{})
return nil
}
translationCounter := mongo.NewCounter(mongoManager)
cfg := &ai_summarizer.Config{
APIBaseURL: conf.APIBaseURL,
APITimeout: conf.APITimeout,
APIRetryCount: conf.APIRetryCount,
APIRetryDelay: conf.APIRetryDelay,
MinioEndpoint: conf.MinioEndpoint,
MinioAccessKey: conf.MinioAccessKey,
MinioSecretKey: conf.MinioSecretKey,
MinioUseSSL: conf.MinioUseSSL,
MinioRegion: conf.MinioRegion,
MinioBucket: conf.MinioBucket,
OnSuccessfulTranslation: mongo.AITranslationSuccessCallback(translationCounter),
}
client, err := ai_summarizer.NewClient(cfg, log)
if err != nil {
log.Error("Failed to initialize AI Summarizer client", map[string]interface{}{
"error": err.Error(),
})
return nil
}
log.Info("AI Summarizer HTTP client initialized successfully", map[string]interface{}{
"base_url": conf.APIBaseURL,
"timeout": conf.APITimeout,
})
return client
}
// InitAISummarizerStorage initializes the AI summarizer MinIO storage client
func InitAISummarizerStorage(conf AISummarizerConfig, log logger.Logger) *ai_summarizer.StorageClient {
if conf.MinioEndpoint == "" {
log.Warn("AI Summarizer MinIO endpoint not configured, storage-based summaries will be unavailable", map[string]interface{}{})
return nil
}
cfg := &ai_summarizer.Config{
APIBaseURL: conf.APIBaseURL,
APITimeout: conf.APITimeout,
APIRetryCount: conf.APIRetryCount,
APIRetryDelay: conf.APIRetryDelay,
MinioEndpoint: conf.MinioEndpoint,
MinioAccessKey: conf.MinioAccessKey,
MinioSecretKey: conf.MinioSecretKey,
MinioUseSSL: conf.MinioUseSSL,
MinioRegion: conf.MinioRegion,
MinioBucket: conf.MinioBucket,
}
storage, err := ai_summarizer.NewStorageClient(cfg, log)
if err != nil {
log.Error("Failed to initialize AI Summarizer storage client", map[string]interface{}{
"error": err.Error(),
})
return nil
}
log.Info("AI Summarizer storage client initialized successfully", map[string]interface{}{
"endpoint": conf.MinioEndpoint,
"bucket": conf.MinioBucket,
})
return storage
}
// InitGoRulesClient initializes the GoRules SDK client.
func InitGoRulesClient(_ GoRulesConfig, log logger.Logger) gorules.Client {
// GoRules SDK temporarily disabled — gorulessdk private repo unreachable from server.
// Kanban falls back to built-in bid workflow columns when client is nil.
log.Warn("GoRules client disabled; Kanban uses built-in bid workflow columns", map[string]interface{}{})
return nil
/*
if conf.BaseURL == "" || conf.Token == "" || conf.RuleID == "" {
log.Warn("GoRules client not configured, falling back to static Kanban statuses", map[string]interface{}{})
return nil
}
timeout := conf.Timeout
if timeout <= 0 {
timeout = 10 * time.Second
}
client, err := gorules.New(gorules.Config{
BaseURL: conf.BaseURL,
Token: conf.Token,
Timeout: timeout,
}, log)
if err != nil {
log.Error("Failed to initialize GoRules client", map[string]interface{}{
"error": err.Error(),
})
return nil
}
log.Info("GoRules client initialized successfully", map[string]interface{}{
"base_url": conf.BaseURL,
"timeout": timeout,
"rule_id": conf.RuleID,
})
return client
*/
}
// InitElasticsearch creates an Elasticsearch client for audit log storage.
func InitElasticsearch(conf config.ElasticsearchConfig, log logger.Logger) elasticsearch.Client {
esConfig := elasticsearch.Config{
URL: conf.URL,
Username: conf.Username,
Password: conf.Password,
IndexPrefix: conf.IndexPrefix,
Enabled: conf.Enabled,
RequestTimeout: conf.RequestTimeout,
BulkFlushPeriod: conf.BulkFlushPeriod,
QueueSize: conf.QueueSize,
}
client, err := elasticsearch.NewClient(esConfig, log)
if err != nil {
log.Warn("Elasticsearch unavailable, audit logs will be file-only", map[string]interface{}{
"error": err.Error(),
"url": conf.URL,
})
return elasticsearch.NewNoopClient()
}
return client
}
// InitAuditLogger creates a composite audit logger with optional Elasticsearch persistence.
func InitAuditLogger(log logger.Logger, esClient elasticsearch.Client) (audit.Logger, audit.Store) {
store := elasticsearch.NewAuditStore(esClient)
return audit.NewCompositeLogger(log, store), store
}