From 705939bbf525a2e481ecb1ef92726dd00cda97ad Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 28 Jan 2026 13:39:06 +0700 Subject: [PATCH 1/4] [FIX/BE-US] delete *1000 egg mass --- .../production/recordings/services/recording.service.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 61f96e81..c3e21770 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -1692,7 +1692,8 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm var eggMass float64 if remainingChick > 0 && totalEggWeightGrams > 0 { - eggMass = (totalEggWeightGrams / remainingChick) / 1000 + // totalEggWeightGrams is in grams; egg mass is grams per hen. + eggMass = totalEggWeightGrams / remainingChick updates["egg_mass"] = eggMass recording.EggMass = &eggMass } else { From 392b2f51ad3ef08dfce645f1d1684b27d0491ecf Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 28 Jan 2026 10:32:18 +0000 Subject: [PATCH 2/4] Edit production.yml --- ci/production.yml | 74 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/ci/production.yml b/ci/production.yml index 930d15f4..48bf64fb 100644 --- a/ci/production.yml +++ b/ci/production.yml @@ -49,40 +49,80 @@ build_production: # ========================= -# MIGRATE (PRODUCTION - MANUAL) +# MIGRATE (PRODUCTION) # ========================= migrate_production: stage: migrate rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' - allow_failure: false + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' needs: - job: build_production artifacts: false script: | set -e - cd /opt/deploy/lti - test -f .env || (echo "❌ .env not found" && exit 1) + echo "✅ Running migrations (production) ..." + cd "$DEPLOY_DIR" + test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1) + test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1) + + # ✅ load env dari server set -a . ./.env set +a - # Validasi env wajib - : "${DB_HOST:?DB_HOST not set}" - : "${DB_PORT:?DB_PORT not set}" - : "${DB_USER:?DB_USER not set}" - : "${DB_PASSWORD:?DB_PASSWORD not set}" - : "${DB_NAME:?DB_NAME not set}" + # ✅ validasi + test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1) + test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1) + test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1) + test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1) + test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1) - DB_SSLMODE="${DB_SSLMODE:-require}" - export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" + export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}" + echo "✅ DATABASE_URL=$DATABASE_URL" - echo "✅ Running migrations (production)..." - docker run --rm \ - -v "/opt/deploy/lti/internal/database/migrations:/migrations:ro" \ + # ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!) + echo "✅ Ensuring postgres & redis running ..." + docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true + + # ✅ Ambil network key dari compose + COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')" + echo "✅ Compose network key: $COMPOSE_NETWORK_KEY" + + # ✅ Cari network name yang dipakai docker + NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)" + test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1) + + echo "✅ Docker network detected: $NETWORK_NAME" + + # ✅ Migrations dari repo (CI workspace) + echo "✅ Checking migrations from repo..." + ls -lah "$CI_PROJECT_DIR/internal/database/migrations" + + echo "✅ Running migrations via migrate/migrate container" + set +e + out=$(docker run --rm \ + --network "$NETWORK_NAME" \ + -v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \ migrate/migrate:v4.15.2 \ - -path=/migrations -database "$DATABASE_URL" up + -path=/migrations -database "$DATABASE_URL" up 2>&1) + code=$? + set -e + + echo "$out" + + # ✅ Handle no change dengan benar (tidak false-success) + if echo "$out" | grep -qi "no change"; then + echo "✅ No change (already up to date)" + exit 0 + fi + + if [ $code -ne 0 ]; then + echo "❌ Migration failed with exit code $code" + exit $code + fi + + echo "✅ Migration applied successfully" # ========================= From 478487870b67729dd746ee4d15e62f639a1cbcf2 Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Wed, 28 Jan 2026 17:34:11 +0700 Subject: [PATCH 3/4] Revert "Edit production.yml" This reverts commit 392b2f51ad3ef08dfce645f1d1684b27d0491ecf --- ci/production.yml | 74 +++++++++++------------------------------------ 1 file changed, 17 insertions(+), 57 deletions(-) diff --git a/ci/production.yml b/ci/production.yml index 48bf64fb..930d15f4 100644 --- a/ci/production.yml +++ b/ci/production.yml @@ -49,80 +49,40 @@ build_production: # ========================= -# MIGRATE (PRODUCTION) +# MIGRATE (PRODUCTION - MANUAL) # ========================= migrate_production: stage: migrate rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' + allow_failure: false needs: - job: build_production artifacts: false script: | set -e - echo "✅ Running migrations (production) ..." + cd /opt/deploy/lti + test -f .env || (echo "❌ .env not found" && exit 1) - cd "$DEPLOY_DIR" - test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1) - test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1) - - # ✅ load env dari server set -a . ./.env set +a - # ✅ validasi - test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1) - test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1) - test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1) - test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1) - test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1) + # Validasi env wajib + : "${DB_HOST:?DB_HOST not set}" + : "${DB_PORT:?DB_PORT not set}" + : "${DB_USER:?DB_USER not set}" + : "${DB_PASSWORD:?DB_PASSWORD not set}" + : "${DB_NAME:?DB_NAME not set}" - export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}" - echo "✅ DATABASE_URL=$DATABASE_URL" + DB_SSLMODE="${DB_SSLMODE:-require}" + export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" - # ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!) - echo "✅ Ensuring postgres & redis running ..." - docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true - - # ✅ Ambil network key dari compose - COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')" - echo "✅ Compose network key: $COMPOSE_NETWORK_KEY" - - # ✅ Cari network name yang dipakai docker - NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)" - test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1) - - echo "✅ Docker network detected: $NETWORK_NAME" - - # ✅ Migrations dari repo (CI workspace) - echo "✅ Checking migrations from repo..." - ls -lah "$CI_PROJECT_DIR/internal/database/migrations" - - echo "✅ Running migrations via migrate/migrate container" - set +e - out=$(docker run --rm \ - --network "$NETWORK_NAME" \ - -v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \ + echo "✅ Running migrations (production)..." + docker run --rm \ + -v "/opt/deploy/lti/internal/database/migrations:/migrations:ro" \ migrate/migrate:v4.15.2 \ - -path=/migrations -database "$DATABASE_URL" up 2>&1) - code=$? - set -e - - echo "$out" - - # ✅ Handle no change dengan benar (tidak false-success) - if echo "$out" | grep -qi "no change"; then - echo "✅ No change (already up to date)" - exit 0 - fi - - if [ $code -ne 0 ]; then - echo "❌ Migration failed with exit code $code" - exit $code - fi - - echo "✅ Migration applied successfully" + -path=/migrations -database "$DATABASE_URL" up # ========================= From a21b554fc7b4de8b55caac5982d3750ae7d12e69 Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 29 Jan 2026 14:33:26 +0700 Subject: [PATCH 4/4] [FIX/BE-US] changes permission to redis and scope --- internal/config/config.go | 5 +++ .../modules/sso/controllers/sso.controller.go | 21 +++++++-- .../sso/controllers/user_sync.controller.go | 6 +++ internal/modules/sso/verifier/profile.go | 44 ++++++++++++++----- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 71fb430c..af723b3b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,6 +61,7 @@ var ( SSOCookieDomain string SSOCookieSecure bool SSOCookieSameSite string + SSOAccessTokenMaxBytes int SSOTokenBlacklistPrefix string SSOPKCETTL time.Duration SSOUserSyncDrift time.Duration @@ -144,6 +145,10 @@ func init() { SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN") SSOCookieSecure = viper.GetBool("SSO_COOKIE_SECURE") SSOCookieSameSite = defaultString(viper.GetString("SSO_COOKIE_SAMESITE"), "Lax") + SSOAccessTokenMaxBytes = viper.GetInt("SSO_ACCESS_TOKEN_MAX_BYTES") + if SSOAccessTokenMaxBytes <= 0 { + SSOAccessTokenMaxBytes = 4096 + } SSOTokenBlacklistPrefix = defaultString(viper.GetString("SSO_TOKEN_BLACKLIST_PREFIX"), "sso:blacklist") if ttl := viper.GetInt("SSO_PKCE_TTL_SECONDS"); ttl > 0 { SSOPKCETTL = time.Duration(ttl) * time.Second diff --git a/internal/modules/sso/controllers/sso.controller.go b/internal/modules/sso/controllers/sso.controller.go index b49d73e5..5e75d4a9 100644 --- a/internal/modules/sso/controllers/sso.controller.go +++ b/internal/modules/sso/controllers/sso.controller.go @@ -200,7 +200,7 @@ func (h *Controller) Refresh(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusUnauthorized, "invalid access token") } - issueCookies(c, struct { + if err := issueCookies(c, struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` @@ -218,7 +218,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error { IDToken: tokenResp.IDToken, Error: tokenResp.Error, Description: tokenResp.Description, - }, verification) + }, verification); err != nil { + return err + } utils.Log.WithFields(logrus.Fields{ "user_id": verification.UserID, @@ -307,7 +309,9 @@ func (h *Controller) Callback(c *fiber.Ctx) error { } // prepare cookies - issueCookies(c, tokenResp, verification) + if err := issueCookies(c, tokenResp, verification); err != nil { + return err + } redirectTarget := sessionData.ReturnTo if redirectTarget == "" { @@ -742,13 +746,21 @@ func issueCookies(c *fiber.Ctx, tokenResp struct { IDToken string `json:"id_token"` Error string `json:"error"` Description string `json:"error_description"` -}, verification *sso.VerificationResult) { +}, verification *sso.VerificationResult) error { if revoker := session.GetRevocationStore(); revoker != nil && verification != nil { if err := revoker.ClearUserLogout(c.Context(), verification.UserID); err != nil { utils.Log.WithError(err).Warn("failed to clear logout marker") } } + if max := config.SSOAccessTokenMaxBytes; max > 0 && len(tokenResp.AccessToken) > max { + utils.Log.WithFields(logrus.Fields{ + "token_len": len(tokenResp.AccessToken), + "max_len": max, + }).Warn("sso access token exceeds cookie size limit") + return fiber.NewError(fiber.StatusRequestEntityTooLarge, "access token too large") + } + accessName := resolveSSOCookieName(config.SSOAccessCookieName, "access") refreshName := resolveSSOCookieName(config.SSORefreshCookieName, "refresh") maxAge := tokenResp.ExpiresIn @@ -790,6 +802,7 @@ func issueCookies(c *fiber.Ctx, tokenResp struct { // Optional: expose limited info via headers for FE debugging (avoid tokens) c.Set("X-Auth-User", fmt.Sprintf("%d", verification.UserID)) + return nil } func clearSSOCookie(c *fiber.Ctx, name string) { diff --git a/internal/modules/sso/controllers/user_sync.controller.go b/internal/modules/sso/controllers/user_sync.controller.go index 72c7768a..bdc7900e 100644 --- a/internal/modules/sso/controllers/user_sync.controller.go +++ b/internal/modules/sso/controllers/user_sync.controller.go @@ -291,6 +291,8 @@ func (h *UserSyncController) upsertUser(c *fiber.Ctx, alias string, req *userSyn "user_id": req.User.ID, }).Info("sso user synced") + sso.InvalidateProfileCache(c.Context(), uint(req.User.ID)) + msg := fmt.Sprintf("User %s successfully", req.Action) return c.Status(fiber.StatusOK).JSON(response.Success{ Code: fiber.StatusOK, @@ -318,6 +320,8 @@ func (h *UserSyncController) logoutUser(c *fiber.Ctx, alias string, req *userSyn "user_id": req.User.ID, }).Info("sso user logout enforced") + sso.InvalidateProfileCache(c.Context(), uint(req.User.ID)) + return c.Status(fiber.StatusOK).JSON(response.Common{ Code: fiber.StatusOK, Status: "success", @@ -341,6 +345,8 @@ func (h *UserSyncController) removeUser(c *fiber.Ctx, alias string, req *userSyn "user_id": req.User.ID, }).Info("sso user deleted") + sso.InvalidateProfileCache(c.Context(), uint(req.User.ID)) + return c.Status(fiber.StatusOK).JSON(response.Common{ Code: fiber.StatusOK, Status: "success", diff --git a/internal/modules/sso/verifier/profile.go b/internal/modules/sso/verifier/profile.go index e3cd40ca..4876db1e 100644 --- a/internal/modules/sso/verifier/profile.go +++ b/internal/modules/sso/verifier/profile.go @@ -265,24 +265,44 @@ func profileCacheKey(userID uint) string { return profileCachePrefix + strconv.FormatUint(uint64(userID), 10) } +// InvalidateProfileCache clears cached profile data for the given user in both local and Redis caches. +func InvalidateProfileCache(ctx context.Context, userID uint) { + if userID == 0 { + return + } + key := profileCacheKey(userID) + profileLocalCache.Delete(key) + + client := cache.Redis() + if client == nil { + return + } + if ctx == nil { + ctx = context.Background() + } + if err := client.Del(ctx, key).Err(); err != nil && !errors.Is(err, redis.Nil) { + utils.Log.WithError(err).Warn("sso profile redis delete failed") + } +} + func canonicalPermissionName(name string) string { return strings.ToLower(strings.TrimSpace(name)) } // 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"` + 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"`