Skip to content

Commit 5d677a0

Browse files
committed
feat: user document storage
1 parent 292e8f3 commit 5d677a0

35 files changed

+1431
-46
lines changed

api/.env-template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ FORMATION_API_ACCOUNT=
77
FORMATION_API_KEY=
88
FORMATION_API_BASE_URL=
99
BUSINESS_NAME_BASE_URL=
10+
DOCUMENT_S3_BUCKET=

api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"dependencies": {
3535
"@aws-sdk/client-dynamodb": "3.58.0",
36+
"@aws-sdk/client-s3": "^3.58.0",
3637
"@aws-sdk/lib-dynamodb": "3.58.0",
3738
"@aws-sdk/util-dynamodb": "3.58.0",
3839
"airtable": "0.11.3",

api/serverless.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const myNJServiceUrl = process.env.MYNJ_SERVICE_URL || "";
3838
const useFakeSelfReg = process.env.USE_FAKE_SELF_REG || "";
3939
const intercomHashSecret = process.env.INTERCOM_HASH_SECRET || "";
4040

41+
const documentS3Bucket = `nj-bfs-user-documents-${ssmLocation}`;
42+
4143
const serverlessConfiguration: AWS = {
4244
useDotenv: true,
4345
service: "businessnjgov-api",
@@ -100,6 +102,11 @@ const serverlessConfiguration: AWS = {
100102
Action: ["s3:GetObject"],
101103
Resource: "arn:aws:s3:::*/*",
102104
},
105+
{
106+
Effect: "Allow",
107+
Action: ["s3:PutObject", "s3:ListBucket", "s3:GetObject"],
108+
Resource: `arn:aws:s3:::${documentS3Bucket}/*`,
109+
},
103110
{
104111
Effect: "Allow",
105112
Action: ["secretsmanager:GetSecretValue"],
@@ -116,6 +123,7 @@ const serverlessConfiguration: AWS = {
116123
AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1",
117124
USERS_TABLE: usersTable,
118125
LICENSE_STATUS_BASE_URL: licenseStatusBaseUrl,
126+
DOCUMENT_S3_BUCKET: documentS3Bucket,
119127
CMS_OAUTH_CLIENT_ID: cmsoAuthClientId,
120128
CMS_OAUTH_CLIENT_SECRET: cmsoAuthClientSecret,
121129
BUSINESS_NAME_BASE_URL: businessNameBaseUrl,

api/src/api/formationRouter.test.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,23 @@ import {
99
generateGetFilingResponse,
1010
generateUserData,
1111
} from "../../test/factories";
12+
import { saveFileFromUrl } from "../domain/s3Writer";
1213
import { FormationClient, UserDataClient } from "../domain/types";
1314
import { formationRouterFactory } from "./formationRouter";
14-
import { getSignedInUserId } from "./userRouter";
15+
import { getSignedInUser, getSignedInUserId } from "./userRouter";
1516

1617
jest.mock("./userRouter", () => ({
1718
getSignedInUserId: jest.fn(),
19+
getSignedInUser: jest.fn(),
1820
}));
21+
22+
jest.mock("../domain/s3Writer.ts", () => ({
23+
saveFileFromUrl: jest.fn(),
24+
}));
25+
26+
const fakeSaveFileFromUrl = saveFileFromUrl as jest.Mock;
1927
const fakeSignedInUserId = getSignedInUserId as jest.Mock;
28+
const fakeSignedInUser = getSignedInUser as jest.Mock;
2029

2130
describe("formationRouter", () => {
2231
let app: Express;
@@ -26,6 +35,13 @@ describe("formationRouter", () => {
2635
beforeEach(async () => {
2736
jest.resetAllMocks();
2837
fakeSignedInUserId.mockReturnValue("some-id");
38+
fakeSignedInUser.mockReturnValue({
39+
sub: "1234",
40+
"custom:myNJUserKey": "1234myNJUserKey",
41+
"custom:identityId": "us-east-1:identityId",
42+
email: "whatever@gmail.com",
43+
identities: undefined,
44+
});
2945
stubFormationClient = {
3046
form: jest.fn(),
3147
getCompletedFiling: jest.fn(),
@@ -99,6 +115,18 @@ describe("formationRouter", () => {
99115
});
100116

101117
describe("/completed-filing", () => {
118+
let dateNowSpy: jest.SpyInstance;
119+
beforeEach(() => {
120+
dateNowSpy = jest.spyOn(Date, "now").mockImplementation(() => 1487076708000);
121+
fakeSaveFileFromUrl.mockReset();
122+
fakeSaveFileFromUrl.mockImplementation((link, location, bucket) =>
123+
Promise.resolve(`http://${location}`)
124+
);
125+
});
126+
afterAll(() => {
127+
dateNowSpy.mockRestore();
128+
});
129+
102130
it("saves and returns updated user data with get-filing response", async () => {
103131
const getFilingResponse = generateGetFilingResponse({ success: true });
104132
stubFormationClient.getCompletedFiling.mockResolvedValue(getFilingResponse);
@@ -127,9 +155,19 @@ describe("formationRouter", () => {
127155
...userData.profileData,
128156
entityId: getFilingResponse.entityId,
129157
dateOfFormation: userData.formationData.formationFormData.businessStartDate,
158+
documents: {
159+
...userData.profileData.documents,
160+
formationDoc: `http://us-east-1:identityId/formationDoc-1487076708000.pdf`,
161+
certifiedDoc: `http://us-east-1:identityId/certifiedDoc-1487076708000.pdf`,
162+
standingDoc: `http://us-east-1:identityId/standingDoc-1487076708000.pdf`,
163+
},
130164
},
131165
};
132-
166+
expect(fakeSaveFileFromUrl).toHaveBeenCalledWith(
167+
getFilingResponse.formationDoc,
168+
`us-east-1:identityId/formationDoc-1487076708000.pdf`,
169+
undefined
170+
);
133171
expect(response.body).toEqual(expectedNewUserData);
134172
expect(stubUserDataClient.put).toHaveBeenCalledWith(expectedNewUserData);
135173
expect(stubFormationClient.getCompletedFiling).toHaveBeenCalledWith("some-formation-id");
@@ -155,5 +193,57 @@ describe("formationRouter", () => {
155193
},
156194
});
157195
});
196+
197+
it("only fetches files that are in the filingResponse", async () => {
198+
const getFilingResponse = generateGetFilingResponse({ success: true, certifiedDoc: "" });
199+
stubFormationClient.getCompletedFiling.mockResolvedValue(getFilingResponse);
200+
201+
const userData = generateUserData({
202+
formationData: generateFormationData({
203+
formationResponse: generateFormationSubmitResponse({ formationId: "some-formation-id" }),
204+
}),
205+
});
206+
stubUserDataClient.get.mockResolvedValue(userData);
207+
await request(app).get(`/completed-filing`).send();
208+
209+
const expectedNewUserData = {
210+
...userData,
211+
formationData: {
212+
...userData.formationData,
213+
getFilingResponse: getFilingResponse,
214+
},
215+
taskProgress: {
216+
...userData.taskProgress,
217+
"form-business-entity": "COMPLETED",
218+
},
219+
profileData: {
220+
...userData.profileData,
221+
entityId: getFilingResponse.entityId,
222+
dateOfFormation: userData.formationData.formationFormData.businessStartDate,
223+
documents: {
224+
...userData.profileData.documents,
225+
formationDoc: `http://us-east-1:identityId/formationDoc-1487076708000.pdf`,
226+
standingDoc: `http://us-east-1:identityId/standingDoc-1487076708000.pdf`,
227+
},
228+
},
229+
};
230+
231+
expect(fakeSaveFileFromUrl).toHaveBeenCalledWith(
232+
getFilingResponse.formationDoc,
233+
`us-east-1:identityId/formationDoc-1487076708000.pdf`,
234+
process.env.DOCUMENT_S3_BUCKET
235+
);
236+
expect(fakeSaveFileFromUrl).toHaveBeenCalledWith(
237+
getFilingResponse.standingDoc,
238+
`us-east-1:identityId/standingDoc-1487076708000.pdf`,
239+
process.env.DOCUMENT_S3_BUCKET
240+
);
241+
expect(fakeSaveFileFromUrl).not.toHaveBeenCalledWith(
242+
getFilingResponse.certifiedDoc,
243+
`us-east-1:identityId/certifiedDoc-1487076708000.pdf`,
244+
process.env.DOCUMENT_S3_BUCKET
245+
);
246+
expect(stubUserDataClient.put).toHaveBeenCalledWith(expectedNewUserData);
247+
});
158248
});
159249
});

api/src/api/formationRouter.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { FormationSubmitResponse, GetFilingResponse } from "@shared/formationData";
2+
import { ProfileDocuments } from "@shared/profileData";
23
import { Router } from "express";
4+
import { saveFileFromUrl } from "../domain/s3Writer";
35
import { FormationClient, UserDataClient } from "../domain/types";
4-
import { getSignedInUserId } from "./userRouter";
6+
import { getSignedInUser, getSignedInUserId } from "./userRouter";
57

68
export const formationRouterFactory = (
79
formationClient: FormationClient,
@@ -32,6 +34,7 @@ export const formationRouterFactory = (
3234
});
3335

3436
router.get("/completed-filing", async (req, res) => {
37+
const signedInUser = getSignedInUser(req);
3538
const signedInUserId = getSignedInUserId(req);
3639
const userData = await userDataClient.get(signedInUserId);
3740

@@ -46,10 +49,34 @@ export const formationRouterFactory = (
4649
const taskProgress = userData.taskProgress;
4750
let entityId = userData.profileData.entityId;
4851
let dateOfFormation = userData.profileData.dateOfFormation;
52+
const documents: Partial<ProfileDocuments> = {};
53+
4954
if (getFilingResponse.success) {
5055
taskProgress["form-business-entity"] = "COMPLETED";
5156
entityId = getFilingResponse.entityId;
5257
dateOfFormation = userData.formationData.formationFormData.businessStartDate;
58+
59+
documents["formationDoc"] = await saveFileFromUrl(
60+
getFilingResponse.formationDoc,
61+
`${signedInUser["custom:identityId"]}/formationDoc-${Date.now()}.pdf`,
62+
process.env.DOCUMENT_S3_BUCKET as string
63+
);
64+
65+
if (getFilingResponse.certifiedDoc) {
66+
documents["certifiedDoc"] = await saveFileFromUrl(
67+
getFilingResponse.certifiedDoc,
68+
`${signedInUser["custom:identityId"]}/certifiedDoc-${Date.now()}.pdf`,
69+
process.env.DOCUMENT_S3_BUCKET as string
70+
);
71+
}
72+
73+
if (getFilingResponse.standingDoc) {
74+
documents["standingDoc"] = await saveFileFromUrl(
75+
getFilingResponse.standingDoc,
76+
`${signedInUser["custom:identityId"]}/standingDoc-${Date.now()}.pdf`,
77+
process.env.DOCUMENT_S3_BUCKET as string
78+
);
79+
}
5380
}
5481
const userDataWithResponse = {
5582
...userData,
@@ -62,12 +89,14 @@ export const formationRouterFactory = (
6289
...userData.profileData,
6390
entityId,
6491
dateOfFormation,
92+
documents: { ...userData.profileData.documents, ...documents },
6593
},
6694
};
6795
await userDataClient.put(userDataWithResponse);
6896
res.json(userDataWithResponse);
6997
})
70-
.catch(() => {
98+
.catch((error) => {
99+
console.error(error);
71100
res.status(500).json();
72101
});
73102
});

api/src/api/userRouter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const getTokenFromHeader = (req: Request): string => {
1616
type CognitoJWTPayload = {
1717
sub: string;
1818
"custom:myNJUserKey": string;
19+
"custom:identityId": string | undefined;
1920
email: string;
2021
identities: CognitoIdentityPayload[] | undefined;
2122
};
@@ -29,8 +30,11 @@ type CognitoIdentityPayload = {
2930
userId: string;
3031
};
3132

33+
export const getSignedInUser = (req: Request): CognitoJWTPayload =>
34+
jwt.decode(getTokenFromHeader(req)) as CognitoJWTPayload;
35+
3236
export const getSignedInUserId = (req: Request): string => {
33-
const signedInUser = jwt.decode(getTokenFromHeader(req)) as CognitoJWTPayload;
37+
const signedInUser = getSignedInUser(req);
3438
const myNJIdentityPayload = signedInUser.identities?.find((it) => it.providerName === "myNJ");
3539
return myNJIdentityPayload?.userId || signedInUser.sub;
3640
};

api/src/db/migrations/migrations.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { migrate_v42_to_v43 } from "./v43_add_initial_flow_to_profile_data";
4040
import { migrate_v43_to_v44 } from "./v44_add_cannabis_license_to_profile_data";
4141
import { migrate_v44_to_v45 } from "./v45_add_hidden_opportunities_to_preferences";
4242
import { migrate_v45_to_v46 } from "./v46_add_task_item_checklist";
43+
import { migrate_v46_to_v47 } from "./v47_add_profile_documents";
4344
import { migrate_v3_to_v4 } from "./v4_add_municipality";
4445
import { migrate_v4_to_v5 } from "./v5_add_liquor_license";
4546
import { migrate_v5_to_v6 } from "./v6_add_home_based_business";
@@ -97,6 +98,7 @@ export const Migrations: MigrationFunction[] = [
9798
migrate_v43_to_v44,
9899
migrate_v44_to_v45,
99100
migrate_v45_to_v46,
101+
migrate_v46_to_v47,
100102
];
101103

102104
export const CURRENT_VERSION = Migrations.length;

0 commit comments

Comments
 (0)