Skip to content

Fix Civil ID S3 hardening and env-based S3 credentials#71

Open
mzgamers28-cmyk wants to merge 13 commits into
BAWES-Universe:masterfrom
mzgamers28-cmyk:bounty/civil-id-s3-hardening-55
Open

Fix Civil ID S3 hardening and env-based S3 credentials#71
mzgamers28-cmyk wants to merge 13 commits into
BAWES-Universe:masterfrom
mzgamers28-cmyk:bounty/civil-id-s3-hardening-55

Conversation

@mzgamers28-cmyk

@mzgamers28-cmyk mzgamers28-cmyk commented May 14, 2026

Copy link
Copy Markdown

Summary

/claim #55

Implements Phase 2 and the code-side parts of Phase 4 for the AWS S3 Security Remediation & Civil ID Upload Fix bounty.

  • makes Civil ID front/back removal best-effort for missing S3 objects while still clearing the DB field and marking the candidate for re-verification
  • fixes Civil ID S3 paths from the old candidate-civil-id/ prefix to the current photos/ prefix
  • changes Civil ID replacement to copy and verify the new file before deleting the old one
  • adds S3 HEAD-based object existence checks and safer delete return handling
  • validates civil_id and civil_expiry_date on update-civil-id-expiry-date, returning operation:error instead of raw failures
  • moves the targeted temp/permanent S3 credentials to env vars in shared, prod Railway, dev Railway, docker, krushn, krushn-nginx, and CI configs
  • documents the required S3 environment variables
  • adds functional regression coverage for missing/invalid Civil ID expiry update payloads

Validation

  • Searched the changed code paths to confirm candidate-civil-id/ is no longer used for Civil ID deletes.
  • Searched config files to confirm the targeted ODY2X/WCUM hardcoded S3 key references were removed.
  • Could not run php -l or the PHP test suite in this environment because PHP, Composer, Docker, and Git are not installed.

Deployment Notes

Before deploying, set these environment variables in Railway/CI/local secret stores:

  • AWS_TEMP_BUCKET_KEY
  • AWS_TEMP_BUCKET_SECRET
  • AWS_PERMANENT_S3_ACCESS_KEY_ID
  • AWS_PERMANENT_S3_SECRET_ACCESS_KEY
  • AWS_PERMANENT_S3_REGION
  • AWS_PERMANENT_S3_BUCKET

Summary by CodeRabbit

Release Notes

  • Documentation

    • Added S3 Environment Variables section to README specifying required credentials for setup.
  • New Features

    • Enhanced validation for civil ID submissions with stricter input requirements and localized error messages.
  • Bug Fixes

    • Improved S3 file existence detection logic.
    • Migrated S3 credentials from hardcoded values to environment variables across all environments for enhanced security.
  • Tests

    • Added functional tests validating civil ID expiry date and ID field requirements.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@mzgamers28-cmyk has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 25 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8feb6ec8-99f3-484d-aba1-aa20ac7d8032

📥 Commits

Reviewing files that changed from the base of the PR and between 73c4bb2 and 388ed85.

📒 Files selected for processing (7)
  • common/components/S3ResourceManager.php
  • environments/circle-ci/common/config/main-local.php
  • environments/dev-server-railway/common/config/main-local.php
  • environments/docker/common/config/main-local.php
  • environments/krushn-nginx/common/config/main-local.php
  • environments/krushn/common/config/main-local.php
  • environments/prod-railway/common/config/main-local.php
📝 Walkthrough

Walkthrough

This PR externalizes S3 credentials to environment variables across all deployment environments, hardens Civil ID file operations with validation and error handling, and improves S3 resource manager methods for file existence detection and deletion.

Changes

S3 Credentials and Civil ID Management

Layer / File(s) Summary
Documentation and S3 Infrastructure Configuration
README.md, common/config/main.php, environments/circle-ci/common/config/main-local.php, environments/dev-server-railway/common/config/main-local.php, environments/docker/common/config/main-local.php, environments/krushn-nginx/common/config/main-local.php, environments/krushn/common/config/main-local.php, environments/prod-railway/common/config/main-local.php
README documents required S3 environment variables (temporary and permanent bucket credentials). All environment configurations replace hardcoded AWS credentials with getenv() calls sourcing AWS_TEMP_BUCKET_* and AWS_PERMANENT_S3_* variables, with appropriate fallback defaults.
S3 Resource Manager Method Improvements
common/components/S3ResourceManager.php
delete() now returns a boolean computed from AWS response DeleteMarker. fileExists() is rewritten to distinguish URLs (starting with http) from S3 object keys, with proper handling for not-found errors versus other exceptions.
Civil ID File Storage Contract and Implementation
common/models/Candidate.php
Civil ID photo attributes remapped from civil-id to photos folder. deleteFile() pre-checks Civil ID file existence and logs context on S3 errors. updateCivilId() validates temp filename, normalizes destination paths under photos/, verifies copy success, and deletes replaced files.
Civil ID Photo API Endpoints
candidate/modules/v1/controllers/AccountController.php
Remove-photo actions throw 404 when candidate missing, clear photos while setting re-verification flag, and wrap persistence in try-catch with localized error responses. Upload-photo actions validate updateCivilId() return value. Expiry-date update trims inputs, validates required presence and date parseability, and wraps save with exception handling.
Civil ID Endpoint Validation Tests
candidate/tests/functional/AccountCest.php
Added functional tests for update-civil-id-expiry-date endpoint validating error responses when civil_id is missing or civil_expiry_date is invalid.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Secrets no longer hardcoded, now fly free,
From photos folder, Civil IDs safely stored in S3,
Environment whispers their keys, endpoints stand tall,
With try-catch and validation to guard against all!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main objectives of the PR: fixing Civil ID S3 handling and moving S3 credentials to environment variables.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot

dosubot Bot commented May 14, 2026

Copy link
Copy Markdown

Related Knowledge

2 documents with suggested updates are ready for review.

BAWES Universe

Civil ID Upload Flow
View Suggested Changes
@@ -81,14 +81,12 @@
 'temporaryBucketResourceManager' => [
     'class' => 'common\components\S3ResourceManager',
     'region' => 'eu-west-2', // Bucket based in London
-    'key'    => 'AKIAWMITDJRKVN5ODY2X',
-    'secret' => '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',
+    'key'    => getenv('AWS_TEMP_BUCKET_KEY') ?: '',
+    'secret' => getenv('AWS_TEMP_BUCKET_SECRET') ?: '',
     'bucket' => 'studenthub-public-anyone-can-upload-24hr-expiry'
     // Access URL: https://studenthub-public-anyone-can-upload-24hr-expiry.s3.amazonaws.com/
 ],
 ```
-
-> ⚠️ The key/secret for the temp bucket are committed to `common/config/main.php`. These credentials are deliberately limited to upload-only access on a bucket with a 24-hour object expiry policy and no sensitive data. That said, consider migrating these to environment variables (`AWS_TEMP_BUCKET_KEY` / `AWS_TEMP_BUCKET_SECRET`) for consistency with production practice.
 
 
 
@@ -118,6 +116,19 @@
 ],
 ```
 
+**Production Railway** (`environments/prod-railway/common/config/main-local.php`) uses environment variables for credentials:
+
+```php
+'resourceManager' => [
+    'class'      => 'common\components\S3ResourceManager',
+    'authMethod' => \common\components\S3ResourceManager::AUTH_VIA_KEY_AND_SECRET,
+    'region'     => getenv('AWS_PERMANENT_S3_REGION') ?: 'eu-west-2',
+    'bucket'     => getenv('AWS_PERMANENT_S3_BUCKET') ?: 'studenthub-uploads',
+    'key'        => getenv('AWS_PERMANENT_S3_ACCESS_KEY_ID') ?: '',
+    'secret'     => getenv('AWS_PERMANENT_S3_SECRET_ACCESS_KEY') ?: '',
+],
+```
+
 **Dev / Staging** (`environments/dev/common/config/main-local.php`) [[10]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/environments/dev/common/config/main-local.php#L82-L93):
 
 ```php
@@ -151,8 +162,12 @@
 
 | Variable | Mapped To | Used For |
 |---|---|---|
-| `AWS_TEMP_BUCKET_KEY` | `params['aws_temp_access_key_id']` | Temp bucket key served to frontend via `/aws/config` |
-| `AWS_TEMP_BUCKET_SECRET` | `params['aws_temp_secret_access_key']` | Temp bucket secret served to frontend via `/aws/config` |
+| `AWS_TEMP_BUCKET_KEY` | `params['aws_temp_access_key_id']` and `temporaryBucketResourceManager.key` | Temp bucket key served to frontend via `/aws/config` |
+| `AWS_TEMP_BUCKET_SECRET` | `params['aws_temp_secret_access_key']` and `temporaryBucketResourceManager.secret` | Temp bucket secret served to frontend via `/aws/config` |
+| `AWS_PERMANENT_S3_ACCESS_KEY_ID` | `resourceManager.key` (Railway/docker environments) | Permanent bucket access key for Railway/docker |
+| `AWS_PERMANENT_S3_SECRET_ACCESS_KEY` | `resourceManager.secret` (Railway/docker environments) | Permanent bucket secret for Railway/docker |
+| `AWS_PERMANENT_S3_REGION` | `resourceManager.region` (Railway/docker environments) | Permanent bucket region for Railway/docker |
+| `AWS_PERMANENT_S3_BUCKET` | `resourceManager.bucket` (Railway/docker environments) | Permanent bucket name for Railway/docker |
 | `AWS_TEXTRACT_ACCESS_KEY_ID` | `idExpiryDateExtractor.key` | AWS Textract for future ID OCR extraction |
 | `AWS_TEXTRACT_SECRET_ACCESS_KEY` | `idExpiryDateExtractor.secret` | AWS Textract for future ID OCR extraction |
 
@@ -411,26 +426,48 @@
         ? 'candidate_civil_photo_front'
         : 'candidate_civil_photo_back';
 
-    // Delete previous file from permanent bucket if one exists
-    if (!empty($this->oldAttributes[$idSide])) {
-        $this->deleteFile('civil-id', $side);
-    }
-
-    $fileName     = $this->$idSide;
+    $fileName = $this->$idSide;
+
+    if (!$fileName) {
+        $this->addError($idSide, Yii::t('app', 'file not available to save.'));
+
+        return false;
+    }
+
     $sourceBucket = Yii::$app->temporaryBucketResourceManager->bucket;
-    $targetPath   = "photos/" . $fileName;
-
+
+    $targetPath = strpos($fileName, 'photos/') === 0 ? $fileName : "photos/" . $fileName;
+
+    // Copy using S3ResourceManager Component
+    
     try {
-        return Yii::$app->resourceManager->copy($fileName, $targetPath, $sourceBucket);
+
+        $result = Yii::$app->resourceManager->copy($fileName, $targetPath, $sourceBucket);
+
+        if (!Yii::$app->resourceManager->fileExists($targetPath)) {
+            throw new \RuntimeException('Copied Civil ID file was not found at destination.');
+        }
+
+        if (!empty($this->oldAttributes[$idSide]) && $this->oldAttributes[$idSide] !== $fileName) {
+            $this->deleteFile('civil-id', $side);
+        }
+
+        return $result;
 
     } catch (\Aws\S3\Exception\S3Exception $e) {
+
         Yii::error($e->getMessage(), 'candidate');
+
         $this->addError($idSide, Yii::t('app', 'file not available to save.'));
+
         return false;
 
     } catch (\Exception $e) {
+
         Yii::error($e->getMessage(), 'candidate');
+
         $this->addError($idSide, Yii::t('app', 'file not available to save.'));
+
         return false;
     }
 }
@@ -438,8 +475,9 @@
 
 Key behaviours:
 
-- **Old file cleanup:** If the candidate already has a civil photo stored (`oldAttributes[$idSide]` is set), the old file is deleted from the permanent bucket before the new one is copied. This prevents orphaned files accumulating.
-- **Target path:** Files are stored as `photos/{filename}` in the permanent bucket.
+- **Copy then verify:** The new file is first copied to the permanent bucket, then its existence is verified using `fileExists()`. Only after confirmation does the old file get deleted. This ordering prevents data loss if the copy fails.
+- **Best-effort old file cleanup:** If the candidate already has a civil photo stored (`oldAttributes[$idSide]` is set) and it differs from the new filename, the old file is deleted from the permanent bucket. The deletion is wrapped in try-catch logic in `deleteFile()`, and for `'civil-id'` type deletions, failures are logged but do not block the operation.
+- **Target path:** Files are stored as `photos/{filename}` in the permanent bucket. The code handles both legacy paths and the current convention.
 - **Error handling:** Both `S3Exception` and generic `\Exception` are caught. Errors are logged to the `candidate` category (routed to Slack/Sentry) and surfaced as a model validation error.
 
 ***
@@ -476,6 +514,60 @@
     $sourceBucket   // "studenthub-public-anyone-can-upload-24hr-expiry"
 );
 ```
+
+***
+
+### S3ResourceManager — fileExists()
+
+**File:** `common/components/S3ResourceManager.php`
+
+The `fileExists()` method checks whether a file exists in S3. For S3 object keys (non-URL strings), it uses the SDK's `headObject()` call [[60]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3ResourceManager.php#L174-L188):
+
+```php
+public function fileExists($filenameOrUrl)
+{
+    if (!$filenameOrUrl) {
+        return false;
+    }
+
+    $isUrl = false;
+    if (strpos($filenameOrUrl, 'http') === 0) {
+        $isUrl = true;
+    }
+
+    if (!$isUrl) {
+        try {
+            $this->getClient()->headObject([
+                'Bucket' => $this->bucket,
+                'Key' => $filenameOrUrl,
+            ]);
+
+            return true;
+        } catch (AwsException $e) {
+            if ((int) $e->getStatusCode() === 404 || $e->getAwsErrorCode() === 'NoSuchKey' || $e->getAwsErrorCode() === 'NotFound') {
+                return false;
+            }
+
+            Yii::warning('Unable to check S3 object existence for bucket=' . $this->bucket . ' key=' . $filenameOrUrl . ': ' . $e->getMessage(), 's3');
+
+            return false;
+        } catch (\Exception $e) {
+            Yii::warning('Unable to check S3 object existence for bucket=' . $this->bucket . ' key=' . $filenameOrUrl . ': ' . $e->getMessage(), 's3');
+
+            return false;
+        }
+    }
+
+    // If URL, fall back to HTTP HEAD request
+    $http = new \GuzzleHttp\Client(['base_uri' => $isUrl ? $filenameOrUrl : $this->getUrl($filenameOrUrl)]);
+    try {
+        $response = $http->request('HEAD');
+        // ...
+    }
+}
+```
+
+For URL strings, the method falls back to an HTTP HEAD request. The SDK-based approach works for both public and private objects as long as the configured credentials have appropriate permissions.
 
 ***
 
@@ -599,6 +691,10 @@
 |---|---|---|
 | `AWS_TEMP_BUCKET_KEY` | All environments | IAM access key ID for the temporary upload bucket |
 | `AWS_TEMP_BUCKET_SECRET` | All environments | IAM secret access key for the temporary upload bucket |
+| `AWS_PERMANENT_S3_ACCESS_KEY_ID` | Railway, docker, CircleCI environments | IAM access key ID for the permanent bucket (Railway/docker only; production/dev use IAM roles) |
+| `AWS_PERMANENT_S3_SECRET_ACCESS_KEY` | Railway, docker, CircleCI environments | IAM secret access key for the permanent bucket (Railway/docker only; production/dev use IAM roles) |
+| `AWS_PERMANENT_S3_REGION` | Railway, docker environments | AWS region for the permanent bucket |
+| `AWS_PERMANENT_S3_BUCKET` | Railway, docker environments | Bucket name for the permanent bucket |
 | `AWS_TEXTRACT_ACCESS_KEY_ID` | All environments | Access key for AWS Textract (ID OCR extraction) |
 | `AWS_TEXTRACT_SECRET_ACCESS_KEY` | All environments | Secret key for AWS Textract (ID OCR extraction) |
 
@@ -660,10 +756,12 @@
 - The credentials returned by `/aws/config` should be scoped to `s3:PutObject` on this bucket only (verify this in your IAM policy).
 - The CORS filter on `/aws/config` restricts the response to `allowedOrigins` — browser-based attackers from other origins cannot retrieve the credentials. [[53]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/admin/modules/v1/controllers/AwsController.php#L17-L36)
 
-#### Permanent Bucket: IAM Role, No Keys in Config
+#### Permanent Bucket: IAM Role or Environment Variables Depending on Environment
 
 Production and dev servers access the permanent bucket via an attached IAM role. No access key or secret is stored in the server's configuration files for `resourceManager`. This is the correct posture. [[5]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/environments/prod/common/config/main-local.php#L152-L165) [[10]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/environments/dev/common/config/main-local.php#L82-L93)
 
+Railway and docker environments use `AUTH_VIA_KEY_AND_SECRET` and read credentials from environment variables (`AWS_PERMANENT_S3_ACCESS_KEY_ID`, `AWS_PERMANENT_S3_SECRET_ACCESS_KEY`, `AWS_PERMANENT_S3_REGION`, `AWS_PERMANENT_S3_BUCKET`) rather than hardcoded values in config files.
+
 #### S3FileExistValidator Guards the Copy Operation
 
 Before the backend copies a file from the temp bucket, it validates that the key actually exists in S3 (`HEAD` request via GuzzleHttp). This prevents an attacker from specifying an arbitrary S3 key and tricking the backend into copying a file they did not upload. [[54]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3FileExistValidator.php#L46-L58)
@@ -688,12 +786,6 @@
 
 **Recommendation:** Replace the static key/secret with **AWS STS `AssumeRole` temporary credentials** that expire in 15–60 minutes. The `/aws/config` endpoint would call `sts:AssumeRole` each request and return short-lived `AccessKeyId`, `SecretAccessKey`, and `SessionToken`.
 
-#### 🟡 Medium Priority: Temp Bucket Credentials Hardcoded in Source
-
-The `temporaryBucketResourceManager` component in `common/config/main.php` contains a hardcoded key and secret [[58]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/config/main.php#L11-L12). While the params array reads from environment variables for the frontend-facing values, the component definition itself does not.
-
-**Recommendation:** Change `common/config/main.php` to read the key and secret from environment variables, matching the pattern already used in `params.php`.
-
 #### 🟡 Medium Priority: No Rate Limiting on Upload Endpoints
 
 `POST /v1/account/update-civil-photo-front` and `POST /v1/account/update-civil-photo-back` have no rate limiting. An authenticated candidate could issue many upload requests in quick succession.
@@ -706,6 +798,6 @@
 
 **Recommendation:** Consider integrating an AWS Lambda function triggered on temp-bucket `PutObject` events to run ClamAV or a commercial AV solution before the backend is permitted to copy the file.
 
-#### 🟢 Low Priority: `fileExists()` Uses HTTP HEAD (Public Access Required)
-
-The `S3FileExistValidator` checks file existence using a GuzzleHttp `HEAD` request to the public URL of the file [[60]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3ResourceManager.php#L174-L188). This means the file must be publicly accessible in the temp bucket for validation to work. If the ACL recommendation above is ever applied to the temp bucket, this validation approach would need to be replaced with an SDK `headObject` call using credentials.
+#### 🟢 Low Priority: fileExists() Now Uses SDK-Based headObject for Non-URL Keys
+
+The `S3ResourceManager::fileExists()` method now uses the AWS SDK's `headObject()` call for S3 object keys (non-URL strings) rather than relying purely on HTTP HEAD requests [[60]](https://github.com/BAWES-Universe/studenthub/blob/7b023ff371b8a33dc49a350984a5adf263d120f2/common/components/S3ResourceManager.php#L174-L188). This approach works for both public and private objects as long as the configured credentials have appropriate permissions. For URL strings, the method still falls back to an HTTP HEAD request.

[Accept] [Edit] [Decline]

DB, API endpoint, S3, Slack, and payment keys
View Suggested Changes
@@ -14,11 +14,18 @@
 # Not found in main.php. Set this to your backend API base URL.
 API_BASE_URL=http://localhost:8000/api
 
-# --- S3 Configuration (temporaryBucketResourceManager) ---
-S3_REGION=eu-west-2
-S3_KEY=your_s3_access_key
-S3_SECRET=your_s3_secret_key
-S3_BUCKET=plugn-public-anyone-can-upload-24hr-expiry
+# --- S3 Configuration ---
+
+# Temporary Bucket (24hr expiry for uploads)
+AWS_TEMP_BUCKET_KEY=your_temp_bucket_access_key
+AWS_TEMP_BUCKET_SECRET=your_temp_bucket_secret_key
+
+# Permanent Bucket (Railway/Docker environments)
+# Note: CircleCI uses IAM roles instead of these env vars
+AWS_PERMANENT_S3_ACCESS_KEY_ID=your_permanent_s3_access_key_id
+AWS_PERMANENT_S3_SECRET_ACCESS_KEY=your_permanent_s3_secret_access_key
+AWS_PERMANENT_S3_REGION=eu-west-2
+AWS_PERMANENT_S3_BUCKET=studenthub-uploads-dev-server
 
 # --- Slack Configuration ---
 SLACK_URL=https://hooks.slack.com/services/your/slack/webhook
@@ -68,6 +75,7 @@
 
 **Notes:**
 - Replace all `your_*` values with your actual credentials or secrets.
-- The S3, Slack, and payment keys are directly mapped from the structure in [`common/config/main.php`](https://github.com/BAWES-Universe/plugn/blob/bc485b0a1da61d516955c5dc4fc29e95afccea92/common/config/main.php#L3-L172).
+- The S3 configuration includes both temporary bucket credentials (for 24-hour expiry uploads) and permanent bucket credentials (used in Railway/Docker environments). CircleCI uses IAM roles for S3 access rather than environment variables.
+- Slack and payment keys are directly mapped from the structure in [`common/config/main.php`](https://github.com/BAWES-Universe/plugn/blob/bc485b0a1da61d516955c5dc4fc29e95afccea92/common/config/main.php#L3-L172).
 - Database and API endpoint keys are placeholders, as they are not defined in `main.php` and may be set elsewhere in your project.
 - All other configuration keys in `main.php` are optional and can be set if your local environment requires them.

[Accept] [Edit] [Decline]

How did I do? Any feedback?  Join Discord

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
environments/prod-railway/common/config/main-local.php (1)

154-166: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update comment to reflect Railway's intentional use of key/secret authentication.

Line 154 sets authMethod to AUTH_VIA_KEY_AND_SECRET, but the comment at lines 160-161 states "For Dev and Production servers, access is via server embedded IAM roles so no key/secret required." This comment is incorrect for the Railway environment—Railway requires key/secret authentication and does not support IAM roles in the same way as standard AWS EC2 deployments.

Update the comment to clarify that this Railway production environment deliberately uses key/secret authentication, or remove the misleading comment about IAM roles if it's inapplicable here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@environments/prod-railway/common/config/main-local.php` around lines 154 -
166, The comment incorrectly implies production uses embedded IAM roles; update
the block around authMethod (set to
\common\components\S3ResourceManager::AUTH_VIA_KEY_AND_SECRET) to state that the
Railway environment intentionally uses key/secret authentication and that IAM
role-based access is not available here; adjust or remove the lines mentioning
"For Dev and Production servers, access is via server embedded IAM roles so no
key/secret required" and optionally note the relevant env vars
(AWS_PERMANENT_S3_ACCESS_KEY_ID, AWS_PERMANENT_S3_SECRET_ACCESS_KEY) are
required in this deployment.
environments/dev-server-railway/common/config/main-local.php (1)

106-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Contradiction confirmed: auth method setup conflicts with IAM role comment across all Railway configs.

Both dev-server-railway and prod-railway have identical configuration: authMethod is explicitly set to AUTH_VIA_KEY_AND_SECRET, yet the comment states "access is via server embedded IAM roles so no key/secret required."

The S3ResourceManager validation accepts empty strings (checks === null), so if the AWS environment variables aren't set at runtime, empty credentials will be passed to S3Client, causing failures.

To resolve:

  • If AWS credentials are always provided via environment variables in Railway: Update the comment to clarify that key/secret auth is used, not IAM roles (e.g., "Credentials sourced from AWS_PERMANENT_S3_* environment variables").
  • If IAM roles should be used instead: Change authMethod to \common\components\S3ResourceManager::AUTH_VIA_IAM_ROLE and remove the key and secret entries. This allows the SDK's default credential chain to handle IAM role assumption.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@environments/dev-server-railway/common/config/main-local.php` around lines
106 - 117, The config currently sets 'authMethod' =>
\common\components\S3ResourceManager::AUTH_VIA_KEY_AND_SECRET while the comment
claims IAM role usage and the file still defines 'key' and 'secret' from
AWS_PERMANENT_S3_* env vars; because S3ResourceManager treats empty strings as
valid values this can pass empty credentials into S3Client. Either (A) if
Railway provides keys via AWS_PERMANENT_S3_ACCESS_KEY_ID /
AWS_PERMANENT_S3_SECRET_ACCESS_KEY, update the comment to state that credentials
are sourced from those env vars and keep authMethod as AUTH_VIA_KEY_AND_SECRET,
or (B) if IAM role chaining should be used, change authMethod to
\common\components\S3ResourceManager::AUTH_VIA_IAM_ROLE and remove the 'key' and
'secret' entries so the SDK default credential provider (IAM role) is used;
ensure the chosen option is applied consistently across dev-server-railway and
prod-railway.
🧹 Nitpick comments (2)
candidate/tests/functional/AccountCest.php (1)

468-493: ⚡ Quick win

Add a regression test for missing civil_expiry_date

Nice additions. One branch added in the controller is still untested: empty/missing civil_expiry_date should return operation=error with "Civil ID Expiry Date is required".

Suggested test addition
+    public function tryUpdateCivilExpiryRejectsMissingExpiryDate(FunctionalTester $I)
+    {
+        $I->amGoingTo('try to update civil id expiry date without expiry date');
+        $I->sendPOST('v1/account/update-civil-id-expiry-date', [
+            'civil_id' => '70'
+        ]);
+        $I->seeResponseCodeIs(HttpCode::OK); // 200
+        $I->seeResponseContainsJson([
+            'operation' => 'error',
+            'message' => 'Civil ID Expiry Date is required'
+        ]);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@candidate/tests/functional/AccountCest.php` around lines 468 - 493, Add a new
functional test that covers the missing civil_expiry_date branch: create a
method (e.g. tryUpdateCivilExpiryRejectsMissingExpiryDate) modeled like
tryUpdateCivilExpiryRejectsMissingCivilId and
tryUpdateCivilExpiryRejectsInvalidDate that sends a POST to
'v1/account/update-civil-id-expiry-date' including a valid 'civil_id' but
omitting 'civil_expiry_date', then assert HttpCode::OK and that the JSON
response contains 'operation' => 'error' and 'message' => 'Civil ID Expiry Date
is required' to cover the untested controller branch.
README.md (1)

35-45: ⚡ Quick win

Enhance S3 environment variable documentation.

The documentation lists the required environment variables but lacks critical details. Consider adding:

  • Which variables are required vs optional for each environment
  • Example values or format guidance
  • Which deployment environments require which subset of variables
  • Links to setup instructions or deployment notes
📝 Proposed documentation enhancement
 ## S3 Environment Variables
 
-The application expects S3 credentials to be supplied through environment variables rather than committed config values.
+The application requires S3 credentials to be supplied through environment variables rather than committed config values.
+
+### Temporary Bucket (24-hour expiry uploads)
+Required for all environments:
 
 - `AWS_TEMP_BUCKET_KEY`
 - `AWS_TEMP_BUCKET_SECRET`
+
+### Permanent Storage Bucket
+Required for production and Railway deployments:
+
 - `AWS_PERMANENT_S3_ACCESS_KEY_ID`
 - `AWS_PERMANENT_S3_SECRET_ACCESS_KEY`
 - `AWS_PERMANENT_S3_REGION`
 - `AWS_PERMANENT_S3_BUCKET`
+
+See deployment notes in this PR for setup instructions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 35 - 45, Update the "S3 Environment Variables" README
section to clearly mark which env vars are required vs optional, provide example
values/formats (e.g., AWS_TEMP_BUCKET_KEY=AKIA... ,
AWS_PERMANENT_S3_REGION=us-east-1), indicate which deployment environments
(local dev, staging, production, CI) need each subset of variables, and add
links to setup/deployment guides or AWS IAM/S3 credential docs; reference the
exact variable names listed (AWS_TEMP_BUCKET_KEY, AWS_TEMP_BUCKET_SECRET,
AWS_PERMANENT_S3_ACCESS_KEY_ID, AWS_PERMANENT_S3_SECRET_ACCESS_KEY,
AWS_PERMANENT_S3_REGION, AWS_PERMANENT_S3_BUCKET) so readers can map
requirements to their environment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@candidate/modules/v1/controllers/AccountController.php`:
- Around line 1489-1496: The code uses strtotime on $candidate_civil_expiry_date
which is too permissive; replace this with strict parsing using
DateTime::createFromFormat (e.g. 'Y-m-d' or the expected format) and validate
with DateTime::getLastErrors to ensure the input is a calendar-valid date, then
use ->getTimestamp() for $candidate_civil_expiry_timestamp; if parsing or
getLastErrors indicates problems, return the same error response. Ensure you
update the logic around $candidate_civil_expiry_timestamp and the existing error
return so invalid/malformed dates are rejected.

In `@common/components/S3ResourceManager.php`:
- Line 165: The delete() method is incorrectly treating DeleteMarker=false as a
failure; instead, rely on the absence of exceptions/SDK errors to indicate
success and always return true after a successful S3 delete call. In
S3ResourceManager::delete(), stop interpreting $result['DeleteMarker'] as
failure — replace the current return expression with an unconditional true (so
successful SDK responses return true) and ensure any exceptions from the S3
client continue to propagate or are handled upstream.

In `@common/config/main.php`:
- Around line 11-12: The config currently uses empty-string fallbacks for
AWS_TEMP_BUCKET_KEY and AWS_TEMP_BUCKET_SECRET which defers credential errors;
change the fallbacks from '' to null (or false) in the configuration array so
the AWS SDK can use its default credential chain or fail fast, and add a startup
validation step that checks the AWS_TEMP_BUCKET_KEY/AWS_TEMP_BUCKET_SECRET
values (or SDK credential resolution result) and throws a clear, early
configuration error if no credentials are available; update references to these
keys in the code that constructs the S3 client (e.g., the S3 client creation
path) to rely on null rather than empty strings.

In `@common/models/Candidate.php`:
- Around line 2786-2788: Normalize the S3 keys before comparing old vs new to
avoid deleting a freshly-uploaded file: when checking
$this->oldAttributes[$idSide] !== $fileName in Candidate.php, canonicalize both
values (e.g., strip known prefixes like "photos/", normalize leading/trailing
slashes or use basename-equivalent) and then compare those normalized strings;
only call $this->deleteFile('civil-id', $side) if the normalized old key is
non-empty and different from the normalized new key. Ensure you apply the
normalization consistently to both the value from $this->oldAttributes[$idSide]
and $fileName.

In `@environments/circle-ci/common/config/main-local.php`:
- Around line 74-75: The config currently falls back to empty strings for 'key'
and 'secret' (getenv('AWS_TEMP_BUCKET_KEY') ?: '' and
getenv('AWS_TEMP_BUCKET_SECRET') ?: ''), which lets AWS init succeed but fails
later; change these fallbacks to null (e.g., return null instead of '') so the
AWS SDK's credential provider chain is used, or add an explicit early validation
that throws/terminates if either getenv('AWS_TEMP_BUCKET_KEY') or
getenv('AWS_TEMP_BUCKET_SECRET') is missing to fail fast in CI; update the
entries for 'key' and 'secret' accordingly and, if adding validation, place it
during config/bootstrap so missing credentials cause a clear startup error.

In `@environments/dev-server-railway/common/config/main-local.php`:
- Around line 109-110: The config currently uses empty-string fallbacks for
'key' and 'secret' which hides missing AWS credentials; change the getenv
fallback from '' to null so the AWS SDK can use its default credential chain
(i.e., set the 'key' => getenv('AWS_PERMANENT_S3_ACCESS_KEY_ID') ?: null and
'secret' => getenv('AWS_PERMANENT_S3_SECRET_ACCESS_KEY') ?: null), and
optionally add a startup validation routine that checks these config values (or
the resolved SDK credentials) and throws a clear error if neither environment
credentials nor an SDK provider are available; update references in the config
array where 'key' and 'secret' are defined to apply this change.

In `@environments/docker/common/config/main-local.php`:
- Around line 93-94: The config currently falls back to empty strings for 'key'
and 'secret' which defers credential errors; change the fallbacks for
AWS_PERMANENT_S3_ACCESS_KEY_ID and AWS_PERMANENT_S3_SECRET_ACCESS_KEY to null
instead of '' so missing env vars produce immediate/clear errors during SDK
initialization; update the entries for 'key' and 'secret' to return null when
getenv(...) is not set (preserving the getenv calls and variable names) so the
AWS SDK fails fast.

In `@environments/krushn-nginx/common/config/main-local.php`:
- Around line 95-96: The config currently falls back to empty strings for 'key'
and 'secret', which defers credential failures; change the fallbacks so
getenv('AWS_PERMANENT_S3_ACCESS_KEY_ID') and
getenv('AWS_PERMANENT_S3_SECRET_ACCESS_KEY') return null instead of '' (so the
AWS SDK will use its default credential chain or surface a clearer error), e.g.
replace the empty-string fallbacks for the 'key' and 'secret' entries in the
config array with null (or explicitly convert false -> null) and optionally add
environment validation that throws a clear exception if credentials remain null
in environments where they must be present.

In `@environments/krushn/common/config/main-local.php`:
- Around line 94-95: The config currently uses empty-string fallbacks for 'key'
and 'secret' (getenv('AWS_PERMANENT_S3_ACCESS_KEY_ID') ?: '' and
getenv('AWS_PERMANENT_S3_SECRET_ACCESS_KEY') ?: ''), which hides missing env
vars and defers failures; change the fallbacks to null (or remove the
empty-string fallback) so the AWS SDK will use its default credential provider
chain or produce a clear error, and optionally add an explicit check that throws
a clear exception if both values are null to fail fast during initialization.

In `@environments/prod-railway/common/config/main-local.php`:
- Around line 157-158: The config currently falls back to empty strings for the
S3 credentials (the 'key' and 'secret' entries built from
getenv('AWS_PERMANENT_S3_ACCESS_KEY_ID') and
getenv('AWS_PERMANENT_S3_SECRET_ACCESS_KEY')), which defers credential errors to
runtime; update the fallback to null so the AWS SDK can consult its default
credential chain (replace the empty-string fallback with null) and/or add a
startup validation that checks these env vars and throws a clear error (or
exits) if both are missing to fail fast in production; locate the getenv usage
that sets 'key' and 'secret' and implement one of these fixes.

---

Outside diff comments:
In `@environments/dev-server-railway/common/config/main-local.php`:
- Around line 106-117: The config currently sets 'authMethod' =>
\common\components\S3ResourceManager::AUTH_VIA_KEY_AND_SECRET while the comment
claims IAM role usage and the file still defines 'key' and 'secret' from
AWS_PERMANENT_S3_* env vars; because S3ResourceManager treats empty strings as
valid values this can pass empty credentials into S3Client. Either (A) if
Railway provides keys via AWS_PERMANENT_S3_ACCESS_KEY_ID /
AWS_PERMANENT_S3_SECRET_ACCESS_KEY, update the comment to state that credentials
are sourced from those env vars and keep authMethod as AUTH_VIA_KEY_AND_SECRET,
or (B) if IAM role chaining should be used, change authMethod to
\common\components\S3ResourceManager::AUTH_VIA_IAM_ROLE and remove the 'key' and
'secret' entries so the SDK default credential provider (IAM role) is used;
ensure the chosen option is applied consistently across dev-server-railway and
prod-railway.

In `@environments/prod-railway/common/config/main-local.php`:
- Around line 154-166: The comment incorrectly implies production uses embedded
IAM roles; update the block around authMethod (set to
\common\components\S3ResourceManager::AUTH_VIA_KEY_AND_SECRET) to state that the
Railway environment intentionally uses key/secret authentication and that IAM
role-based access is not available here; adjust or remove the lines mentioning
"For Dev and Production servers, access is via server embedded IAM roles so no
key/secret required" and optionally note the relevant env vars
(AWS_PERMANENT_S3_ACCESS_KEY_ID, AWS_PERMANENT_S3_SECRET_ACCESS_KEY) are
required in this deployment.

---

Nitpick comments:
In `@candidate/tests/functional/AccountCest.php`:
- Around line 468-493: Add a new functional test that covers the missing
civil_expiry_date branch: create a method (e.g.
tryUpdateCivilExpiryRejectsMissingExpiryDate) modeled like
tryUpdateCivilExpiryRejectsMissingCivilId and
tryUpdateCivilExpiryRejectsInvalidDate that sends a POST to
'v1/account/update-civil-id-expiry-date' including a valid 'civil_id' but
omitting 'civil_expiry_date', then assert HttpCode::OK and that the JSON
response contains 'operation' => 'error' and 'message' => 'Civil ID Expiry Date
is required' to cover the untested controller branch.

In `@README.md`:
- Around line 35-45: Update the "S3 Environment Variables" README section to
clearly mark which env vars are required vs optional, provide example
values/formats (e.g., AWS_TEMP_BUCKET_KEY=AKIA... ,
AWS_PERMANENT_S3_REGION=us-east-1), indicate which deployment environments
(local dev, staging, production, CI) need each subset of variables, and add
links to setup/deployment guides or AWS IAM/S3 credential docs; reference the
exact variable names listed (AWS_TEMP_BUCKET_KEY, AWS_TEMP_BUCKET_SECRET,
AWS_PERMANENT_S3_ACCESS_KEY_ID, AWS_PERMANENT_S3_SECRET_ACCESS_KEY,
AWS_PERMANENT_S3_REGION, AWS_PERMANENT_S3_BUCKET) so readers can map
requirements to their environment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6e26ece-016a-4198-88f1-4b43d505c132

📥 Commits

Reviewing files that changed from the base of the PR and between 7b023ff and 73c4bb2.

📒 Files selected for processing (12)
  • README.md
  • candidate/modules/v1/controllers/AccountController.php
  • candidate/tests/functional/AccountCest.php
  • common/components/S3ResourceManager.php
  • common/config/main.php
  • common/models/Candidate.php
  • environments/circle-ci/common/config/main-local.php
  • environments/dev-server-railway/common/config/main-local.php
  • environments/docker/common/config/main-local.php
  • environments/krushn-nginx/common/config/main-local.php
  • environments/krushn/common/config/main-local.php
  • environments/prod-railway/common/config/main-local.php

Comment on lines +1489 to +1496
$candidate_civil_expiry_timestamp = strtotime($candidate_civil_expiry_date);

$candidate->scenario = "updateCivilExpiryDateAndCivilID";
if ($candidate_civil_expiry_timestamp === false) {
return [
"operation" => "error",
"message" => Yii::t('candidate', "Civil ID Expiry Date is invalid"),
];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use strict calendar validation for civil_expiry_date

strtotime() is too permissive and can accept malformed dates by coercion. This allows invalid civil expiry values to be stored as different valid dates.

Suggested fix
-        $candidate_civil_expiry_timestamp = strtotime($candidate_civil_expiry_date);
-
-        if ($candidate_civil_expiry_timestamp === false) {
+        $dt = \DateTimeImmutable::createFromFormat('!Y-m-d', $candidate_civil_expiry_date);
+        $dtErrors = \DateTimeImmutable::getLastErrors();
+
+        if (
+            !$dt ||
+            $dtErrors['warning_count'] > 0 ||
+            $dtErrors['error_count'] > 0 ||
+            $dt->format('Y-m-d') !== $candidate_civil_expiry_date
+        ) {
             return [
                 "operation" => "error",
                 "message" => Yii::t('candidate', "Civil ID Expiry Date is invalid"),
             ];
         }
@@
-            $candidate->candidate_civil_expiry_date = date('Y-m-d', $candidate_civil_expiry_timestamp);
+            $candidate->candidate_civil_expiry_date = $dt->format('Y-m-d');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@candidate/modules/v1/controllers/AccountController.php` around lines 1489 -
1496, The code uses strtotime on $candidate_civil_expiry_date which is too
permissive; replace this with strict parsing using DateTime::createFromFormat
(e.g. 'Y-m-d' or the expected format) and validate with DateTime::getLastErrors
to ensure the input is a calendar-valid date, then use ->getTimestamp() for
$candidate_civil_expiry_timestamp; if parsing or getLastErrors indicates
problems, return the same error response. Ensure you update the logic around
$candidate_civil_expiry_timestamp and the existing error return so
invalid/malformed dates are rejected.

Comment thread common/components/S3ResourceManager.php Outdated
Comment thread common/config/main.php
Comment on lines +11 to +12
'key' => getenv('AWS_TEMP_BUCKET_KEY') ?: '',
'secret' => getenv('AWS_TEMP_BUCKET_SECRET') ?: '',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Empty-string fallbacks silently defer credential failures to runtime.

When AWS_TEMP_BUCKET_KEY or AWS_TEMP_BUCKET_SECRET are unset, the configuration falls back to empty strings. The AWS SDK accepts empty credentials during initialization but fails later when S3 operations are attempted, producing "invalid credentials" errors that are harder to diagnose than a missing-configuration error at startup.

Consider using null or false as the fallback so the application fails fast during initialization if credentials are missing, or add explicit validation at startup.

🔒 Proposed fix to fail fast on missing credentials
-            'key' => getenv('AWS_TEMP_BUCKET_KEY') ?: '',
-            'secret' => getenv('AWS_TEMP_BUCKET_SECRET') ?: '',
+            'key' => getenv('AWS_TEMP_BUCKET_KEY') ?: null,
+            'secret' => getenv('AWS_TEMP_BUCKET_SECRET') ?: null,

This allows the S3 client to fall back to AWS SDK's default credential chain (environment, IAM role, etc.) or fail explicitly if no credentials are available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/config/main.php` around lines 11 - 12, The config currently uses
empty-string fallbacks for AWS_TEMP_BUCKET_KEY and AWS_TEMP_BUCKET_SECRET which
defers credential errors; change the fallbacks from '' to null (or false) in the
configuration array so the AWS SDK can use its default credential chain or fail
fast, and add a startup validation step that checks the
AWS_TEMP_BUCKET_KEY/AWS_TEMP_BUCKET_SECRET values (or SDK credential resolution
result) and throws a clear, early configuration error if no credentials are
available; update references to these keys in the code that constructs the S3
client (e.g., the S3 client creation path) to rely on null rather than empty
strings.

Comment on lines +2786 to +2788
if (!empty($this->oldAttributes[$idSide]) && $this->oldAttributes[$idSide] !== $fileName) {
$this->deleteFile('civil-id', $side);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize both old/new keys before deciding deletion

Line 2786 compares oldAttributes[$idSide] to raw $fileName. If old is photos/foo.jpg and new is foo.jpg, this condition is true even though both target the same S3 key, so the subsequent delete can remove the newly copied file.

Suggested fix
-            if (!empty($this->oldAttributes[$idSide]) && $this->oldAttributes[$idSide] !== $fileName) {
-                $this->deleteFile('civil-id', $side);
-            }
+            $oldPath = !empty($this->oldAttributes[$idSide]) ? $this->oldAttributes[$idSide] : null;
+            $oldTargetPath = $oldPath
+                ? (strpos($oldPath, 'photos/') === 0 ? $oldPath : "photos/" . $oldPath)
+                : null;
+
+            if ($oldTargetPath && $oldTargetPath !== $targetPath) {
+                $this->deleteFile('civil-id', $side);
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@common/models/Candidate.php` around lines 2786 - 2788, Normalize the S3 keys
before comparing old vs new to avoid deleting a freshly-uploaded file: when
checking $this->oldAttributes[$idSide] !== $fileName in Candidate.php,
canonicalize both values (e.g., strip known prefixes like "photos/", normalize
leading/trailing slashes or use basename-equivalent) and then compare those
normalized strings; only call $this->deleteFile('civil-id', $side) if the
normalized old key is non-empty and different from the normalized new key.
Ensure you apply the normalization consistently to both the value from
$this->oldAttributes[$idSide] and $fileName.

Comment thread environments/circle-ci/common/config/main-local.php Outdated
Comment thread environments/dev-server-railway/common/config/main-local.php Outdated
Comment thread environments/docker/common/config/main-local.php Outdated
Comment thread environments/krushn-nginx/common/config/main-local.php Outdated
Comment thread environments/krushn/common/config/main-local.php Outdated
Comment thread environments/prod-railway/common/config/main-local.php Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@BAWES

BAWES commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

🤖 Operations Status Check

This PR has been idle for 632h. Please provide a status update or push changes to keep it moving. If it's ready for review, please re-request. If blocked, please note what's needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants