Skip to content

Commit f73c7c2

Browse files
committed
[CORE-8147] deprecated filestack
1 parent 9ab4265 commit f73c7c2

43 files changed

Lines changed: 322 additions & 1385 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/upgrades/angular-18-to-19-security-fixes.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,37 @@ review and update as needed:
341341
- `Apollo.use()`, `Apollo.watchQuery()`, `Apollo.mutate()`, `Apollo.query()` signatures
342342
- check for changes in `ApolloModule.forRoot()` vs standalone provider pattern
343343

344+
### 5.5. Lambda@Edge forwarder — S3 path prefix (COMPLETED March 2026)
345+
346+
**Root cause:** The Angular 19 `application` builder (`@angular-devkit/build-angular ^19`) changed the build output structure by introducing a `browser/` subdirectory:
347+
348+
| Builder | Angular version | Output path | S3 key prefix |
349+
|---------|-----------------|-------------|---------------|
350+
| `browser` (webpack) | 17 / 18 | `dist/v3/{locale}/` | `/{locale}/` |
351+
| `application` (esbuild) | **19+** | `dist/v3/browser/{locale}/` | `/browser/{locale}/` |
352+
353+
This mismatch caused `lambda/forwarder/index.js` to rewrite all CloudFront URIs to non-existent S3 keys (e.g. `/en-US/index.html` instead of `/browser/en-US/index.html`). S3 returned `403 AccessDenied` — surfaced as raw XML to every user loading the app.
354+
355+
**Fix applied to `lambda/forwarder/index.js`:**
356+
357+
```javascript
358+
// before (Angular 17/18)
359+
request.uri = "/en-US/index.html"; // fallback
360+
request.uri = `/${locale}/index.html`; // spa route
361+
// (no static-asset rewrite needed — assets were at root)
362+
363+
// after (Angular 19+)
364+
request.uri = "/browser/en-US/index.html"; // fallback
365+
request.uri = `/browser/${locale}/index.html`; // spa route
366+
} else if (!request.uri.startsWith('/browser/')) {
367+
request.uri = `/browser${request.uri}`; // static assets
368+
}
369+
```
370+
371+
**Validation:** Confirmed working on `p2-stage` after deployment (March 2026).
372+
373+
**Future Angular upgrades:** check `ls dist/v3/` after building to verify the output structure before merging. If thenstructure changes again, update `forwarder/index.js` in the same PR. See `lambda/README.md` for the full dependency documentation.
374+
344375
---
345376

346377
## Phase 6 — Testing & Validation
@@ -446,6 +477,7 @@ npm audit > ./output/angular-19-upgrade-audit.log 2>&1
446477
| TypeScript 5.6 stricter checks | incremental fix of any new type errors |
447478
| ngx-quill v27 regressions | minimal risk — usually just peer dep bump |
448479
| CI build failures | run full CI pipeline before merging |
480+
| **Lambda@Edge S3 path mismatch** (**occurred** March 2026) | Angular 19 `application` builder adds `browser/` subdirectory; `lambda/forwarder/index.js` must be updated in the same PR — see §5.5 |
449481

450482
**Rollback plan:** revert to the pre-upgrade commit on `angular-eos-upgrades-prerelease` branch. all changes are confined to `package.json`, `package-lock.json`, and targeted source fixes.
451483

@@ -480,10 +512,13 @@ npm audit > ./output/angular-19-upgrade-audit.log 2>&1
480512
- [ ] update uppy service/component if API changed
481513
- [ ] update apollo-angular usage if API changed
482514
- [ ] fix any Angular 19 deprecation warnings
515+
- [x] update `lambda/forwarder/index.js` for `browser/` path prefix *(completed March 2026)*
483516
- [ ] **Phase 6:** Validation
484517
- [ ] `npm run prebuildv3` succeeds
485518
- [ ] `ng build v3` succeeds
486519
- [ ] `npm test` passes
487520
- [ ] `npm run lint` passes
488521
- [ ] `npm audit` shows reduced vulnerability count
489522
- [ ] manual testing on staging
523+
- [ ] verify app loads at `https://app.<stack>.practera.com/en-US` without S3 AccessDenied
524+
- [ ] verify deep-links with query params (magic-link login) resolve to correct locale

lambda/README.md

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,76 @@
11
### Description
22

3-
This directory will hold `lambda@edge` functions.
3+
This directory holds `lambda@edge` functions deployed to CloudFront as `origin-request` handlers.
44

5-
`forwarder` - lambda function that sits infront of the CDN, handles `globalization` redirection.
6-
`versioner` - function to create lambda function version.
5+
`forwarder` - rewrites incoming CloudFront URIs to the correct S3 object keys for the Angular i18n locale builds.
6+
`versioner` - creates a numbered Lambda version ARN that CloudFront requires for Lambda@Edge associations.
7+
8+
---
9+
10+
### i18n / Angular Build Output Dependency
11+
12+
The `forwarder` function is **tightly coupled** to the Angular build output directory structure. Breaking this coupling causes S3 `AccessDenied` (403) errors surfaced as raw XML to end users.
13+
14+
#### How Angular i18n builds are deployed
15+
16+
`projects/v3` is compiled with `"localize": true` in `angular.json`, producing one subfolder per locale:
17+
18+
| Angular version | Build output path | S3 object key prefix |
19+
|-----------------|-------------------|----------------------|
20+
| 17 / 18 (webpack builder) | `dist/v3/{locale}/` | `/{locale}/` |
21+
| **19+ (application builder)** | `dist/v3/browser/{locale}/` | `/browser/{locale}/` |
22+
23+
The `aws s3 sync dist/v3/ s3://$BUCKET --delete` step (CI/CD step 22) mirrors this structure directly into S3, so the S3 key prefix always matches the build output.
24+
25+
#### What the forwarder does
26+
27+
CloudFront does **not** forward query strings to S3 (`QueryString: false`). When a user visits a deep-link such as `/en-US?auth_token=…`, CloudFront calls the forwarder with `request.uri = "/en-US"`. The forwarder:
28+
29+
1. Extracts the first path segment as the locale (`en-US`, `ja`, `ms`, `es`).
30+
2. For unknown locales or bare `/`, falls back to the default locale.
31+
3. For SPA routes (no file extension), rewrites to `/{prefix}/{locale}/index.html`.
32+
4. For static assets (with file extension), rewrites to `/{prefix}{original_uri}`.
33+
5. Passes `version.json` requests through unchanged (whitelist).
34+
35+
#### Supported locales
36+
37+
The locale whitelist in `forwarder/index.js` must stay in sync with the locales configured in `angular.json`:
38+
39+
```javascript
40+
const locales = ["en-US", "ja", "ms", "es"];
41+
```
42+
43+
To add a new locale: update both `angular.json` (`i18n.locales`) **and** the `locales` array in `forwarder/index.js`.
44+
45+
#### ⚠️ What to check on every Angular major version upgrade
46+
47+
When upgrading Angular major versions, verify the build output structure has not changed before deploying:
48+
49+
```bash
50+
# after building, check what subdirectories are produced
51+
ls dist/v3/
52+
# Angular 19+: should show browser/
53+
# Angular 17/18: should show en-US/ ja/ ms/ es/
54+
```
55+
56+
If the output structure changes, update the path prefix in `forwarder/index.js` **before** merging to trunk so both changes deploy together in the same CI/CD run.
57+
58+
#### Incident history
59+
60+
| Date | Trigger | Symptom | Fix |
61+
|------|---------|---------|-----|
62+
| March 2026 | Angular 18 → 19 upgrade | S3 `AccessDenied` XML shown to all users on `app.p2-stage.practera.com` | Added `/browser/` prefix to all rewritten URIs in `forwarder/index.js` |
63+
64+
---
765

866
### Deployment
967

10-
Once `AWS` credentials is ready, just run `deploy.sh`. Make sure you installed [sam](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) on your machine.
68+
Once `AWS` credentials are ready, run `deploy.sh`. Requires [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html).
69+
70+
In CI/CD (`p2-stage-appv3.yml`) the deploy sequence is:
71+
1. Build Angular app (`ng build v3`)
72+
2. Deploy Lambda@Edge (`bash lambda/deploy.sh`) → exports `HandlerVersionArn`
73+
3. Deploy CloudFormation/Serverless stack (picks up new `HandlerVersionArn`)
74+
4. Sync `dist/v3/` to S3
75+
76+
Both the Lambda function and the S3 content must reflect the same output path structure.

lambda/forwarder/index.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const path = require('path');
22

3-
exports.handler = async (evt, context, cb) => {
3+
exports.handler = async (evt) => {
44
const { request } = evt.Records[0].cf;
55

66
console.log(`Original Uri: ${request.uri}`);
@@ -11,21 +11,27 @@ exports.handler = async (evt, context, cb) => {
1111
const locales = ["en-US", "ja", "ms", "es"];
1212
const lastPartUrl = uriParts[uriParts.length - 1];
1313

14-
// whitelisted version.json request
14+
// whitelisted version.json request — note: query strings are in request.querystring,
15+
// not in request.uri, so only the path filename is tested here
1516
console.log("trailingURL::", lastPartUrl);
16-
if (lastPartUrl.match(/^version\.json(?:\?t=\d+)?$/) !== null) {
17-
return cb(null, request);
17+
if (lastPartUrl.match(/^version\.json$/) !== null) {
18+
return request;
1819
}
1920

2021
if (locale === "" || locale === "index.html" || !locales.includes(locale)) {
2122
request.uri = "/en-US/index.html";
2223
console.log("Go to default page and locale.");
23-
return cb(null, request);
24+
return request;
2425
}
2526

2627
const fileExt = path.extname(lastPartUrl);
27-
if (!fileExt) request.uri = `/${locale}/index.html`;
28+
if (!fileExt) {
29+
request.uri = `/browser/${locale}/index.html`;
30+
} else if (!request.uri.startsWith('/browser/')) {
31+
// rewrite static asset paths to match Angular 19+ application builder output
32+
request.uri = `/browser${request.uri}`;
33+
}
2834

2935
console.log(`New Uri: ${request.uri}`);
30-
return cb(null, request);
36+
return request;
3137
};

lambda/forwarder/template.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Resources:
3737
Properties:
3838
Handler: index.handler
3939
CodeUri: ./bin/handler.zip
40-
Runtime: nodejs18.x
40+
Runtime: nodejs22.x
4141
Timeout: 10
4242
Role: !GetAtt HandlerRole.Arn
4343
HandlerVersion:
@@ -53,4 +53,4 @@ Outputs:
5353
Description: "Arn Version for Lambda function to associate unto CDN"
5454
Value: !GetAtt HandlerVersion.FunctionArn
5555
Export:
56-
Name: !Sub "${StackName}-HandlerVersion-${Env}"
56+
Name: !Sub "${StackName}-HandlerVersion-${Env}"

lambda/versioner/template.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Resources:
1919
Properties:
2020
Handler: index.handler
2121
CodeUri: ./bin/handler.zip
22-
Runtime: nodejs18.x
22+
Runtime: nodejs22.x
2323
Timeout: 10
2424
Role: !GetAtt LambdaVersionHelperRole.Arn
2525
LambdaVersionHelperRole:
@@ -51,4 +51,4 @@ Outputs:
5151
Description: "Lambda Function Versioner ARN"
5252
Value: !GetAtt Versioner.Arn
5353
Export:
54-
Name: !Sub "${StackName}-LambdaVersionerArn-${Env}"
54+
Name: !Sub "${StackName}-LambdaVersionerArn-${Env}"

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
"core-js": "^3.21.1",
6262
"dayjs": "^1.11.10",
6363
"exif-js": "^2.3.0",
64-
"filestack-js": "^3.30.0",
6564
"franc-min": "^6.2.0",
6665
"graphql": "^16.8.1",
6766
"ics": "^3.7.2",

projects/v3/src/app/components/activity/activity.component.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@
4545
}
4646

4747
.focusable:focus {
48-
border: 1px solid var(--ion-color-primary);
49-
display: flex;
48+
outline: 2px solid var(--ion-color-primary);
49+
outline-offset: -2px;
5050
}

projects/v3/src/app/components/components.module.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import { FastFeedbackComponent } from './fast-feedback/fast-feedback.component';
1818
import { ReviewRatingComponent } from './review-rating/review-rating.component';
1919
import { CircleProgressComponent } from './circle-progress/circle-progress.component';
2020
import { NgCircleProgressModule } from 'ng-circle-progress';
21-
import { FilestackComponent } from './filestack/filestack.component';
22-
import { FilestackPreviewComponent } from './filestack-preview/filestack-preview.component';
21+
import { FilePreviewComponent } from './file-preview/file-preview.component';
2322
import { ContactNumberFormComponent } from './contact-number-form/contact-number-form.component';
2423
import { ClickableItemComponent } from './clickable-item/clickable-item.component';
2524
import { AssessmentComponent } from './assessment/assessment.component';
@@ -90,8 +89,7 @@ const largeCircleDefaultConfig = {
9089
FilePopupComponent,
9190
FileDisplayComponent,
9291
VideoConversionComponent,
93-
FilestackComponent,
94-
FilestackPreviewComponent,
92+
FilePreviewComponent,
9593
ImgComponent,
9694
ListItemComponent,
9795
LockTeamAssessmentPopUpComponent,
@@ -136,8 +134,7 @@ const largeCircleDefaultConfig = {
136134
FilePopupComponent,
137135
FileDisplayComponent,
138136
VideoConversionComponent,
139-
FilestackComponent,
140-
FilestackPreviewComponent,
137+
FilePreviewComponent,
141138
ImgComponent,
142139
IonicModule,
143140
ListItemComponent,

projects/v3/src/app/components/file-display/file-display.component.spec.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange, DebugElement } from '@angular/core';
33
import { ComponentFixture, TestBed, fakeAsync, flushMicrotasks, waitForAsync, tick } from '@angular/core/testing';
44
import { FileDisplayComponent } from './file-display.component';
5-
import { FilestackService } from '@v3/services/filestack.service';
65
import { ReactiveFormsModule, FormControl } from '@angular/forms';
76
import { UtilsService } from '@v3/services/utils.service';
87
import { TestUtils } from '@testingv3/utils';
@@ -19,7 +18,6 @@ class OnChangedValues extends SimpleChange {
1918
describe('FileDisplayComponent', () => {
2019
let component: FileDisplayComponent;
2120
let fixture: ComponentFixture<FileDisplayComponent>;
22-
let filestackSpy: jasmine.SpyObj<FilestackService>;
2321
let utilsSpy: jasmine.SpyObj<UtilsService>;
2422

2523
beforeEach(waitForAsync(() => {
@@ -32,14 +30,6 @@ describe('FileDisplayComponent', () => {
3230
provide: UtilsService,
3331
useClass: TestUtils,
3432
},
35-
{
36-
provide: FilestackService,
37-
useValue: jasmine.createSpyObj('FilestackService', [
38-
'previewFile',
39-
'getWorkflowStatus',
40-
'metadata'
41-
])
42-
},
4333
{
4434
provide: ModalController,
4535
useValue: jasmine.createSpyObj('ModalController', {
@@ -55,7 +45,6 @@ describe('FileDisplayComponent', () => {
5545
beforeEach(() => {
5646
fixture = TestBed.createComponent(FileDisplayComponent);
5747
component = fixture.debugElement.componentInstance;
58-
filestackSpy = TestBed.inject(FilestackService) as jasmine.SpyObj<FilestackService>;
5948
utilsSpy = TestBed.inject(UtilsService) as jasmine.SpyObj<UtilsService>;
6049
});
6150

projects/v3/src/app/components/file-display/file-display.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { FileInput, TusFileResponse } from '../types/assessment';
1111
import { FilePopupComponent } from '../file-popup/file-popup.component';
1212
import { ModalController } from '@ionic/angular';
1313

14-
// @TODO: make compatible with FileStack format (remove when no longer needed)
15-
interface FileStackCompatible extends TusFileResponse {
14+
// backward-compatible file type that includes legacy property names
15+
interface DisplayableFile extends TusFileResponse {
1616
filename: string;
1717
mimetype: string;
1818
url: string;
@@ -26,7 +26,7 @@ interface FileStackCompatible extends TusFileResponse {
2626
})
2727
export class FileDisplayComponent {
2828
@Input() fileType: string = 'any';
29-
@Input() file?: FileStackCompatible;
29+
@Input() file?: DisplayableFile;
3030
@Input() isFileComponent: boolean = false; // flag parent component is FileComponent
3131
@ViewChild('videoEle') videoEle?: ElementRef = new ElementRef(null);
3232
@Output() removeFile: EventEmitter<any> = new EventEmitter<any>();

0 commit comments

Comments
 (0)