Merge pull request 'company feedback performance update' (#20) from TM-540 into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_back/pulls/20 Reviewed-by: Hadi Barzegar <barzagarhadi@gmail.com>
This commit is contained in:
@@ -248,6 +248,8 @@ func main() {
|
||||
// Initialize HTTP server
|
||||
e := bootstrap.InitHTTPServer(conf, logger)
|
||||
|
||||
router.SetupCSPSecurity(e)
|
||||
|
||||
// Register routes
|
||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, kanbanHandler, fileStoreHandler, dashboardHandler, xssPolicy)
|
||||
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler, categoryHandler, flagHandler, notificationHandler, contactHandler, cmsHandler, fileStoreHandler, xssPolicy)
|
||||
|
||||
@@ -22,17 +22,20 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, kanbanHandler *kanban.Handler, fileStoreHandler *filestore.Handler, dashboardHandler *dashboard.Handler, xssPolicy *security.XSSPolicy) {
|
||||
adminV1 := e.Group("/admin/v1")
|
||||
|
||||
// Add CSP middleware to admin routes (stricter policy)
|
||||
// SetupCSPSecurity registers global CSP middleware and the CSP report endpoint once.
|
||||
func SetupCSPSecurity(e *echo.Echo) {
|
||||
adminCSPConfig := security.DefaultCSPConfig()
|
||||
adminCSPConfig.ScriptSrc = []string{"'self'"} // No unsafe-inline for admin
|
||||
adminCSPConfig.StyleSrc = []string{"'self'"} // No unsafe-inline for admin
|
||||
e.Use(security.CSPMiddleware(adminCSPConfig))
|
||||
|
||||
// Add CSP report endpoint
|
||||
publicCSPConfig := security.DefaultCSPConfig()
|
||||
|
||||
e.Use(security.CSPMiddlewareForPaths("/admin", adminCSPConfig, "/api/v1", publicCSPConfig))
|
||||
e.POST("/api/v1/csp-report", security.CSPReportHandler)
|
||||
}
|
||||
|
||||
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, kanbanHandler *kanban.Handler, fileStoreHandler *filestore.Handler, dashboardHandler *dashboard.Handler, xssPolicy *security.XSSPolicy) {
|
||||
adminV1 := e.Group("/admin/v1")
|
||||
|
||||
// Admin Users Routes
|
||||
adminUsersGP := adminV1.Group("/users")
|
||||
@@ -221,13 +224,6 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
|
||||
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler, categoryHandler *company_category.Handler, flagHandler *assets.Handler, notificationHandler *notification.NotificationHandler, contactHandler *contact.Handler, cmsHandler *cms.Handler, fileStoreHandler *filestore.Handler, xssPolicy *security.XSSPolicy) {
|
||||
v1 := e.Group("/api/v1")
|
||||
|
||||
// Add CSP middleware to public routes
|
||||
cspConfig := security.DefaultCSPConfig()
|
||||
e.Use(security.CSPMiddleware(cspConfig))
|
||||
|
||||
// Add CSP report endpoint
|
||||
e.POST("/api/v1/csp-report", security.CSPReportHandler)
|
||||
|
||||
customerGP := v1.Group("/profile")
|
||||
{
|
||||
customerGP.POST("/login", customerHandler.Login)
|
||||
|
||||
@@ -16,6 +16,7 @@ type Service interface {
|
||||
Update(ctx context.Context, id string, form *CompanyForm) (*CompanyResponse, error)
|
||||
GetByID(ctx context.Context, id string) (*CompanyResponse, error)
|
||||
GetByIDs(ctx context.Context, ids []string) ([]*CompanyResponse, error)
|
||||
GetNamesByIDs(ctx context.Context, ids []string) (map[string]*CompanyResponse, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// Company listing and search
|
||||
@@ -389,6 +390,30 @@ func (s *companyService) GetByIDs(ctx context.Context, ids []string) ([]*Company
|
||||
return companyResponses, nil
|
||||
}
|
||||
|
||||
// GetNamesByIDs returns company id and name keyed by company ID without loading categories.
|
||||
func (s *companyService) GetNamesByIDs(ctx context.Context, ids []string) (map[string]*CompanyResponse, error) {
|
||||
if len(ids) == 0 {
|
||||
return map[string]*CompanyResponse{}, nil
|
||||
}
|
||||
|
||||
companies, err := s.repository.GetByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get companies by IDs for names", map[string]interface{}{
|
||||
"count": len(ids),
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to get companies by IDs")
|
||||
}
|
||||
|
||||
out := make(map[string]*CompanyResponse, len(companies))
|
||||
for _, c := range companies {
|
||||
id := c.ID.Hex()
|
||||
out[id] = &CompanyResponse{ID: id, Name: c.Name}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UpdateCompanyStatus updates company status
|
||||
func (s *companyService) UpdateStatus(ctx context.Context, id string, form *StatusForm) error {
|
||||
// Get company to check if exists
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package document_scraper
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"strings"
|
||||
"tm/pkg/response"
|
||||
|
||||
@@ -25,7 +26,7 @@ func APIKeyMiddleware(apiKey string) echo.MiddlewareFunc {
|
||||
return response.Unauthorized(c, "API key required. Use X-API-Key header or Authorization: ApiKey <key>")
|
||||
}
|
||||
|
||||
if apiKeyHeader != apiKey {
|
||||
if subtle.ConstantTimeCompare([]byte(apiKeyHeader), []byte(apiKey)) != 1 {
|
||||
return response.Unauthorized(c, "Invalid API key")
|
||||
}
|
||||
|
||||
|
||||
@@ -137,10 +137,19 @@ func (s *feedbackService) Search(ctx context.Context, criteria SearchForm, pagin
|
||||
|
||||
meta := pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset)
|
||||
|
||||
tenderByID, companyByID := s.loadListDetails(ctx, page.Items)
|
||||
|
||||
result := make([]*FeedbackResponse, len(page.Items))
|
||||
for i, feedback := range page.Items {
|
||||
tender, company := s.getDetails(ctx, feedback.TenderID, feedbackCompanyID(feedback))
|
||||
result[i] = feedback.ToResponse(tender, company)
|
||||
var tenderResp *tenderResponse
|
||||
if t, ok := tenderByID[feedback.TenderID]; ok {
|
||||
tenderResp = t
|
||||
}
|
||||
var companyResp *companyResponse
|
||||
if cid := feedbackCompanyID(feedback); cid != "" {
|
||||
companyResp = companyByID[cid]
|
||||
}
|
||||
result[i] = feedback.ToResponse(tenderResp, companyResp)
|
||||
}
|
||||
|
||||
return &FeedbackListResponse{
|
||||
@@ -175,6 +184,77 @@ func feedbackCompanyID(f Feedback) string {
|
||||
return *f.CompanyID
|
||||
}
|
||||
|
||||
func uniqueNonEmptyStrings(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, v := range values {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func collectFeedbackListIDs(items []Feedback) (tenderIDs, companyIDs []string) {
|
||||
tenderIDs = make([]string, 0, len(items))
|
||||
companyIDs = make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
tenderIDs = append(tenderIDs, item.TenderID)
|
||||
companyIDs = append(companyIDs, feedbackCompanyID(item))
|
||||
}
|
||||
return uniqueNonEmptyStrings(tenderIDs), uniqueNonEmptyStrings(companyIDs)
|
||||
}
|
||||
|
||||
// loadListDetails batch-loads tender and company data for feedback list responses.
|
||||
func (s *feedbackService) loadListDetails(ctx context.Context, items []Feedback) (
|
||||
map[string]*tenderResponse,
|
||||
map[string]*companyResponse,
|
||||
) {
|
||||
tenderByID := make(map[string]*tenderResponse)
|
||||
companyByID := make(map[string]*companyResponse)
|
||||
|
||||
if len(items) == 0 {
|
||||
return tenderByID, companyByID
|
||||
}
|
||||
|
||||
tenderIDs, companyIDs := collectFeedbackListIDs(items)
|
||||
|
||||
if len(tenderIDs) > 0 {
|
||||
tenders, err := s.tenderService.GetByIDs(ctx, tenderIDs)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to batch load tenders for feedback list", map[string]interface{}{
|
||||
"count": len(tenderIDs),
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
for id, t := range tenders {
|
||||
tenderByID[id] = tenderToFeedbackResponse(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(companyIDs) > 0 {
|
||||
companies, err := s.companyService.GetNamesByIDs(ctx, companyIDs)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to batch load companies for feedback list", map[string]interface{}{
|
||||
"count": len(companyIDs),
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
for id, c := range companies {
|
||||
companyByID[id] = companyToFeedbackResponse(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tenderByID, companyByID
|
||||
}
|
||||
|
||||
func tenderToFeedbackResponse(t *tender.TenderResponse) *tenderResponse {
|
||||
if t == nil {
|
||||
return nil
|
||||
|
||||
@@ -18,6 +18,7 @@ type TenderRepository interface {
|
||||
// Tender CRUD operations
|
||||
Create(ctx context.Context, tender *Tender) error
|
||||
GetByID(ctx context.Context, id string) (*Tender, error)
|
||||
GetByIDs(ctx context.Context, ids []string) ([]Tender, error)
|
||||
GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error)
|
||||
GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error)
|
||||
GetByContractFolderID(ctx context.Context, contractFolderID string) (*Tender, error)
|
||||
@@ -214,6 +215,33 @@ func (r *tenderRepository) GetByID(ctx context.Context, id string) (*Tender, err
|
||||
return tender, nil
|
||||
}
|
||||
|
||||
// GetByIDs retrieves tenders by IDs in a single query.
|
||||
func (r *tenderRepository) GetByIDs(ctx context.Context, ids []string) ([]Tender, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
objectIDs := make([]bson.ObjectID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
objectID, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
objectIDs = append(objectIDs, objectID)
|
||||
}
|
||||
|
||||
result, err := r.ormRepo.FindAll(ctx, bson.M{"_id": bson.M{"$in": objectIDs}}, orm.NewPaginationBuilder().Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get tenders by IDs", map[string]interface{}{
|
||||
"count": len(ids),
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Items, nil
|
||||
}
|
||||
|
||||
// GetByContractNoticeID retrieves a tender by contract notice ID
|
||||
func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) {
|
||||
filter := bson.M{"contract_notice_id": contractNoticeID}
|
||||
|
||||
@@ -47,6 +47,7 @@ type TenderDocumentDownload struct {
|
||||
type Service interface {
|
||||
// Tender operations
|
||||
GetByID(ctx context.Context, id string, language string) (*TenderResponse, error)
|
||||
GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error)
|
||||
Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
|
||||
@@ -128,6 +129,30 @@ func (s *tenderService) GetByID(ctx context.Context, id string, language string)
|
||||
return s.buildDetailResponse(ctx, tender, language), nil
|
||||
}
|
||||
|
||||
// GetByIDs loads multiple tenders for list endpoints without MinIO or AI enrichment.
|
||||
func (s *tenderService) GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error) {
|
||||
if len(ids) == 0 {
|
||||
return map[string]*TenderResponse{}, nil
|
||||
}
|
||||
|
||||
tenders, err := s.repository.GetByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to retrieve tenders by IDs", map[string]interface{}{
|
||||
"count": len(ids),
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to retrieve tenders: %w", err)
|
||||
}
|
||||
|
||||
out := make(map[string]*TenderResponse, len(tenders))
|
||||
for i := range tenders {
|
||||
resp := tenders[i].ToResponseWithLanguage("")
|
||||
out[resp.ID] = resp
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// buildDetailResponse maps a tender to API form and enriches translation (MinIO) and summary.
|
||||
func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender, language string) *TenderResponse {
|
||||
resp := tender.ToResponseWithLanguage("")
|
||||
|
||||
@@ -108,6 +108,27 @@ func CSPMiddleware(config *CSPConfig) echo.MiddlewareFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// CSPMiddlewareForPaths applies CSP middleware based on request path prefix.
|
||||
// Requests matching no prefix proceed without a CSP header.
|
||||
func CSPMiddlewareForPaths(adminPrefix string, adminConfig *CSPConfig, publicPrefix string, publicConfig *CSPConfig) echo.MiddlewareFunc {
|
||||
adminMW := CSPMiddleware(adminConfig)
|
||||
publicMW := CSPMiddleware(publicConfig)
|
||||
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
path := c.Request().URL.Path
|
||||
switch {
|
||||
case strings.HasPrefix(path, adminPrefix):
|
||||
return adminMW(next)(c)
|
||||
case strings.HasPrefix(path, publicPrefix):
|
||||
return publicMW(next)(c)
|
||||
default:
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSPReportHandler handles CSP violation reports
|
||||
func CSPReportHandler(c echo.Context) error {
|
||||
var report struct {
|
||||
|
||||
Reference in New Issue
Block a user