diff --git a/cmd/api/main.go b/cmd/api/main.go index a7c278d7..76de7729 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -14,8 +14,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/database" "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/route" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index a831c25b..b7229382 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -7,8 +7,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) @@ -24,6 +24,10 @@ type AuthContext struct { User *entity.User Roles []sso.Role Permissions map[string]struct{} + UserAreaIDs []uint + UserLocationIDs []uint + UserAllArea bool + UserAllLocation bool } // Auth validates the incoming request against the central SSO access token and @@ -67,15 +71,19 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl var roles []sso.Role permissions := make(map[string]struct{}) + var profile *sso.UserProfile if verification.UserID != 0 { - if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { + if p, err := sso.FetchProfile(c.Context(), token, verification); err != nil { utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") - } else if profile != nil { - roles = profile.Roles - for _, perm := range profile.PermissionNames() { - if perm != "" { - permissions[perm] = struct{}{} - } + } else { + profile = p + } + } + if profile != nil { + roles = profile.Roles + for _, perm := range profile.PermissionNames() { + if perm != "" { + permissions[perm] = struct{}{} } } } @@ -86,6 +94,16 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl User: user, Roles: roles, Permissions: permissions, + UserAreaIDs: nil, + UserLocationIDs: nil, + UserAllArea: false, + UserAllLocation: false, + } + if profile != nil { + ctx.UserAreaIDs = profile.AreaIDs + ctx.UserLocationIDs = profile.LocationIDs + ctx.UserAllArea = profile.AllArea + ctx.UserAllLocation = profile.AllLocation } c.Locals(authContextLocalsKey, ctx) diff --git a/internal/middleware/role_scope.go b/internal/middleware/role_scope.go new file mode 100644 index 00000000..8430b6fc --- /dev/null +++ b/internal/middleware/role_scope.go @@ -0,0 +1,636 @@ +package middleware + +import ( + "errors" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +type ScopeFilter struct { + IDs []uint + Restrict bool +} + +type roleScope struct { + allArea bool + allLocation bool + areaIDs []uint + locationIDs []uint + hasAnyScopes bool +} + +func ResolveAreaScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { + scope, err := collectRoleScope(c) + if err != nil || !scope.hasAnyScopes { + return ScopeFilter{}, err + } + + if scope.allArea || scope.allLocation { + return ScopeFilter{}, nil + } + + allowed := uniqueUint(scope.areaIDs) + if len(scope.locationIDs) > 0 { + derived, err := areaIDsByLocationIDs(db, scope.locationIDs) + if err != nil { + return ScopeFilter{}, err + } + allowed = uniqueUint(append(allowed, derived...)) + } + + if len(allowed) == 0 { + return ScopeFilter{Restrict: true}, nil + } + return ScopeFilter{IDs: allowed, Restrict: true}, nil +} + +func ResolveLocationScope(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, error) { + scope, err := collectRoleScope(c) + if err != nil || !scope.hasAnyScopes { + return ScopeFilter{}, err + } + + if scope.allLocation || scope.allArea { + return ScopeFilter{}, nil + } + + areaIDs := uniqueUint(scope.areaIDs) + locationIDs := uniqueUint(scope.locationIDs) + + switch { + case len(locationIDs) > 0 && len(areaIDs) > 0: + filtered, err := filterLocationIDsByAreaIDs(db, locationIDs, areaIDs) + if err != nil { + return ScopeFilter{}, err + } + locationIDs = filtered + case len(locationIDs) == 0 && len(areaIDs) > 0: + derived, err := locationIDsByAreaIDs(db, areaIDs) + if err != nil { + return ScopeFilter{}, err + } + locationIDs = derived + } + + locationIDs = uniqueUint(locationIDs) + if len(locationIDs) == 0 { + return ScopeFilter{Restrict: true}, nil + } + return ScopeFilter{IDs: locationIDs, Restrict: true}, nil +} + +func ResolveLocationAreaScopes(c *fiber.Ctx, db *gorm.DB) (ScopeFilter, ScopeFilter, error) { + locationScope, err := ResolveLocationScope(c, db) + if err != nil { + return ScopeFilter{}, ScopeFilter{}, err + } + areaScope, err := ResolveAreaScope(c, db) + if err != nil { + return ScopeFilter{}, ScopeFilter{}, err + } + return locationScope, areaScope, nil +} + +func collectRoleScope(c *fiber.Ctx) (roleScope, error) { + ctx, ok := AuthDetails(c) + if !ok || ctx == nil { + return roleScope{}, nil + } + + userAreaIDs := uniqueUint(ctx.UserAreaIDs) + userLocationIDs := uniqueUint(ctx.UserLocationIDs) + userScope := roleScope{ + allArea: ctx.UserAllArea, + allLocation: ctx.UserAllLocation, + areaIDs: userAreaIDs, + locationIDs: userLocationIDs, + hasAnyScopes: ctx.UserAllArea || ctx.UserAllLocation || len(userAreaIDs) > 0 || len(userLocationIDs) > 0, + } + if userScope.hasAnyScopes { + return userScope, nil + } + + return roleScope{}, nil +} + +func areaIDsByLocationIDs(db *gorm.DB, locationIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(locationIDs) == 0 { + return nil, nil + } + var areaIDs []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("id IN ?", locationIDs). + Distinct("area_id"). + Pluck("area_id", &areaIDs).Error; err != nil { + return nil, err + } + return areaIDs, nil +} + +func locationIDsByAreaIDs(db *gorm.DB, areaIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(areaIDs) == 0 { + return nil, nil + } + var locationIDs []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("area_id IN ?", areaIDs). + Distinct("id"). + Pluck("id", &locationIDs).Error; err != nil { + return nil, err + } + return locationIDs, nil +} + +func filterLocationIDsByAreaIDs(db *gorm.DB, locationIDs, areaIDs []uint) ([]uint, error) { + if db == nil { + return nil, errors.New("database not configured") + } + if len(locationIDs) == 0 || len(areaIDs) == 0 { + return nil, nil + } + var filtered []uint + if err := db.Model(&entity.Location{}). + Where("deleted_at IS NULL"). + Where("id IN ?", locationIDs). + Where("area_id IN ?", areaIDs). + Distinct("id"). + Pluck("id", &filtered).Error; err != nil { + return nil, err + } + return filtered, nil +} + +func uniqueUint(ids []uint) []uint { + if len(ids) == 0 { + return nil + } + seen := make(map[uint]struct{}, len(ids)) + result := make([]uint, 0, len(ids)) + for _, id := range ids { + if id == 0 { + continue + } + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + result = append(result, id) + } + return result +} + +func ApplyScopeFilter(db *gorm.DB, scope ScopeFilter, column string) *gorm.DB { + if db == nil || !scope.Restrict { + return db + } + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + return db.Where(column+" IN ?", scope.IDs) +} + +func ApplyLocationScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) { + scopeDB := db + if db != nil { + scopeDB = db.Session(&gorm.Session{NewDB: true}) + } + scope, err := ResolveLocationScope(c, scopeDB) + if err != nil { + return db, err + } + return ApplyScopeFilter(db, scope, column), nil +} + +func ApplyAreaScope(c *fiber.Ctx, db *gorm.DB, column string) (*gorm.DB, error) { + scopeDB := db + if db != nil { + scopeDB = db.Session(&gorm.Session{NewDB: true}) + } + scope, err := ResolveAreaScope(c, scopeDB) + if err != nil { + return db, err + } + return ApplyScopeFilter(db, scope, column), nil +} + +func ApplyLocationAreaScope(c *fiber.Ctx, db *gorm.DB, locationColumn, areaColumn string) (*gorm.DB, error) { + scopeDB := db + if db != nil { + scopeDB = db.Session(&gorm.Session{NewDB: true}) + } + + if locationColumn != "" { + locationScope, err := ResolveLocationScope(c, scopeDB) + if err != nil { + return db, err + } + db = ApplyScopeFilter(db, locationScope, locationColumn) + } + + if areaColumn != "" { + areaScope, err := ResolveAreaScope(c, scopeDB) + if err != nil { + return db, err + } + db = ApplyScopeFilter(db, areaScope, areaColumn) + } + + return db, nil +} + +func EnsureWarehouseAccess(c *fiber.Ctx, db *gorm.DB, warehouseID uint) error { + if warehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid warehouse id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Warehouse not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.Warehouse{}). + Where("id = ?", warehouseID), + scope, + "warehouses.location_id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Warehouse not found") + } + return nil +} + +func EnsureAreaAccess(c *fiber.Ctx, db *gorm.DB, areaID uint) error { + if areaID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid area id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveAreaScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Area not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.Area{}). + Where("id = ?", areaID), + scope, + "areas.id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Area not found") + } + return nil +} + +func EnsureLocationAccess(c *fiber.Ctx, db *gorm.DB, locationID uint) error { + if locationID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid location id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Location not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.Location{}). + Where("id = ?", locationID), + scope, + "locations.id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Location not found") + } + return nil +} + +func EnsureKandangAccess(c *fiber.Ctx, db *gorm.DB, kandangID uint) error { + if kandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.Kandang{}). + Where("id = ?", kandangID), + scope, + "kandangs.location_id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + return nil +} + +func EnsureProductWarehouseAccess(c *fiber.Ctx, db *gorm.DB, productWarehouseID uint) error { + if productWarehouseID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid product warehouse id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("product_warehouses pw"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("pw.id = ?", productWarehouseID) + q = ApplyScopeFilter(q, scope, "w.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") + } + return nil +} + +func EnsureStockLogAccess(c *fiber.Ctx, db *gorm.DB, stockLogID uint) error { + if stockLogID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid stock log id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Stock log not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("stock_logs sl"). + Joins("JOIN product_warehouses pw ON pw.id = sl.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("sl.id = ?", stockLogID) + q = ApplyScopeFilter(q, scope, "w.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Stock log not found") + } + return nil +} + +func EnsureMarketingAccess(c *fiber.Ctx, db *gorm.DB, marketingID uint) error { + if marketingID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid marketing id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("marketings m"). + Joins("JOIN marketing_products mp ON mp.marketing_id = m.id"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("m.id = ?", marketingID) + q = ApplyScopeFilter(q, scope, "w.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + return nil +} + +func EnsureRecordingAccess(c *fiber.Ctx, db *gorm.DB, recordingID uint) error { + if recordingID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid recording id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("recordings r"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = r.project_flock_kandangs_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Where("r.id = ?", recordingID) + q = ApplyScopeFilter(q, scope, "pf.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Recording not found") + } + return nil +} + +func EnsureUniformityAccess(c *fiber.Ctx, db *gorm.DB, uniformityID uint) error { + if uniformityID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid uniformity id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("project_flock_kandang_uniformity u"). + Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Where("u.id = ?", uniformityID) + q = ApplyScopeFilter(q, scope, "pf.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + return nil +} + +func EnsureLayingTransferAccess(c *fiber.Ctx, db *gorm.DB, transferID uint) error { + if transferID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid transfer id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Transfer not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("laying_transfers lt"). + Joins("JOIN project_flocks pf_from ON pf_from.id = lt.from_project_flock_id"). + Joins("JOIN project_flocks pf_to ON pf_to.id = lt.to_project_flock_id"). + Where("lt.id = ?", transferID). + Where("(pf_from.location_id IN ? OR pf_to.location_id IN ?)", scope.IDs, scope.IDs) + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Transfer not found") + } + return nil +} + +func EnsureProjectFlockAccess(c *fiber.Ctx, db *gorm.DB, projectFlockID uint) error { + if projectFlockID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Project Flock not found") + } + + var count int64 + if err := ApplyScopeFilter( + db.WithContext(c.Context()). + Model(&entity.ProjectFlock{}). + Where("id = ?", projectFlockID), + scope, + "project_flocks.location_id", + ).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Project Flock not found") + } + return nil +} + +func EnsureProjectFlockKandangAccess(c *fiber.Ctx, db *gorm.DB, projectFlockID, projectFlockKandangID uint) error { + if projectFlockKandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project flock kandang id") + } + if db == nil { + return fiber.NewError(fiber.StatusInternalServerError, "Database not configured") + } + + scope, err := ResolveLocationScope(c, db) + if err != nil || !scope.Restrict { + return err + } + if len(scope.IDs) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") + } + + var count int64 + q := db.WithContext(c.Context()). + Table("project_flock_kandangs"). + Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). + Where("project_flock_kandangs.id = ?", projectFlockKandangID) + if projectFlockID > 0 { + q = q.Where("project_flock_kandangs.project_flock_id = ?", projectFlockID) + } + q = ApplyScopeFilter(q, scope, "project_flocks.location_id") + if err := q.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") + } + return nil +} diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 78453226..bd068843 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -12,6 +12,7 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" @@ -98,6 +99,11 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit statusFilter := "" if params.ProjectStatus != nil { @@ -111,6 +117,12 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withClosingRelations(db) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") + } if params.LocationID != nil { db = db.Where("location_id = ?", *params.LocationID) } @@ -150,6 +162,10 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl } func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + projectFlock, err := s.ProjectFlockRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") @@ -161,6 +177,13 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj } func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) { + if projectFlockKandangID != nil { + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { + return nil, err + } + } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err + } realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID) if err != nil { @@ -174,8 +197,8 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectF } func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) { - if projectFlockID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err } if kandangID != nil { @@ -321,8 +344,8 @@ func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectF } func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { - if projectFlockID == 0 { - return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, 0, err } if params == nil { @@ -344,14 +367,6 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing") } - if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan") - } - s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err) - return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") - } - warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID) if err != nil { s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err) @@ -580,6 +595,14 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID } func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) { + if projectFlockKandangID != nil { + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { + return nil, err + } + } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err + } + budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err @@ -668,8 +691,12 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl } func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) { - if projectFlockID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + if projectFlockKandangID != nil { + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), projectFlockID, *projectFlockKandangID); err != nil { + return nil, err + } + } else if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), projectFlockID); err != nil { + return nil, err } rows, err := s.Repository.GetExpeditionHPP(c.Context(), projectFlockID, projectFlockKandangID) diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index f306c74d..e2974039 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -9,7 +9,7 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" @@ -134,6 +134,87 @@ func (s dailyChecklistService) withRelations(db *gorm.DB) *gorm.DB { return db.Preload("Kandang") } +func (s dailyChecklistService) ensureChecklistAccess(c *fiber.Ctx, checklistID uint) error { + if checklistID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid checklist id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("daily_checklists dc"). + Joins("JOIN kandangs k ON k.id = dc.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("dc.id = ?", checklistID) + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + return nil +} + +func (s dailyChecklistService) ensureKandangAccess(c *fiber.Ctx, kandangID uint) error { + if kandangID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("kandangs k"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("k.id = ?", kandangID) + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + return nil +} + +func (s dailyChecklistService) ensureTaskAccess(c *fiber.Ctx, taskID uint) error { + if taskID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid task id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("daily_checklist_activity_tasks t"). + Joins("JOIN daily_checklists dc ON dc.id = t.checklist_id"). + Joins("JOIN kandangs k ON k.id = dc.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("t.id = ?", taskID) + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Task not found") + } + return nil +} + func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]DailyChecklistListItem, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -143,7 +224,15 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([ db := s.Repository.DB().WithContext(c.Context()). Table("daily_checklists dc"). - Joins("JOIN kandangs k ON k.id = dc.kandang_id") + Joins("JOIN kandangs k ON k.id = dc.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id") + + var scopeErr error + db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if scopeErr != nil { + return nil, 0, scopeErr + } if params.DateFrom != "" { dateFrom, err := time.Parse("2006-01-02", params.DateFrom) @@ -294,6 +383,9 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([ } func (s dailyChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.DailyChecklist, error) { + if err := s.ensureChecklistAccess(c, id); err != nil { + return nil, err + } dailyChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") @@ -399,6 +491,9 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := s.ensureKandangAccess(c, req.KandangId); err != nil { + return nil, err + } date, err := time.Parse("2006-01-02", req.Date) if err != nil { @@ -431,6 +526,9 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := s.ensureChecklistAccess(c, id); err != nil { + return nil, err + } deletedIDs := make([]uint, 0) if req.DeletedDocumentIDs != nil { @@ -456,7 +554,7 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i updateBody["reject_reason"] = *req.RejectReason } - actorID, err := middleware.ActorIDFromContext(c) + actorID, err := m.ActorIDFromContext(c) if err != nil { return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") } @@ -502,6 +600,9 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i } func (s dailyChecklistService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.ensureChecklistAccess(c, id); err != nil { + return err + } if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") @@ -516,6 +617,9 @@ func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validati if err := s.Validate.Struct(req); err != nil { return err } + if err := s.ensureChecklistAccess(c, id); err != nil { + return err + } if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -597,6 +701,9 @@ func (s dailyChecklistService) AssignPhases(c *fiber.Ctx, id uint, req *validati } func (s dailyChecklistService) RemoveAssignment(c *fiber.Ctx, id uint, employeeID uint) error { + if err := s.ensureChecklistAccess(c, id); err != nil { + return err + } if _, err := s.Repository.GetByID(c.Context(), id, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") @@ -634,6 +741,9 @@ func (s dailyChecklistService) GetTasks(c *fiber.Ctx, checklistID uint) ([]entit if checklistID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") } + if err := s.ensureChecklistAccess(c, checklistID); err != nil { + return nil, err + } if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -658,6 +768,9 @@ func (s dailyChecklistService) GetChecklistPhaseIDs(c *fiber.Ctx, checklistID ui if checklistID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "checklist_id is required") } + if err := s.ensureChecklistAccess(c, checklistID); err != nil { + return nil, err + } if _, err := s.Repository.GetByID(c.Context(), checklistID, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -687,6 +800,9 @@ func (s dailyChecklistService) UpdateAssignment(c *fiber.Ctx, req *validation.Up if err := s.Validate.Struct(req); err != nil { return err } + if err := s.ensureTaskAccess(c, req.TaskID); err != nil { + return err + } task := new(entity.DailyChecklistActivityTask) if err := s.Repository.DB().WithContext(c.Context()).First(task, req.TaskID).Error; err != nil { @@ -808,6 +924,9 @@ func (s dailyChecklistService) AssignTasks(c *fiber.Ctx, id uint, req *validatio if err := s.Validate.Struct(req); err != nil { return err } + if err := s.ensureChecklistAccess(c, id); err != nil { + return err + } employeeIDs, err := parseIDs(req.EmployeeIDs) if err != nil { @@ -900,8 +1019,16 @@ func (s dailyChecklistService) GetSummary(c *fiber.Ctx, params *validation.Summa Joins("JOIN daily_checklists d ON d.id = t.checklist_id"). Joins("JOIN kandangs k ON k.id = d.kandang_id"). Joins("JOIN employees e ON e.id = a.employee_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas ar ON ar.id = loc.area_id"). Where("d.date BETWEEN ? AND ? AND d.status = ?", dateFrom, dateTo, "APPROVED") + var scopeErr error + db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "ar.id") + if scopeErr != nil { + return nil, scopeErr + } + if params.Category != "" { db = db.Where("d.category = ?", params.Category) } @@ -946,6 +1073,15 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report return nil, 0, err } + locationScope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + areaScope, err := m.ResolveAreaScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit buildBase := func() *gorm.DB { @@ -962,6 +1098,9 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year). Where("dc.status = ?", "APPROVED") + db = m.ApplyScopeFilter(db, locationScope, "loc.id") + db = m.ApplyScopeFilter(db, areaScope, "a.id") + if params.AreaID != nil { db = db.Where("a.id = ?", *params.AreaID) } diff --git a/internal/modules/dashboards/controllers/dashboard.controller.go b/internal/modules/dashboards/controllers/dashboard.controller.go index bebad10f..f74f31a7 100644 --- a/internal/modules/dashboards/controllers/dashboard.controller.go +++ b/internal/modules/dashboards/controllers/dashboard.controller.go @@ -6,6 +6,7 @@ import ( "strings" "time" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/dashboards/validations" @@ -81,6 +82,20 @@ func (u *DashboardController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid include") } + scope, err := m.ResolveLocationScope(c, u.DashboardService.DB()) + if err != nil { + return err + } + if scope.Restrict { + if len(scope.IDs) == 0 { + lokasiIds = []uint{} + } else if len(lokasiIds) > 0 { + lokasiIds = intersectUint(lokasiIds, scope.IDs) + } else { + lokasiIds = scope.IDs + } + } + analysisMode := strings.ToUpper(strings.TrimSpace(c.Query("analysis_mode", validation.AnalysisModeOverview))) metric := strings.ToLower(strings.TrimSpace(c.Query("metric", ""))) @@ -176,6 +191,23 @@ func defaultUintSlice(values []uint) []uint { return values } +func intersectUint(a, b []uint) []uint { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[uint]struct{}, len(b)) + for _, id := range b { + set[id] = struct{}{} + } + out := make([]uint, 0, len(a)) + for _, id := range a { + if _, ok := set[id]; ok { + out = append(out, id) + } + } + return out +} + func parsePeriodDates(startDateRaw, endDateRaw string, location *time.Location) (time.Time, time.Time, time.Time, error) { now := time.Now().In(location) startDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, location) diff --git a/internal/modules/dashboards/services/dashboard.service.go b/internal/modules/dashboards/services/dashboard.service.go index ce2cc0f8..928205d2 100644 --- a/internal/modules/dashboards/services/dashboard.service.go +++ b/internal/modules/dashboards/services/dashboard.service.go @@ -18,10 +18,12 @@ import ( "github.com/go-playground/validator/v10" "github.com/sirupsen/logrus" + "gorm.io/gorm" ) type DashboardService interface { GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) + DB() *gorm.DB } type dashboardService struct { @@ -40,6 +42,10 @@ func NewDashboardService(repo repository.DashboardRepository, validate *validato } } +func (s dashboardService) DB() *gorm.DB { + return s.Repository.DB() +} + func (s dashboardService) GetAll(ctx context.Context, params *validation.Query) (dto.DashboardPerformanceOverviewDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return dto.DashboardPerformanceOverviewDTO{}, 0, err diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index 0ccab661..68890c9a 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -139,9 +139,28 @@ func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context locationID := filters.LocationId areaID := filters.AreaId - if locationID > 0 || areaID > 0 { + if filters.AllowedLocationIDs != nil || filters.AllowedAreaIDs != nil || locationID > 0 || areaID > 0 { db = db.Joins("JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id") + } + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("kandangs.location_id IN ?", filters.AllowedLocationIDs) + } + } + + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Joins("JOIN locations ON locations.id = kandangs.location_id"). + Where("locations.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if locationID > 0 || areaID > 0 { if locationID > 0 { db = db.Where("kandangs.location_id = ?", uint(locationID)) } diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 8b42fbdf..5e6fc420 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -87,16 +87,22 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens return nil, 0, err } + var scopeErr error + offset := (params.Page - 1) * params.Limit expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db, scopeErr = middleware.ApplyLocationScope(c, db, "expenses.location_id") if params.Search != "" { return db.Where("category ILIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { return nil, 0, err @@ -117,7 +123,16 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens } func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) { - expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + var scopeErr error + + expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db, scopeErr = middleware.ApplyLocationScope(c, db, "expenses.location_id") + return db + }) + if scopeErr != nil { + return nil, scopeErr + } if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 16bcf70a..0cad4ff4 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -74,6 +74,10 @@ func (s *adjustmentService) withRelations(db *gorm.DB) *gorm.DB { } func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.AdjustmentStock, error) { + if err := m.EnsureStockLogAccess(c, s.StockLogsRepository.DB(), id); err != nil { + return nil, err + } + adjustmentStock, err := s.AdjustmentStockRepository.GetByID(c.Context(), id, s.withRelations) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -95,6 +99,9 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e if err != nil { return nil, err } + if err := m.EnsureWarehouseAccess(c, s.WarehouseRepo.DB(), uint(req.WarehouseID)); err != nil { + return nil, err + } if err := common.EnsureRelations(c.Context(), common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists}, common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists}, @@ -304,6 +311,19 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu Preload("ProductWarehouse.Warehouse"). Preload("StockLog.CreatedUser") + scope, scopeErr := m.ResolveLocationScope(c, s.AdjustmentStockRepository.DB()) + if scopeErr != nil { + return nil, 0, scopeErr + } + if scope.Restrict { + if len(scope.IDs) == 0 { + return []*entity.AdjustmentStock{}, 0, nil + } + q = q.Joins("JOIN product_warehouses pw_scope ON pw_scope.id = adjustment_stocks.product_warehouse_id"). + Joins("JOIN warehouses w_scope ON w_scope.id = pw_scope.warehouse_id") + q = m.ApplyScopeFilter(q, scope, "w_scope.location_id") + } + if query.ProductID > 0 { q = q.Joins("JOIN product_warehouses ON product_warehouses.id = adjustment_stocks.product_warehouse_id"). Where("product_warehouses.product_id = ?", query.ProductID) diff --git a/internal/modules/inventory/product-stocks/services/product-stock.service.go b/internal/modules/inventory/product-stocks/services/product-stock.service.go index 11475109..63ae97ac 100644 --- a/internal/modules/inventory/product-stocks/services/product-stock.service.go +++ b/internal/modules/inventory/product-stocks/services/product-stock.service.go @@ -4,6 +4,7 @@ import ( "errors" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-stocks/validations" productRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -36,19 +37,49 @@ func NewProductStockService( } } -func (s productStockService) withRelations(db *gorm.DB) *gorm.DB { +func (s productStockService) withRelations(db *gorm.DB, locationScope, areaScope m.ScopeFilter) *gorm.DB { + warehouseScope := func(db *gorm.DB) *gorm.DB { + if locationScope.Restrict { + db = db.Where("warehouses.location_id IN ?", locationScope.IDs) + } + if areaScope.Restrict { + db = db.Where("warehouses.area_id IN ?", areaScope.IDs) + } + return db + } + productWarehouseScope := func(db *gorm.DB) *gorm.DB { + db = db.Joins("JOIN warehouses w ON w.id = product_warehouses.warehouse_id") + if locationScope.Restrict { + db = db.Where("w.location_id IN ?", locationScope.IDs) + } + if areaScope.Restrict { + db = db.Where("w.area_id IN ?", areaScope.IDs) + } + return db + } + stockLogScope := func(db *gorm.DB) *gorm.DB { + db = db. + Joins("JOIN product_warehouses pw ON pw.id = stock_logs.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id") + if locationScope.Restrict { + db = db.Where("w.location_id IN ?", locationScope.IDs) + } + if areaScope.Restrict { + db = db.Where("w.area_id IN ?", areaScope.IDs) + } + return db.Order("stock_logs.created_at ASC") + } + return db. Preload("CreatedUser"). Preload("Uom"). Preload("ProductCategory"). Preload("Flags"). - Preload("ProductWarehouses"). - Preload("ProductWarehouses.Warehouse"). + Preload("ProductWarehouses", productWarehouseScope). + Preload("ProductWarehouses.Warehouse", warehouseScope). Preload("ProductWarehouses.Warehouse.Location"). Preload("ProductWarehouses.Warehouse.Location.Area"). - Preload("ProductWarehouses.StockLogs", func(db *gorm.DB) *gorm.DB { - return db.Order("created_at ASC") - }). + Preload("ProductWarehouses.StockLogs", stockLogScope). Preload("ProductWarehouses.StockLogs.CreatedUser"). Preload("ProductSuppliers"). Preload("ProductSuppliers.Supplier", func(db *gorm.DB) *gorm.DB { @@ -61,17 +92,40 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, err } + locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.ProductRepository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit productStocks, total, err := s.ProductRepository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = db.Where(`EXISTS ( - SELECT 1 - FROM product_warehouses pw - WHERE pw.product_id = products.id - AND pw.qty > 0 - )`) + if locationScope.Restrict || areaScope.Restrict { + if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) { + return db.Where("1 = 0") + } + db = db.Where(`EXISTS ( + SELECT 1 + FROM product_warehouses pw + JOIN warehouses w ON w.id = pw.warehouse_id + WHERE pw.product_id = products.id + AND pw.qty > 0 + AND (? OR w.location_id IN ?) + AND (? OR w.area_id IN ?) + )`, + !locationScope.Restrict, locationScope.IDs, + !areaScope.Restrict, areaScope.IDs, + ) + } else { + db = db.Where(`EXISTS ( + SELECT 1 + FROM product_warehouses pw + WHERE pw.product_id = products.id + AND pw.qty > 0 + )`) + } - db = s.withRelations(db) + db = s.withRelations(db, locationScope, areaScope) if params.Search != "" { db = db.Where("products.name ILIKE ?", "%"+params.Search+"%") } @@ -86,7 +140,34 @@ func (s productStockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, error) { - product, err := s.ProductRepository.GetByID(c.Context(), id, s.withRelations) + locationScope, areaScope, err := m.ResolveLocationAreaScopes(c, s.ProductRepository.DB()) + if err != nil { + return nil, err + } + + if locationScope.Restrict || areaScope.Restrict { + if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) { + return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") + } + var count int64 + if err := s.ProductRepository.DB().WithContext(c.Context()). + Table("product_warehouses pw"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("pw.product_id = ?", id). + Where("pw.qty > 0"). + Where("(? OR w.location_id IN ?)", !locationScope.Restrict, locationScope.IDs). + Where("(? OR w.area_id IN ?)", !areaScope.Restrict, areaScope.IDs). + Count(&count).Error; err != nil { + return nil, err + } + if count == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") + } + } + + product, err := s.ProductRepository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return s.withRelations(db, locationScope, areaScope) + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") } diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go index 671d964b..47d85a65 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -8,6 +8,7 @@ import ( service "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" ) @@ -24,12 +25,13 @@ func NewProductWarehouseController(productWarehouseService service.ProductWareho func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - ProductId: uint(c.QueryInt("product_id", 0)), - WarehouseId: uint(c.QueryInt("warehouse_id", 0)), - Flags: c.Query("flags", ""), - KandangId: uint(c.QueryInt("kandang_id", 0)), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + ProductId: uint(c.QueryInt("product_id", 0)), + WarehouseId: uint(c.QueryInt("warehouse_id", 0)), + Flags: c.Query("flags", ""), + KandangId: uint(c.QueryInt("kandang_id", 0)), + TransferContext: c.Query(utils.TransferContextKey, ""), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 5b89808c..5bb3f692 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -7,6 +7,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations" kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" @@ -53,6 +54,19 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) return nil, 0, err } + applyScope := true + if params.TransferContext == utils.TransferContextInventoryTransfer { + applyScope = !m.HasPermission(c, m.P_TransferCreateOne) + } + var scope m.ScopeFilter + var err error + if applyScope { + scope, err = m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + } + if params.ProductId > 0 { isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId) if err != nil { @@ -90,6 +104,17 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). + Where("w_scope.deleted_at IS NULL") + if applyScope { + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where("w_scope.location_id IN ?", scope.IDs) + } + } + if params.ProductId != 0 { db = db.Where("product_id = ?", params.ProductId) } @@ -116,7 +141,33 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) } func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductWarehouse, error) { - productWarehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + applyScope := true + if c.Query(utils.TransferContextKey, "") == utils.TransferContextInventoryTransfer { + applyScope = !m.HasPermission(c, m.P_TransferCreateOne) + } + var scope m.ScopeFilter + var err error + if applyScope { + scope, err = m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, err + } + } + + productWarehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db = db.Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id"). + Where("w_scope.deleted_at IS NULL") + if applyScope { + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where("w_scope.location_id IN ?", scope.IDs) + } + } + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found") } diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go index 322d0a00..61a41ad0 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -13,10 +13,11 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` - ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` - WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` - Flags string `query:"flags" validate:"omitempty"` - KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` + WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` + Flags string `query:"flags" validate:"omitempty"` + KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` + TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"` } diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 8278ce9f..e74332bc 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -94,10 +94,24 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db. + Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id"). + Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id"). + Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs) + } if params.Search != "" { searchTerm := "%" + strings.TrimSpace(params.Search) + "%" db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id"). @@ -116,6 +130,28 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit } func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) { + scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB()) + if err != nil { + return nil, err + } + if scope.Restrict { + if len(scope.IDs) == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found") + } + var count int64 + if err := s.StockTransferRepo.DB().WithContext(c.Context()). + Table("stock_transfers"). + Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id"). + Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id"). + Where("stock_transfers.id = ?", id). + Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs). + Count(&count).Error; err != nil { + return nil, err + } + if count == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found") + } + } transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return s.withRelations(db) diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 6f638ac6..231c00d4 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -200,7 +200,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id"). Where("marketing_delivery_products.delivery_date IS NOT NULL") - if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.Search != "" || filters.MarketingType != "" { + if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.AreaId > 0 || filters.LocationId > 0 || filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil || filters.Search != "" || filters.MarketingType != "" { db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") } @@ -250,7 +250,7 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) } - if filters.AreaId > 0 || filters.LocationId > 0 { + if filters.AreaId > 0 || filters.LocationId > 0 || filters.AllowedAreaIDs != nil || filters.AllowedLocationIDs != nil { db = db.Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = product_warehouses.project_flock_kandang_id"). Joins("LEFT JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id") @@ -261,6 +261,22 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C if filters.LocationId > 0 { db = db.Where("project_flocks.location_id = ?", filters.LocationId) } + + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("project_flocks.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("project_flocks.location_id IN ?", filters.AllowedLocationIDs) + } + } } if filters.MarketingType != "" { diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 7e60662d..2022cc78 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -71,6 +71,10 @@ func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { } func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), marketingId); err != nil { + return nil, err + } + marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") @@ -95,6 +99,11 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { @@ -106,6 +115,23 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where( + `EXISTS ( + SELECT 1 + FROM marketing_products mp + JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id + JOIN warehouses w ON w.id = pw.warehouse_id + WHERE mp.marketing_id = marketings.id + AND w.location_id IN ? + )`, + scope.IDs, + ) + } + if params.MarketingId != 0 { return db.Where("id = ?", params.MarketingId) } @@ -134,6 +160,9 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO } func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { @@ -173,6 +202,10 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery return nil, err } + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), req.MarketingId); err != nil { + return nil, err + } + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists}, ); err != nil { @@ -323,6 +356,10 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO return nil, err } + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, ); err != nil { diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index dbf99219..9d950307 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -70,6 +70,10 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { } func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") @@ -108,6 +112,9 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } for _, item := range req.MarketingProducts { + if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { + return nil, err + } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, ); err != nil { @@ -197,6 +204,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return nil, err } + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } + actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err @@ -219,11 +230,14 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } if len(req.MarketingProducts) > 0 { - for _, item := range req.MarketingProducts { - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, - ); err != nil { - return nil, err + for _, item := range req.MarketingProducts { + if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { + return nil, err + } + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err } } } @@ -414,6 +428,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return err + } + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { @@ -478,6 +496,12 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e return nil, err } + for _, id := range req.ApprovableIds { + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { + return nil, err + } + } + actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err diff --git a/internal/modules/master/areas/services/area.service.go b/internal/modules/master/areas/services/area.service.go index e6f9205c..6110aaef 100644 --- a/internal/modules/master/areas/services/area.service.go +++ b/internal/modules/master/areas/services/area.service.go @@ -47,16 +47,22 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar return nil, 0, err } + var scopeErr error + offset := (params.Page - 1) * params.Limit areas, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db, scopeErr = m.ApplyAreaScope(c, db, "id") if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get areas: %+v", err) return nil, 0, err @@ -65,7 +71,16 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar } func (s areaService) GetOne(c *fiber.Ctx, id uint) (*entity.Area, error) { - area, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + var scopeErr error + + area, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db, scopeErr = m.ApplyAreaScope(c, db, "id") + return db + }) + if scopeErr != nil { + return nil, scopeErr + } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Area not found") } diff --git a/internal/modules/master/employees/services/employees.service.go b/internal/modules/master/employees/services/employees.service.go index b3673eaf..2817a9fa 100644 --- a/internal/modules/master/employees/services/employees.service.go +++ b/internal/modules/master/employees/services/employees.service.go @@ -5,6 +5,7 @@ import ( "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -43,6 +44,61 @@ func (s employeesService) withRelations(db *gorm.DB) *gorm.DB { Where("employees.deleted_at IS NULL") } +func (s employeesService) ensureEmployeeAccess(c *fiber.Ctx, employeeID uint) error { + if employeeID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("employees e"). + Joins("JOIN employee_kandangs ek ON ek.employee_id = e.id"). + Joins("JOIN kandangs k ON k.id = ek.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("e.id = ?", employeeID). + Where("e.deleted_at IS NULL") + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count == 0 { + return fiber.NewError(fiber.StatusNotFound, "Employees not found") + } + return nil +} + +func (s employeesService) ensureKandangIDsAccess(c *fiber.Ctx, kandangIDs []uint) error { + if len(kandangIDs) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") + } + + db := s.Repository.DB().WithContext(c.Context()). + Table("kandangs k"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("k.id IN ?", kandangIDs) + + scopedDB, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return err + } + + var count int64 + if err := scopedDB.Count(&count).Error; err != nil { + return err + } + if count != int64(len(kandangIDs)) { + return fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + return nil +} + func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -52,17 +108,29 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id"). + Joins("JOIN kandangs k ON k.id = ek.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id") + var scopeErr error + db, scopeErr = m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if scopeErr != nil { + return db.Where("1 = 0") + } if params.Search != "" { db = db.Where("employees.name ILIKE ?", "%"+params.Search+"%") } if params.KandangId != nil { - db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id"). - Where("ek.kandang_id = ?", *params.KandangId) + db = db.Where("ek.kandang_id = ?", *params.KandangId) } if params.IsActive != nil { db = db.Where("employees.is_active = ?", *params.IsActive) } - return db.Order("employees.created_at DESC").Order("employees.updated_at DESC") + return db. + Select("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at"). + Group("employees.id, employees.name, employees.is_active, employees.created_at, employees.updated_at"). + Order("employees.created_at DESC"). + Order("employees.updated_at DESC") }) if err != nil { @@ -73,6 +141,9 @@ func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s employeesService) GetOne(c *fiber.Ctx, id uint) (*entity.Employees, error) { + if err := s.ensureEmployeeAccess(c, id); err != nil { + return nil, err + } employees, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found") @@ -98,6 +169,9 @@ func (s *employeesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if len(kandangIDs) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") } + if err := s.ensureKandangIDsAccess(c, kandangIDs); err != nil { + return nil, err + } if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB { return db.Where("LOWER(name) = ?", strings.ToLower(name)) @@ -147,6 +221,9 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := s.ensureEmployeeAccess(c, id); err != nil { + return nil, err + } updateBody := make(map[string]any) var ( @@ -181,6 +258,9 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if len(ids) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id") } + if err := s.ensureKandangIDsAccess(c, ids); err != nil { + return nil, err + } kandangIDs = ids needKandangUpdate = true @@ -234,6 +314,9 @@ func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } func (s employeesService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.ensureEmployeeAccess(c, id); err != nil { + return err + } if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Employees not found") diff --git a/internal/modules/master/kandangs/services/kandang.service.go b/internal/modules/master/kandangs/services/kandang.service.go index 9f83f0ce..159bc410 100644 --- a/internal/modules/master/kandangs/services/kandang.service.go +++ b/internal/modules/master/kandangs/services/kandang.service.go @@ -49,10 +49,13 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity return nil, 0, err } + var scopeErr error + offset := (params.Page - 1) * params.Limit kandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db, scopeErr = m.ApplyLocationScope(c, db, "kandangs.location_id") if params.Search != "" { return db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -65,6 +68,9 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get kandangs: %+v", err) return nil, 0, err @@ -73,7 +79,16 @@ func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } func (s kandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Kandang, error) { - kandang, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + var scopeErr error + + kandang, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db, scopeErr = m.ApplyLocationScope(c, db, "kandangs.location_id") + return db + }) + if scopeErr != nil { + return nil, scopeErr + } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found") } @@ -88,6 +103,9 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureLocationAccess(c, s.Repository.DB(), req.LocationId); err != nil { + return nil, err + } if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil { s.Log.Errorf("Failed to check kandang name: %+v", err) @@ -162,6 +180,14 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureKandangAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + if req.LocationId != nil { + if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil { + return nil, err + } + } existing, err := s.Repository.GetByID(c.Context(), id, nil) if errors.Is(err, gorm.ErrRecordNotFound) { @@ -253,6 +279,10 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } func (s kandangService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureKandangAccess(c, s.Repository.DB(), id); err != nil { + return err + } + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Kandang not found") diff --git a/internal/modules/master/locations/services/location.service.go b/internal/modules/master/locations/services/location.service.go index 3a1d1e23..03f6cf45 100644 --- a/internal/modules/master/locations/services/location.service.go +++ b/internal/modules/master/locations/services/location.service.go @@ -47,10 +47,13 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return nil, 0, err } + var scopeErr error + offset := (params.Page - 1) * params.Limit locations, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + db, scopeErr = m.ApplyLocationScope(c, db, "locations.id") if params.Search != "" { db = db.Where("name ILIKE ?", "%"+params.Search+"%") } @@ -60,6 +63,9 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get locations: %+v", err) return nil, 0, err @@ -68,7 +74,16 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit } func (s locationService) GetOne(c *fiber.Ctx, id uint) (*entity.Location, error) { - location, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + var scopeErr error + + location, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db, scopeErr = m.ApplyLocationScope(c, db, "locations.id") + return db + }) + if scopeErr != nil { + return nil, scopeErr + } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } diff --git a/internal/modules/master/warehouses/controllers/warehouse.controller.go b/internal/modules/master/warehouses/controllers/warehouse.controller.go index 4e93cb52..8d1572fa 100644 --- a/internal/modules/master/warehouses/controllers/warehouse.controller.go +++ b/internal/modules/master/warehouses/controllers/warehouse.controller.go @@ -3,11 +3,13 @@ package controller import ( "math" "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" ) @@ -23,6 +25,11 @@ func NewWarehouseController(warehouseService service.WarehouseService) *Warehous } func (u *WarehouseController) GetAll(c *fiber.Ctx) error { + excludeIDs, err := parseCommaSeparatedUint(c.Query("exclude_id", "")) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + query := &validation.Query{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), @@ -30,6 +37,8 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error { AreaId: c.QueryInt("area_id", 0), LocationId: c.QueryInt("location_id", 0), ActiveProjectFlockOnly: c.QueryBool("active_project_flock", false), + TransferContext: c.Query(utils.TransferContextKey, ""), + ExcludeIDs: excludeIDs, } if query.Page < 1 || query.Limit < 1 { @@ -56,6 +65,28 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error { }) } +func parseCommaSeparatedUint(raw string) ([]uint, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + + parts := strings.Split(raw, ",") + out := make([]uint, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + value, err := strconv.ParseUint(part, 10, 64) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid exclude_id") + } + out = append(out, uint(value)) + } + return out, nil +} + func (u *WarehouseController) GetOne(c *fiber.Ctx) error { param := c.Params("id") diff --git a/internal/modules/master/warehouses/services/warehouse.service.go b/internal/modules/master/warehouses/services/warehouse.service.go index 0730bc48..9d6321b5 100644 --- a/internal/modules/master/warehouses/services/warehouse.service.go +++ b/internal/modules/master/warehouses/services/warehouse.service.go @@ -48,10 +48,20 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } + var scopeErr error + offset := (params.Page - 1) * params.Limit warehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) + applyScope := true + if params.TransferContext == utils.TransferContextInventoryTransfer { + applyScope = !m.HasPermission(c, m.P_TransferCreateOne) + } + + if applyScope { + db, scopeErr = m.ApplyLocationAreaScope(c, db, "warehouses.location_id", "warehouses.area_id") + } if params.Search != "" { db = db.Where("warehouses.name ILIKE ?", "%"+params.Search+"%") } @@ -78,9 +88,15 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti ) `, "Aktif") } + if len(params.ExcludeIDs) > 0 { + db = db.Where("warehouses.id NOT IN ?", params.ExcludeIDs) + } return db.Order("created_at DESC").Order("updated_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get warehouses: %+v", err) return nil, 0, err @@ -89,7 +105,16 @@ func (s warehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s warehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.Warehouse, error) { - warehouse, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + var scopeErr error + + warehouse, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + db, scopeErr = m.ApplyLocationAreaScope(c, db, "warehouses.location_id", "warehouses.area_id") + return db + }) + if scopeErr != nil { + return nil, scopeErr + } if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } @@ -120,6 +145,19 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if err := validateWarehouseTypeRequirements(typ, &req.AreaId, &req.LocationId, &req.KandangId, createValidationOpts); err != nil { return nil, err } + if err := m.EnsureAreaAccess(c, s.Repository.DB(), req.AreaId); err != nil { + return nil, err + } + if req.LocationId != nil { + if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil { + return nil, err + } + } + if req.KandangId != nil { + if err := m.EnsureKandangAccess(c, s.Repository.DB(), *req.KandangId); err != nil { + return nil, err + } + } //? Check relation area, location, and kandang if err := common.EnsureRelations(c.Context(), @@ -158,6 +196,21 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := s.Validate.Struct(req); err != nil { return nil, err } + if req.AreaId != nil { + if err := m.EnsureAreaAccess(c, s.Repository.DB(), *req.AreaId); err != nil { + return nil, err + } + } + if req.LocationId != nil { + if err := m.EnsureLocationAccess(c, s.Repository.DB(), *req.LocationId); err != nil { + return nil, err + } + } + if req.KandangId != nil { + if err := m.EnsureKandangAccess(c, s.Repository.DB(), *req.KandangId); err != nil { + return nil, err + } + } existing, err := s.GetOne(c, id) if err != nil { @@ -248,6 +301,10 @@ func (s warehouseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } func (s warehouseService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureWarehouseAccess(c, s.Repository.DB(), id); err != nil { + return err + } + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Warehouse not found") diff --git a/internal/modules/master/warehouses/validations/warehouse.validation.go b/internal/modules/master/warehouses/validations/warehouse.validation.go index be796082..2a2a9f87 100644 --- a/internal/modules/master/warehouses/validations/warehouse.validation.go +++ b/internal/modules/master/warehouses/validations/warehouse.validation.go @@ -23,4 +23,6 @@ type Query struct { AreaId int `query:"area_id" validate:"omitempty,number,gt=0"` LocationId int `query:"location_id" validate:"omitempty,number,gt=0"` ActiveProjectFlockOnly bool `query:"active_project_flock"` + ExcludeIDs []uint `query:"-" validate:"omitempty,dive,gt=0"` + TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"` } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 6f019ffa..61a593d5 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -88,9 +88,14 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit - projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) + projectFlockKandangs, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) @@ -106,6 +111,28 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, nil, nil, err + } + if scope.Restrict { + if len(scope.IDs) == 0 { + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + var count int64 + if err := s.Repository.DB().WithContext(c.Context()). + Table("project_flock_kandangs"). + Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id"). + Where("project_flock_kandangs.id = ?", id). + Where("project_flocks.location_id IN ?", scope.IDs). + Count(&count).Error; err != nil { + return nil, nil, nil, err + } + if count == 0 { + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + } + } + projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 8c5a9298..3df6ad45 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -12,6 +12,7 @@ import ( service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/gofiber/fiber/v2" ) @@ -76,6 +77,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { query.Category = category } + query.TransferContext = c.Query(utils.TransferContextKey, "") if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" { ids, err := parseUintList(kandangRaw) diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index e65dfb4a..346f2176 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -14,6 +14,7 @@ import ( type ProjectflockRepository interface { repository.BaseRepository[entity.ProjectFlock] GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) + GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) @@ -48,6 +49,19 @@ func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offs }) } +func (r *ProjectflockRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]entity.ProjectFlock, int64, error) { + return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { + db = r.applyQueryFilters(r.WithDefaultRelations()(db), params) + if restrict { + if len(locationIDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where("project_flocks.location_id IN ?", locationIDs) + } + return db + }) +} + func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db. diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index f5b55a78..9f5bb0e2 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -20,6 +20,7 @@ type ProjectFlockKandangRepository interface { DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) + GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) @@ -197,6 +198,104 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex return records, total, nil } +func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) { + var records []entity.ProjectFlockKandang + var total int64 + + query, ok := params.(*validation.Query) + + q := r.db.WithContext(ctx). + Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). + Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). + Preload("ProjectFlock"). + Preload("ProjectFlock.Fcr"). + Preload("ProjectFlock.Area"). + Preload("ProjectFlock.Location"). + Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.Kandangs"). + Preload("ProjectFlock.KandangHistory"). + Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse") + + if restrict { + if len(locationIDs) == 0 { + return []entity.ProjectFlockKandang{}, 0, nil + } + q = q.Where("\"project_flocks\".\"location_id\" IN ?", locationIDs) + } + + if ok && query != nil && query.StepName != "" { + q = q.Where(` + EXISTS ( + SELECT 1 FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + AND LOWER("approvals"."step_name") = LOWER(?) + AND "approvals"."id" IN ( + SELECT "approvals"."id" FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + ORDER BY "approvals"."id" DESC + LIMIT 1 + ) + ) + `, "PROJECT_FLOCK_KANDANGS", query.StepName, "PROJECT_FLOCK_KANDANGS") + } + + if ok && query != nil { + if query.Search != "" { + escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(query.Search) + q = q.Where( + r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"). + Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"), + ) + } + + if query.ProjectFlockId > 0 { + q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", query.ProjectFlockId) + } + + if query.KandangId > 0 { + q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", query.KandangId) + } + + if query.Category != "" { + q = q.Where("\"project_flocks\".\"category\" = ?", query.Category) + } + + if query.AreaId > 0 { + q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId) + } + } + + if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" + if ok && query != nil && query.SortBy != "" { + sortOrder := "DESC" + if query.SortOrder == "ASC" { + sortOrder = "ASC" + } + + switch query.SortBy { + case "created_at": + sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder + case "period": + sortBy = "\"project_flocks\".\"period\" " + sortOrder + } + } + + if err := q.Order(sortBy).Offset(offset).Limit(limit).Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil +} + func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { return &projectFlockKandangRepositoryImpl{db: tx} } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 21925a24..96e4b6b0 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -117,9 +117,20 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e return nil, 0, nil, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, nil, err + } + if params.TransferContext == utils.TransferContextTransferToLaying { + if m.HasPermission(c, m.P_TransferToLaying_CreateOne) || m.HasPermission(c, m.P_TransferToLaying_UpdateOne) { + scope.Restrict = false + scope.IDs = nil + } + } + offset := (params.Page - 1) * params.Limit - projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) + projectflocks, total, err := s.Repository.GetAllWithFiltersScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) @@ -193,7 +204,16 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr } func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error) { - projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, nil, err + } + + projectflock, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.Repository.WithDefaultRelations()(db) + db = m.ApplyScopeFilter(db, scope, "project_flocks.location_id") + return db + }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index 5b2a9407..1fb48abe 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -12,16 +12,17 @@ type Create struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` - Search string `query:"search" validate:"omitempty,max=50"` - SortBy string `query:"sort_by" validate:"omitempty"` - SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` - AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` - LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` - Period int `query:"period" validate:"omitempty,number,gt=0"` - Category string `query:"category" validate:"omitempty"` - KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + Search string `query:"search" validate:"omitempty,max=50"` + SortBy string `query:"sort_by" validate:"omitempty"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` + LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` + Period int `query:"period" validate:"omitempty,number,gt=0"` + Category string `query:"category" validate:"omitempty"` + KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` + TransferContext string `query:"transfer_context" validate:"omitempty,oneof=transfer_to_laying"` } type Approve struct { diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 0499bb17..61f96e81 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -108,6 +108,13 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } + var scopeErr error + if params.ProjectFlockKandangId != 0 { + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, params.ProjectFlockKandangId); err != nil { + return nil, 0, err + } + } + limit := params.Limit if limit == 0 { limit = 10 @@ -120,6 +127,10 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti recordings, total, err := s.Repository.GetAll(c.Context(), offset, limit, func(db *gorm.DB) *gorm.DB { db = s.Repository.WithRelations(db) + db = db. + Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") + db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id") if params.ProjectFlockKandangId != 0 { db = db.Where("project_flock_kandangs_id = ?", params.ProjectFlockKandangId) } @@ -127,6 +138,9 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC") }) + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get recordings: %+v", err) return nil, 0, err @@ -141,6 +155,10 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) { + if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + recording, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return s.Repository.WithRelations(db) }) @@ -164,6 +182,9 @@ func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint, r if projectFlockKandangId == 0 { return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") } + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, projectFlockKandangId); err != nil { + return 0, err + } if recordTime.IsZero() { recordTime = time.Now().UTC() @@ -181,6 +202,9 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, req.ProjectFlockKandangId); err != nil { + return nil, err + } ctx := c.Context() recordTime := time.Now().UTC() @@ -352,6 +376,9 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } if req.Stocks == nil && req.Depletions == nil && req.Eggs == nil { return s.GetOne(c, id) @@ -633,6 +660,11 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent if err := s.Validate.Struct(req); err != nil { return nil, err } + for _, id := range req.ApprovableIds { + if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + } actionValue := strings.ToUpper(strings.TrimSpace(req.Action)) var action entity.ApprovalAction @@ -710,6 +742,9 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent } func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureRecordingAccess(c, s.Repository.DB(), id); err != nil { + return err + } ctx := c.Context() actorID, err := m.ActorIDFromContext(c) if err != nil { @@ -1445,6 +1480,7 @@ func ensureRecordingEggsUnused(eggs []entity.RecordingEgg) error { } func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool { + existingUsage := make(map[uint]float64) for _, stock := range existing { var usage float64 diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go index 14fa4118..ebf63252 100644 --- a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -52,6 +52,8 @@ type GetAllFilterParams struct { FlockSource []uint FlockDestination []uint Status []string + LocationIDs []uint + LocationRestrict bool } func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params *GetAllFilterParams) ([]entity.LayingTransfer, int64, error) { @@ -110,6 +112,25 @@ func (r *TransferLayingRepositoryImpl) GetAllWithFilters(ctx context.Context, of } } + if params.LocationRestrict { + if len(params.LocationIDs) == 0 { + q = q.Where("1 = 0") + } else { + q = q.Where( + `EXISTS ( + SELECT 1 FROM project_flocks pf + WHERE pf.id = laying_transfers.from_project_flock_id + AND pf.location_id IN ? + ) OR EXISTS ( + SELECT 1 FROM project_flocks pf + WHERE pf.id = laying_transfers.to_project_flock_id + AND pf.location_id IN ? + )`, + params.LocationIDs, params.LocationIDs, + ) + } + } + if err := q.Count(&total).Error; err != nil { return nil, 0, err } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 3aa4788b..310391c6 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -107,6 +107,11 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit filterParams := &repository.GetAllFilterParams{ @@ -116,6 +121,8 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ FlockSource: params.FlockSource, FlockDestination: params.FlockDestination, Status: params.Status, + LocationIDs: scope.IDs, + LocationRestrict: scope.Restrict, } transferLayings, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, filterParams) @@ -124,11 +131,6 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } - if err != nil { - s.Log.Errorf("Failed to get transferLayings: %+v", err) - return nil, 0, err - } - approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) for i, transfer := range transferLayings { latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, func(db *gorm.DB) *gorm.DB { @@ -168,6 +170,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) if err := s.Validate.Struct(req); err != nil { return nil, err } + if !m.HasPermission(c, m.P_TransferToLaying_CreateOne) { + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil { + return nil, err + } + } actorID, err := m.ActorIDFromContext(c) if err != nil { @@ -396,6 +403,11 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, if err := s.Validate.Struct(req); err != nil { return nil, err } + if !m.HasPermission(c, m.P_TransferToLaying_UpdateOne) { + if err := m.EnsureProjectFlockAccess(c, s.Repository.DB(), req.TargetProjectFlockId); err != nil { + return nil, err + } + } existingTransfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return db.Preload("Sources.ProductWarehouse").Preload("Targets") @@ -571,6 +583,9 @@ func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, } func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), id); err != nil { + return err + } _, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { return db.Preload("Sources.ProductWarehouse").Preload("Targets") @@ -641,6 +656,12 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } + for _, approvableID := range approvableIDs { + if err := m.EnsureLayingTransferAccess(c, s.Repository.DB(), approvableID); err != nil { + return nil, err + } + } + step := utils.TransferToLayingStepPengajuan if action == entity.ApprovalActionApproved { step = utils.TransferToLayingStepDisetujui diff --git a/internal/modules/production/uniformities/repositories/uniformity.repository.go b/internal/modules/production/uniformities/repositories/uniformity.repository.go index 9641c650..8453bf84 100644 --- a/internal/modules/production/uniformities/repositories/uniformity.repository.go +++ b/internal/modules/production/uniformities/repositories/uniformity.repository.go @@ -12,7 +12,7 @@ import ( type UniformityRepository interface { repository.BaseRepository[entity.ProjectFlockKandangUniformity] - GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) + GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query, modifiers ...func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandangUniformity, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error } @@ -27,9 +27,15 @@ func NewUniformityRepository(db *gorm.DB) UniformityRepository { } } -func (r *UniformityRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) { +func (r *UniformityRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query, modifiers ...func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandangUniformity, int64, error) { return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { - return r.applyQueryFilters(r.WithDefaultRelations()(db), params) + db = r.applyQueryFilters(r.WithDefaultRelations()(db), params) + for _, modifier := range modifiers { + if modifier != nil { + db = modifier(db) + } + } + return db }) } diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 79e4d3e7..1e4ccbd5 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -87,9 +87,20 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent return nil, 0, err } - offset := (params.Page - 1) * params.Limit - uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) + var scopeErr error + offset := (params.Page - 1) * params.Limit + uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params, func(db *gorm.DB) *gorm.DB { + db = db. + Joins("JOIN project_flock_kandangs pfk ON pfk.id = project_flock_kandang_uniformity.project_flock_kandang_id"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") + db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id") + return db + }) + + if scopeErr != nil { + return nil, 0, scopeErr + } if err != nil { s.Log.Errorf("Failed to get uniformitys: %+v", err) return nil, 0, err @@ -101,6 +112,10 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent } func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) { + if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + uniformity, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found") @@ -326,6 +341,9 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureProjectFlockKandangAccess(c, s.Repository.DB(), 0, req.ProjectFlockKandangId); err != nil { + return nil, err + } if s.ProjectFlockKandangRepo == nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available") } @@ -484,6 +502,9 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui if err := s.Validate.Struct(req); err != nil { return nil, err } + if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } updateBody := make(map[string]any) var uniformDate *time.Time @@ -699,6 +720,10 @@ func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, } func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil { + return err + } + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") @@ -729,6 +754,11 @@ func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]en if len(ids) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } + for _, id := range ids { + if err := m.EnsureUniformityAccess(c, s.Repository.DB(), id); err != nil { + return nil, err + } + } step := utils.UniformityStepPengajuan if action == entity.ApprovalActionApproved { diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 572069ed..b789ef34 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -125,6 +125,11 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, err } + scope, err := m.ResolveLocationScope(c, s.PurchaseRepo.DB()) + if err != nil { + return nil, 0, err + } + offset := (params.Page - 1) * params.Limit createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo) @@ -148,6 +153,21 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti db = db.Where("created_at < ?", *createdTo) } + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where( + `EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN warehouses w ON w.id = pi.warehouse_id + WHERE pi.purchase_id = purchases.id AND w.location_id IN ? + )`, + scope.IDs, + ) + } + if params.AreaID > 0 { db = db.Where( `EXISTS ( @@ -202,7 +222,42 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error) { - return s.loadPurchase(c.Context(), id) + scope, err := m.ResolveLocationScope(c, s.PurchaseRepo.DB()) + if err != nil { + return nil, err + } + + purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if scope.Restrict { + if len(scope.IDs) == 0 { + return db.Where("1 = 0") + } + db = db.Where( + `EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN warehouses w ON w.id = pi.warehouse_id + WHERE pi.purchase_id = purchases.id AND w.location_id IN ? + )`, + scope.IDs, + ) + } + return db + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, utils.NotFound("Purchase not found") + } + s.Log.Errorf("Failed to get purchase %d: %+v", id, err) + return nil, utils.Internal("Failed to get purchase") + } + if err := s.attachLatestApproval(c.Context(), purchase); err != nil { + s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err) + } + s.applyTravelDocumentURLs(c.Context(), purchase) + + return purchase, nil } func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) { diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index c1982279..9becdf87 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -5,6 +5,7 @@ import ( "strconv" "strings" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -49,6 +50,21 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { RealizationDate: ctx.Query("realization_date", ""), } + locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if locationScope.Restrict { + query.AllowedLocationIDs = toInt64Slice(locationScope.IDs) + } + if areaScope.Restrict { + query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -92,6 +108,29 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { SortOrder: ctx.Query("sort_order", ""), } + locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if locationScope.Restrict { + allowed := toInt64Slice(locationScope.IDs) + if len(allowed) == 0 { + allowed = []int64{-1} + } + query.AllowedLocationIDs = allowed + } + if areaScope.Restrict { + allowed := toInt64Slice(areaScope.IDs) + if len(allowed) == 0 { + allowed = []int64{-1} + } + query.AllowedAreaIDs = allowed + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -132,6 +171,14 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { FilterBy: ctx.Query("filter_by", ""), } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if areaScope.Restrict { + query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -184,6 +231,21 @@ func (c *RepportController) GetDebtSupplier(ctx *fiber.Ctx) error { SortOrder: ctx.Query("sort_order", ""), } + locationScope, err := m.ResolveLocationScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + areaScope, err := m.ResolveAreaScope(ctx, c.RepportService.DB()) + if err != nil { + return err + } + if locationScope.Restrict { + query.AllowedLocationIDs = toInt64Slice(locationScope.IDs) + } + if areaScope.Restrict { + query.AllowedAreaIDs = toInt64Slice(areaScope.IDs) + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -320,6 +382,10 @@ func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error { ProjectFlockKandangID: uint(projectFlockKandangID), } + if err := m.EnsureProjectFlockKandangAccess(ctx, c.RepportService.DB(), 0, query.ProjectFlockKandangID); err != nil { + return err + } + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -367,3 +433,14 @@ func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return result, nil } + +func toInt64Slice(ids []uint) []int64 { + if len(ids) == 0 { + return nil + } + out := make([]int64, 0, len(ids)) + for _, id := range ids { + out = append(out, int64(id)) + } + return out +} diff --git a/internal/modules/repports/repositories/customer_payment.repository.go b/internal/modules/repports/repositories/customer_payment.repository.go index 8a5747aa..9004882b 100644 --- a/internal/modules/repports/repositories/customer_payment.repository.go +++ b/internal/modules/repports/repositories/customer_payment.repository.go @@ -30,7 +30,7 @@ type CustomerPaymentTransaction struct { type CustomerPaymentRepository interface { GetCustomerPaymentTransactions(ctx context.Context, customerID *uint) ([]CustomerPaymentTransaction, error) GetInitialBalanceByCustomer(ctx context.Context, customerID uint) (float64, error) - GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) + GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int, allowedCustomerIDs []uint) ([]uint, int64, error) } type customerPaymentRepositoryImpl struct { @@ -146,7 +146,7 @@ func (r *customerPaymentRepositoryImpl) GetInitialBalanceByCustomer(ctx context. return result.Nominal, nil } -func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int) ([]uint, int64, error) { +func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx context.Context, limit, offset int, allowedCustomerIDs []uint) ([]uint, int64, error) { subQuery := r.db.WithContext(ctx). Table("(" + "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " + @@ -161,26 +161,36 @@ func (r *customerPaymentRepositoryImpl) GetCustomerIDsWithTransactions(ctx conte "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" + ") as customer_ids") + if len(allowedCustomerIDs) > 0 { + subQuery = subQuery.Where("customer_id IN ?", allowedCustomerIDs) + } + var total int64 if err := subQuery.Count(&total).Error; err != nil { return nil, 0, err } var customerIDs []uint - err := r.db.WithContext(ctx). - Table("("+ - "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp "+ - "INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id "+ - "INNER JOIN marketings m ON m.id = mp.marketing_id "+ - "INNER JOIN customers c ON c.id = m.customer_id "+ - "WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL "+ - "UNION "+ - "SELECT DISTINCT c.id as customer_id FROM payments p "+ - "INNER JOIN customers c ON c.id = p.party_id "+ - "WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' "+ - "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL"+ + query := r.db.WithContext(ctx). + Table("(" + + "SELECT DISTINCT c.id as customer_id FROM marketing_delivery_products mdp " + + "INNER JOIN marketing_products mp ON mp.id = mdp.marketing_product_id " + + "INNER JOIN marketings m ON m.id = mp.marketing_id " + + "INNER JOIN customers c ON c.id = m.customer_id " + + "WHERE mdp.delivery_date IS NOT NULL AND m.deleted_at IS NULL AND c.deleted_at IS NULL " + + "UNION " + + "SELECT DISTINCT c.id as customer_id FROM payments p " + + "INNER JOIN customers c ON c.id = p.party_id " + + "WHERE p.party_type = 'CUSTOMER' AND p.direction = 'IN' " + + "AND p.transaction_type = 'PENJUALAN' AND p.deleted_at IS NULL AND c.deleted_at IS NULL" + ") as customer_ids"). - Select("customer_id"). + Select("customer_id") + + if len(allowedCustomerIDs) > 0 { + query = query.Where("customer_id IN ?", allowedCustomerIDs) + } + + err := query. Order("customer_id ASC"). Limit(limit). Offset(offset). diff --git a/internal/modules/repports/repositories/debt_supplier.repository.go b/internal/modules/repports/repositories/debt_supplier.repository.go index 74039ebf..fefcbade 100644 --- a/internal/modules/repports/repositories/debt_supplier.repository.go +++ b/internal/modules/repports/repositories/debt_supplier.repository.go @@ -70,6 +70,7 @@ func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filt Model(&entity.Supplier{}). Joins("JOIN purchases ON purchases.supplier_id = suppliers.id"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN warehouses w ON w.id = purchase_items.warehouse_id"). Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). @@ -79,6 +80,22 @@ func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filt db = db.Where("suppliers.id IN ?", filters.SupplierIDs) } + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("w.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("w.location_id IN ?", filters.AllowedLocationIDs) + } + } + if filters.StartDate != "" { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) @@ -226,12 +243,29 @@ func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplie Table("purchases"). Select("DISTINCT purchases.id"). Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id"). + Joins("JOIN warehouses w ON w.id = purchase_items.warehouse_id"). Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)). Where("purchases.supplier_id IN ?", supplierIDs). Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)). Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)). Where("purchase_items.received_date IS NOT NULL") + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("w.area_id IN ?", filters.AllowedAreaIDs) + } + } + + if filters.AllowedLocationIDs != nil { + if len(filters.AllowedLocationIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("w.location_id IN ?", filters.AllowedLocationIDs) + } + } + if filters.StartDate != "" { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom) diff --git a/internal/modules/repports/repositories/purchase_supplier.repository.go b/internal/modules/repports/repositories/purchase_supplier.repository.go index 6a07c555..3206eaa5 100644 --- a/internal/modules/repports/repositories/purchase_supplier.repository.go +++ b/internal/modules/repports/repositories/purchase_supplier.repository.go @@ -74,10 +74,18 @@ func (r *purchaseSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, Where("products.product_category_id = ?", filters.ProductCategoryId) } - if filters.AreaId > 0 { - db = db. - Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). - Where("warehouses.area_id = ?", filters.AreaId) + if filters.AreaId > 0 || filters.AllowedAreaIDs != nil { + db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id") + if filters.AreaId > 0 { + db = db.Where("warehouses.area_id = ?", filters.AreaId) + } + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("warehouses.area_id IN ?", filters.AllowedAreaIDs) + } + } } if filters.StartDate != "" { @@ -189,10 +197,18 @@ func (r *purchaseSupplierRepositoryImpl) GetItemsBySuppliers(ctx context.Context Where("products.product_category_id = ?", filters.ProductCategoryId) } - if filters.AreaId > 0 { - db = db. - Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id"). - Where("warehouses.area_id = ?", filters.AreaId) + if filters.AreaId > 0 || filters.AllowedAreaIDs != nil { + db = db.Joins("JOIN warehouses ON warehouses.id = purchase_items.warehouse_id") + if filters.AreaId > 0 { + db = db.Where("warehouses.area_id = ?", filters.AreaId) + } + if filters.AllowedAreaIDs != nil { + if len(filters.AllowedAreaIDs) == 0 { + db = db.Where("1 = 0") + } else { + db = db.Where("warehouses.area_id IN ?", filters.AllowedAreaIDs) + } + } } if filters.StartDate != "" { diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index fb5d72ba..d45cba62 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -11,6 +11,7 @@ import ( "strings" "time" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" @@ -45,12 +46,13 @@ type RepportService interface { GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) + DB() *gorm.DB } type repportService struct { Log *logrus.Logger Validate *validator.Validate - DB *gorm.DB + db *gorm.DB ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository @@ -99,7 +101,7 @@ func NewRepportService( return &repportService{ Log: utils.Log, Validate: validate, - DB: db, + db: db, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, @@ -118,6 +120,10 @@ func NewRepportService( } } +func (s *repportService) DB() *gorm.DB { + return s.db +} + func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -405,11 +411,38 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C return nil, 0, err } + locationScope, err := m.ResolveLocationScope(ctx, s.DB()) + if err != nil { + return nil, 0, err + } + areaScope, err := m.ResolveAreaScope(ctx, s.DB()) + if err != nil { + return nil, 0, err + } + + restrictScope := locationScope.Restrict || areaScope.Restrict + var allowedCustomerIDs []uint + if restrictScope { + if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) { + return []dto.CustomerPaymentReportItem{}, 0, nil + } + allowedCustomerIDs, err = s.getCustomerIDsByScope(ctx.Context(), locationScope.IDs, areaScope.IDs) + if err != nil { + return nil, 0, err + } + if len(allowedCustomerIDs) == 0 { + return []dto.CustomerPaymentReportItem{}, 0, nil + } + } + var customerIDs []uint var totalCustomers int64 if len(params.CustomerIDs) > 0 { customerIDs = params.CustomerIDs + if restrictScope { + customerIDs = intersectUint(customerIDs, allowedCustomerIDs) + } totalCustomers = int64(len(customerIDs)) if len(customerIDs) == 0 { @@ -428,7 +461,7 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C offset := (page - 1) * limit var err error - customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset) + customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset, allowedCustomerIDs) if err != nil { return nil, 0, err } @@ -454,6 +487,37 @@ func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.C return result, totalCustomers, nil } +func (s *repportService) getCustomerIDsByScope(ctx context.Context, locationIDs, areaIDs []uint) ([]uint, error) { + if len(locationIDs) == 0 && len(areaIDs) == 0 { + return []uint{}, nil + } + + db := s.db.WithContext(ctx). + Table("customers c"). + Select("DISTINCT c.id"). + Joins("JOIN marketings m ON m.customer_id = c.id"). + Joins("JOIN marketing_products mp ON mp.marketing_id = m.id"). + Joins("JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id"). + Joins("JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id"). + Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). + Where("mdp.delivery_date IS NOT NULL"). + Where("m.deleted_at IS NULL"). + Where("c.deleted_at IS NULL") + + if len(locationIDs) > 0 { + db = db.Where("w.location_id IN ?", locationIDs) + } + if len(areaIDs) > 0 { + db = db.Where("w.area_id IN ?", areaIDs) + } + + var customerIDs []uint + if err := db.Pluck("c.id", &customerIDs).Error; err != nil { + return nil, err + } + return customerIDs, nil +} + func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) { customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil) @@ -801,7 +865,7 @@ func (s *repportService) getUniformityByWeek(ctx context.Context, projectFlockKa } var rows []entity.ProjectFlockKandangUniformity - if err := s.DB.WithContext(ctx). + if err := s.db.WithContext(ctx). Model(&entity.ProjectFlockKandangUniformity{}). Select("week, uniformity, uniform_date, id, chart_data"). Where("project_flock_kandang_id = ?", projectFlockKandangID). @@ -1886,6 +1950,36 @@ func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.Hp return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } + locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, dto.HppPerKandangFiltersDTO{}, err + } + areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB()) + if err != nil { + return nil, dto.HppPerKandangFiltersDTO{}, err + } + + if locationScope.Restrict { + allowed := toInt64Slice(locationScope.IDs) + if len(allowed) == 0 { + locationIDs = []int64{-1} + } else if len(locationIDs) > 0 { + locationIDs = intersectInt64(locationIDs, allowed) + } else { + locationIDs = allowed + } + } + if areaScope.Restrict { + allowed := toInt64Slice(areaScope.IDs) + if len(allowed) == 0 { + areaIDs = []int64{-1} + } else if len(areaIDs) > 0 { + areaIDs = intersectInt64(areaIDs, allowed) + } else { + areaIDs = allowed + } + } + weightMin, err := parseOptionalFloat64(rawWeightMin) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) @@ -1952,6 +2046,51 @@ func parseCommaSeparatedInt64s(raw string) ([]int64, error) { return result, nil } +func toInt64Slice(ids []uint) []int64 { + if len(ids) == 0 { + return nil + } + out := make([]int64, 0, len(ids)) + for _, id := range ids { + out = append(out, int64(id)) + } + return out +} + +func intersectInt64(a, b []int64) []int64 { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[int64]struct{}, len(b)) + for _, id := range b { + set[id] = struct{}{} + } + out := make([]int64, 0, len(a)) + for _, id := range a { + if _, ok := set[id]; ok { + out = append(out, id) + } + } + return out +} + +func intersectUint(a, b []uint) []uint { + if len(a) == 0 || len(b) == 0 { + return nil + } + set := make(map[uint]struct{}, len(b)) + for _, id := range b { + set[id] = struct{}{} + } + out := make([]uint, 0, len(a)) + for _, id := range a { + if _, ok := set[id]; ok { + out = append(out, id) + } + } + return out +} + func parseOptionalFloat64(raw string) (*float64, error) { raw = strings.TrimSpace(raw) if raw == "" { diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 48024dbc..37c581d9 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -1,59 +1,66 @@ package validation type ExpenseQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - Category string `query:"category" validate:"omitempty,oneof=BOP NON-BOP"` - SupplierId int64 `query:"supplier_id" validate:"omitempty"` - KandangId int64 `query:"kandang_id" validate:"omitempty"` - ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` - ProjectFlockId int64 `query:"project_flock_id" validate:"omitempty"` - NonstockId int64 `query:"nonstock_id" validate:"omitempty"` - AreaId int64 `query:"area_id" validate:"omitempty"` - LocationId int64 `query:"location_id" validate:"omitempty"` - RealizationDate string `query:"realization_date" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + Category string `query:"category" validate:"omitempty,oneof=BOP NON-BOP"` + SupplierId int64 `query:"supplier_id" validate:"omitempty"` + KandangId int64 `query:"kandang_id" validate:"omitempty"` + ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` + ProjectFlockId int64 `query:"project_flock_id" validate:"omitempty"` + NonstockId int64 `query:"nonstock_id" validate:"omitempty"` + AreaId int64 `query:"area_id" validate:"omitempty"` + LocationId int64 `query:"location_id" validate:"omitempty"` + RealizationDate string `query:"realization_date" validate:"omitempty"` + AllowedAreaIDs []int64 `query:"-"` + AllowedLocationIDs []int64 `query:"-"` } type MarketingQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - CustomerId int64 `query:"customer_id" validate:"omitempty"` - ProductId int64 `query:"product_id" validate:"omitempty"` - WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` - SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` - AreaId int64 `query:"area_id" validate:"omitempty"` - LocationId int64 `query:"location_id" validate:"omitempty"` - MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"` - FilterBy string `query:"filter_by" validate:"omitempty,oneof= so_date realization_date"` - StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` - EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` - SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` - SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + CustomerId int64 `query:"customer_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` + SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` + AreaId int64 `query:"area_id" validate:"omitempty"` + LocationId int64 `query:"location_id" validate:"omitempty"` + MarketingType string `query:"marketing_type" validate:"omitempty,oneof=ayam telur trading"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof= so_date realization_date"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=so_date realization_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + AllowedAreaIDs []int64 `query:"-"` + AllowedLocationIDs []int64 `query:"-"` } type PurchaseSupplierQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` - AreaId int64 `query:"area_id" validate:"omitempty"` - SupplierId int64 `query:"supplier_id" validate:"omitempty"` - ProductId int64 `query:"product_id" validate:"omitempty"` - ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` - StartDate string `query:"start_date" validate:"omitempty"` - EndDate string `query:"end_date" validate:"omitempty"` - SortBy string `query:"sort_by" validate:"omitempty"` - FilterBy string `query:"filter_by" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` + AreaId int64 `query:"area_id" validate:"omitempty"` + SupplierId int64 `query:"supplier_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + ProductCategoryId int64 `query:"product_category_id" validate:"omitempty"` + StartDate string `query:"start_date" validate:"omitempty"` + EndDate string `query:"end_date" validate:"omitempty"` + SortBy string `query:"sort_by" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty"` + AllowedAreaIDs []int64 `query:"-"` } type DebtSupplierQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` - SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"` - StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` - EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` - FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date"` - SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,gt=0"` + SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` + AllowedAreaIDs []int64 `query:"-"` + AllowedLocationIDs []int64 `query:"-"` } type HppPerKandangQuery struct { diff --git a/internal/modules/sso/controllers/master_data.controller.go b/internal/modules/sso/controllers/master_data.controller.go new file mode 100644 index 00000000..2364011f --- /dev/null +++ b/internal/modules/sso/controllers/master_data.controller.go @@ -0,0 +1,331 @@ +package controllers + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" + + "gitlab.com/mbugroup/lti-api.git/internal/config" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" + "gitlab.com/mbugroup/lti-api.git/internal/response" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) + +type MasterDataController struct { + db *gorm.DB + redis *redis.Client + clients map[string]config.SSOClientConfig + drift time.Duration + nonceTTL time.Duration + localNonce sync.Map +} + +type masterArea struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +type masterLocation struct { + ID uint `json:"id"` + Name string `json:"name"` + AreaID uint `json:"area_id"` +} + +func NewMasterDataController(db *gorm.DB, redis *redis.Client, clients map[string]config.SSOClientConfig) *MasterDataController { + normalized := make(map[string]config.SSOClientConfig, len(clients)) + for alias, cfg := range clients { + alias = strings.ToLower(strings.TrimSpace(alias)) + normalized[alias] = cfg + } + + drift := config.SSOUserSyncDrift + if drift <= 0 { + drift = 2 * time.Minute + } + + nonceTTL := config.SSOUserSyncNonceTTL + if nonceTTL <= 0 { + nonceTTL = 10 * time.Minute + } + + return &MasterDataController{ + db: db, + redis: redis, + clients: normalized, + drift: drift, + nonceTTL: nonceTTL, + } +} + +func (h *MasterDataController) GetAreas(c *fiber.Ctx) error { + if _, _, err := h.authenticate(c, nil); err != nil { + return err + } + + search := strings.TrimSpace(c.Query("search", "")) + ids := parseUintList(c.Query("ids", "")) + + query := h.db.WithContext(c.Context()). + Model(&entity.Area{}). + Where("deleted_at IS NULL") + if search != "" { + query = query.Where("name ILIKE ?", "%"+search+"%") + } + if len(ids) > 0 { + query = query.Where("id IN ?", ids) + } + + var areas []masterArea + if err := query.Order("name ASC").Find(&areas).Error; err != nil { + utils.Log.WithError(err).Error("failed to fetch areas for master data") + return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch areas") + } + + return c.Status(fiber.StatusOK).JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get areas successfully", + Data: areas, + }) +} + +func (h *MasterDataController) GetLocations(c *fiber.Ctx) error { + if _, _, err := h.authenticate(c, nil); err != nil { + return err + } + + search := strings.TrimSpace(c.Query("search", "")) + areaIDs := parseUintList(c.Query("area_ids", "")) + ids := parseUintList(c.Query("ids", "")) + + query := h.db.WithContext(c.Context()). + Model(&entity.Location{}). + Where("deleted_at IS NULL") + if search != "" { + query = query.Where("name ILIKE ?", "%"+search+"%") + } + if len(areaIDs) > 0 { + query = query.Where("area_id IN ?", areaIDs) + } + if len(ids) > 0 { + query = query.Where("id IN ?", ids) + } + + var locations []masterLocation + if err := query.Order("name ASC").Find(&locations).Error; err != nil { + utils.Log.WithError(err).Error("failed to fetch locations for master data") + return fiber.NewError(fiber.StatusInternalServerError, "failed to fetch locations") + } + + return c.Status(fiber.StatusOK).JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get locations successfully", + Data: locations, + }) +} + +func (h *MasterDataController) authenticate(c *fiber.Ctx, body []byte) (string, config.SSOClientConfig, error) { + rawAlias := strings.TrimSpace(c.Get("X-Sync-Client")) + if rawAlias == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing sync client header") + } + + aliasKey := strings.ToLower(rawAlias) + clientCfg, ok := h.clients[aliasKey] + if !ok { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "unknown sync client") + } + + if err := h.verifyAuthorization(c, aliasKey); err != nil { + return "", config.SSOClientConfig{}, err + } + + secret := strings.TrimSpace(clientCfg.SyncSecret) + if secret == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "sync secret not configured") + } + + timestamp := strings.TrimSpace(c.Get("X-Sync-Timestamp")) + nonce := strings.TrimSpace(c.Get("X-Sync-Nonce")) + signature := strings.TrimSpace(c.Get("X-Sync-Signature")) + if timestamp == "" || nonce == "" || signature == "" { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "missing signature headers") + } + if len(nonce) < 16 { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "nonce too short") + } + + ts, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusBadRequest, "invalid timestamp") + } + + msgTime := time.Unix(ts, 0).UTC() + now := time.Now().UTC() + drift := now.Sub(msgTime) + if drift > h.drift || drift < -h.drift { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "timestamp outside allowed window") + } + + providedSig, err := decodeMasterSignature(signature) + if err != nil { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature encoding") + } + + expectedSignature := calculateSignature(secret, rawAlias, timestamp, nonce, body) + if !hmac.Equal(providedSig, expectedSignature) { + return "", config.SSOClientConfig{}, fiber.NewError(fiber.StatusUnauthorized, "invalid signature") + } + + if err := h.registerNonce(c.Context(), aliasKey, nonce); err != nil { + return "", config.SSOClientConfig{}, err + } + + return aliasKey, clientCfg, nil +} + +func (h *MasterDataController) verifyAuthorization(c *fiber.Ctx, alias string) error { + authHeader := strings.TrimSpace(c.Get(fiber.HeaderAuthorization)) + if authHeader == "" { + return fiber.NewError(fiber.StatusUnauthorized, "missing authorization header") + } + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { + return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header") + } + + token := strings.TrimSpace(parts[1]) + if token == "" { + return fiber.NewError(fiber.StatusUnauthorized, "invalid authorization header") + } + + verification, err := sso.VerifyAccessToken(token) + if err != nil { + return fiber.NewError(fiber.StatusUnauthorized, "invalid access token") + } + + if verification.ServiceAlias == "" || verification.ServiceAlias != alias { + return fiber.NewError(fiber.StatusUnauthorized, "service subject mismatch") + } + if !hasAnyScope(verification.Claims.Scopes(), []string{"sync.master", "sync.users"}) { + return fiber.NewError(fiber.StatusForbidden, "missing sync scope") + } + return nil +} + +func (h *MasterDataController) registerNonce(ctx context.Context, alias, nonce string) error { + ttl := h.nonceTTL + if ttl <= 0 { + ttl = 10 * time.Minute + } + + key := fmt.Sprintf("sso:sync:%s:%s", alias, nonce) + if h.redis != nil { + stored, err := h.redis.SetNX(ctx, key, "1", ttl).Result() + if err == nil { + if !stored { + return fiber.NewError(fiber.StatusUnauthorized, "nonce already used") + } + return nil + } + utils.Log.WithError(err).Warn("store sync nonce failed") + } + + now := time.Now().UTC() + if expRaw, ok := h.localNonce.Load(key); ok { + if expTime, ok := expRaw.(time.Time); ok && expTime.After(now) { + return fiber.NewError(fiber.StatusUnauthorized, "nonce already used") + } + } + h.localNonce.Store(key, now.Add(ttl)) + return nil +} + +func calculateSignature(secret, alias, timestamp, nonce string, body []byte) []byte { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(alias)) + mac.Write([]byte("\n")) + mac.Write([]byte(timestamp)) + mac.Write([]byte("\n")) + mac.Write([]byte(nonce)) + mac.Write([]byte("\n")) + if len(body) > 0 { + mac.Write(body) + } + return mac.Sum(nil) +} + +func decodeMasterSignature(sig string) ([]byte, error) { + sig = strings.TrimSpace(sig) + if sig == "" { + return nil, errors.New("empty signature") + } + if decoded, err := hex.DecodeString(sig); err == nil { + return decoded, nil + } + if decoded, err := base64.StdEncoding.DecodeString(sig); err == nil { + return decoded, nil + } + if decoded, err := base64.URLEncoding.DecodeString(sig); err == nil { + return decoded, nil + } + return nil, errors.New("unrecognized signature encoding") +} + +func parseUintList(raw string) []uint { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + parts := strings.Split(raw, ",") + out := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + val, err := strconv.ParseUint(part, 10, 64) + if err != nil || val == 0 { + continue + } + if _, ok := seen[uint(val)]; ok { + continue + } + seen[uint(val)] = struct{}{} + out = append(out, uint(val)) + } + return out +} + +func hasAnyScope(scopes []string, targets []string) bool { + if len(scopes) == 0 || len(targets) == 0 { + return false + } + for _, scope := range scopes { + scope = strings.ToLower(strings.TrimSpace(scope)) + if scope == "" { + continue + } + for _, target := range targets { + if scope == strings.ToLower(strings.TrimSpace(target)) { + return true + } + } + } + return false +} diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index 410e9577..b49d73e5 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -16,7 +16,7 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" - "gitlab.com/mbugroup/lti-api.git/internal/sso" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils/secure" ) diff --git a/internal/modules/sso/controllers/user_sync.controller.go b/internal/modules/sso/controllers/user_sync.controller.go index 9aeb9555..72c7768a 100644 --- a/internal/modules/sso/controllers/user_sync.controller.go +++ b/internal/modules/sso/controllers/user_sync.controller.go @@ -9,23 +9,24 @@ import ( "encoding/json" "errors" "fmt" - "github.com/go-playground/validator/v10" - "github.com/gofiber/fiber/v2" - "github.com/redis/go-redis/v9" - "github.com/sirupsen/logrus" - "gorm.io/gorm" "strconv" "strings" "sync" "time" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/redis/go-redis/v9" + "github.com/sirupsen/logrus" + "gorm.io/gorm" + "gitlab.com/mbugroup/lti-api.git/internal/config" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session" + sso "gitlab.com/mbugroup/lti-api.git/internal/modules/sso/verifier" "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/response" - "gitlab.com/mbugroup/lti-api.git/internal/sso" "gitlab.com/mbugroup/lti-api.git/internal/utils" ) diff --git a/internal/modules/sso/route.go b/internal/modules/sso/route.go index 3f2a699e..a864611e 100644 --- a/internal/modules/sso/route.go +++ b/internal/modules/sso/route.go @@ -26,6 +26,7 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { ctrl := ssoController.NewController(&http.Client{Timeout: 10 * time.Second}, store, session.GetRevocationStore()) userRepo := userRepository.NewUserRepository(db) syncCtrl := ssoController.NewUserSyncController(validate, userRepo, cache.Redis(), config.SSOClients) + masterCtrl := ssoController.NewMasterDataController(db, cache.Redis(), config.SSOClients) group := router.Group("/sso") group.Get("/start", middleware.NewLimiter(30, time.Minute), ctrl.Start) @@ -34,4 +35,6 @@ func Routes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { group.Post("/refresh", middleware.NewLimiter(60, time.Minute), ctrl.Refresh) group.Post("/logout", middleware.NewLimiter(60, time.Minute), ctrl.Logout) group.Post("/users/sync", middleware.NewLimiter(30, time.Minute), syncCtrl.Sync) + group.Get("/master/areas", middleware.NewLimiter(60, time.Minute), masterCtrl.GetAreas) + group.Get("/master/locations", middleware.NewLimiter(60, time.Minute), masterCtrl.GetLocations) } diff --git a/internal/sso/profile.go b/internal/modules/sso/verifier/profile.go similarity index 85% rename from internal/sso/profile.go rename to internal/modules/sso/verifier/profile.go index a211fc74..e3cd40ca 100644 --- a/internal/sso/profile.go +++ b/internal/modules/sso/verifier/profile.go @@ -39,6 +39,10 @@ type UserProfile struct { UserID uint Roles []Role Permissions []Permission + AreaIDs []uint + LocationIDs []uint + AllArea bool + AllLocation bool } // Role describes a role assignment from the SSO profile response. @@ -145,6 +149,10 @@ func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error } roles := envelope.getRoles() + areaIDs := envelope.getAreaIDs() + locationIDs := envelope.getLocationIDs() + allArea := envelope.getAllArea() + allLocation := envelope.getAllLocation() profile := &UserProfile{} // Attempt to infer user id if provided. @@ -183,6 +191,10 @@ func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error } profile.Roles = convertedRoles profile.Permissions = perms + profile.AreaIDs = areaIDs + profile.LocationIDs = locationIDs + profile.AllArea = allArea + profile.AllLocation = allLocation return profile, nil } @@ -260,9 +272,17 @@ func canonicalPermissionName(name string) string { // userInfoEnvelope handles the varying shapes returned by the SSO userinfo endpoint. type userInfoEnvelope struct { Roles []userInfoRole `json:"roles"` + AreaIDs []uint `json:"area_ids"` + LocationIDs []uint `json:"location_ids"` + AllArea bool `json:"all_area"` + AllLocation bool `json:"all_location"` Data *struct { ID int64 `json:"id"` Roles []userInfoRole `json:"roles"` + AreaIDs []uint `json:"area_ids"` + LocationIDs []uint `json:"location_ids"` + AllArea bool `json:"all_area"` + AllLocation bool `json:"all_location"` } `json:"data"` User *struct { ID int64 `json:"id"` @@ -284,6 +304,46 @@ func (e *userInfoEnvelope) getRoles() []userInfoRole { return nil } +func (e *userInfoEnvelope) getAreaIDs() []uint { + if len(e.AreaIDs) > 0 { + return e.AreaIDs + } + if e.Data != nil && len(e.Data.AreaIDs) > 0 { + return e.Data.AreaIDs + } + return nil +} + +func (e *userInfoEnvelope) getLocationIDs() []uint { + if len(e.LocationIDs) > 0 { + return e.LocationIDs + } + if e.Data != nil && len(e.Data.LocationIDs) > 0 { + return e.Data.LocationIDs + } + return nil +} + +func (e *userInfoEnvelope) getAllArea() bool { + if e.AllArea { + return true + } + if e.Data != nil && e.Data.AllArea { + return true + } + return false +} + +func (e *userInfoEnvelope) getAllLocation() bool { + if e.AllLocation { + return true + } + if e.Data != nil && e.Data.AllLocation { + return true + } + return false +} + type userInfoRole struct { ID int64 `json:"id"` Key string `json:"key"` diff --git a/internal/sso/verifier.go b/internal/modules/sso/verifier/verifier.go similarity index 100% rename from internal/sso/verifier.go rename to internal/modules/sso/verifier/verifier.go diff --git a/internal/utils/constant.go b/internal/utils/constant.go index cb8586ac..9abd6a30 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -118,6 +118,16 @@ const ( StockLogTypeRecording StockLogType = "RECORDING" ) +// ------------------------------------------------------------------- +// Transfer context +// ------------------------------------------------------------------- + +const ( + TransferContextKey = "transfer_context" + TransferContextInventoryTransfer = "inventory_transfer" + TransferContextTransferToLaying = "transfer_to_laying" +) + // ------------------------------------------------------------------- // WarehouseType // -------------------------------------------------------------------