diff --git a/src/config/index.ts b/src/config/index.ts index b2341f273..5404edd27 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -111,7 +111,7 @@ export const config = convict({ */ sessionTimeout: { format: Number, - default: oneHour * 24, // 1 day + default: oneHour * 168, // 7 days env: 'SESSION_TIMEOUT' }, confirmationSessionTimeout: { diff --git a/src/server/plugins/engine/pageControllers/QuestionPageController.ts b/src/server/plugins/engine/pageControllers/QuestionPageController.ts index 7b6646b7a..7614afbd5 100644 --- a/src/server/plugins/engine/pageControllers/QuestionPageController.ts +++ b/src/server/plugins/engine/pageControllers/QuestionPageController.ts @@ -21,7 +21,11 @@ import { } from '~/src/server/plugins/engine/helpers.js' import { type FormModel } from '~/src/server/plugins/engine/models/index.js' import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js' +import { extendFileRetention } from '~/src/server/plugins/engine/services/formSubmissionService.js' +import { type FilePersistData } from '~/src/server/plugins/engine/services/types.js' import { + FileStatus, + type FileState, type FormContext, type FormContextRequest, type FormPageViewModel, @@ -321,7 +325,45 @@ export class QuestionPageController extends PageController { } const { cacheService } = request.services([]) - return cacheService.setState(request, updated) + const newState = await cacheService.setState(request, updated) + + await this.updateFileRetention(newState) + + return newState + } + + /** + * Checks the updated session state for any file upload information and, + * if found, calls the file retention service to extend the files' S3 expiration. + * @param state - The current session state. + */ + private async updateFileRetention(state: FormSubmissionState): Promise { + if (!state.upload) { + return + } + + const files: FilePersistData[] = [] + + const uploads = state.upload ?? {} + Object.keys(uploads).forEach((uploadKey) => { + const uploadData = uploads[uploadKey] + if (Array.isArray(uploadData.files) && uploadData.files.length > 0) { + uploadData.files.forEach((file: FileState) => { + // Only extend retention if the file upload is complete otherwise we'll run into a race condition. + if (file.status.form.file.fileStatus === FileStatus.complete) { + files.push({ + fileId: file.status.form.file.fileId + }) + } + }) + } + }) + + if (files.length > 0) { + const retrievalKey = + this.model.def.outputEmail ?? 'defraforms@defra.gov.uk' + await extendFileRetention(files, retrievalKey) + } } makeGetRouteHandler() { diff --git a/src/server/plugins/engine/services/formSubmissionService.js b/src/server/plugins/engine/services/formSubmissionService.js index 2da62b399..cb73d0afa 100644 --- a/src/server/plugins/engine/services/formSubmissionService.js +++ b/src/server/plugins/engine/services/formSubmissionService.js @@ -1,19 +1,19 @@ import { config } from '~/src/config/index.js' import { postJson } from '~/src/server/services/httpService.js' - const submissionUrl = config.get('submissionUrl') /** * Persist files by extending the time-to-live to 30 days - * @param {{fileId: string, initiatedRetrievalKey: string}[]} files - batch of files to persist - * @param {string} persistedRetrievalKey - final retrieval key when submitting + * @param {FilePersistData[]} files Files to persist. + * @param {string} retrievalKey Final retrieval key when submitting (usually the output email). + * @returns {Promise} The result payload. */ -export async function persistFiles(files, persistedRetrievalKey) { +export async function persistFiles(files, retrievalKey) { const postJsonByType = /** @type {typeof postJson} */ (postJson) const payload = { - files, - persistedRetrievalKey + retrievalKey, + files } const result = await postJsonByType(`${submissionUrl}/files/persist`, { @@ -23,6 +23,21 @@ export async function persistFiles(files, persistedRetrievalKey) { return result } +/** + * Extend the retention time for uploaded files. + * @param {FilePersistData[]} files Array of file objects. + * @param {string} retrievalKey The key (usually the user's email) to be used for persistence. + * @returns {Promise} The result payload. + */ +export async function extendFileRetention(files, retrievalKey) { + const payload = { + retrievalKey, + files + } + const result = await postJson(`${submissionUrl}/file/extend`, { payload }) + return result +} + /** * Submit form * @param {SubmitPayload} data - submission data @@ -44,3 +59,7 @@ export async function submit(data) { /** * @import { SubmitPayload, SubmitResponsePayload } from '@defra/forms-model' */ + +/** + * @import { FilePersistData } from '~/src/server/plugins/engine/services/types.js' + */ diff --git a/src/server/plugins/engine/services/types.js b/src/server/plugins/engine/services/types.js new file mode 100644 index 000000000..e1908be14 --- /dev/null +++ b/src/server/plugins/engine/services/types.js @@ -0,0 +1,5 @@ +/** + * @typedef {object} FilePersistData + * @property {string} fileId The unique identifier for the file. + * @property {string} [initiatedRetrievalKey] The retrieval key that was assigned when the file was initiated. + */ diff --git a/src/server/plugins/session.ts b/src/server/plugins/session.ts index e94e42fe9..125be322c 100644 --- a/src/server/plugins/session.ts +++ b/src/server/plugins/session.ts @@ -22,7 +22,8 @@ export default { // storeBlank: false, cookieOptions: { password: config.get('sessionCookiePassword'), - isSecure: config.get('isProduction') + isSecure: config.get('isProduction'), + ttl: 7 * 24 * 60 * 60 * 1000 // 7 days } } } satisfies ServerRegisterPluginObject diff --git a/src/server/types.ts b/src/server/types.ts index e94b876a8..663389e88 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -7,6 +7,7 @@ import { import { type FormModel } from '~/src/server/plugins/engine/models/index.js' import { type DetailItem } from '~/src/server/plugins/engine/models/types.js' +import { type FilePersistData } from '~/src/server/plugins/engine/services/types.js' import { type FormRequestPayload, type FormStatus @@ -22,7 +23,7 @@ export interface FormsService { export interface FormSubmissionService { persistFiles: ( - files: { fileId: string; initiatedRetrievalKey: string }[], + files: FilePersistData[], persistedRetrievalKey: string ) => Promise submit: (data: SubmitPayload) => Promise diff --git a/src/server/views/help/cookies.html b/src/server/views/help/cookies.html index 3bad81816..7b93dbc62 100644 --- a/src/server/views/help/cookies.html +++ b/src/server/views/help/cookies.html @@ -30,7 +30,7 @@

Essential cookies

[ { text: "session" }, { text: "Remembers the information you enter" }, - { text: "When you close the browser, or after 24 hours" } + { text: "When you close the browser, or after 7 days" } ], [ { text: "crumb" },