diff --git a/api/Dockerfile b/api/Dockerfile index 7d855f7a..c0ca06c4 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,5 +1,5 @@ # Use the official Node.js image from the Docker Hub -FROM node:20-alpine AS build +FROM node:22-alpine AS build # Set the working directory inside the container WORKDIR /app @@ -19,4 +19,4 @@ RUN chgrp -R 0 /app && \ USER node -ENTRYPOINT ["./container-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["./container-entrypoint.sh"] diff --git a/api/apiCaptcha.js b/api/apiCaptcha.js index e11997dc..b0b390ba 100644 --- a/api/apiCaptcha.js +++ b/api/apiCaptcha.js @@ -3,11 +3,8 @@ var svgCaptcha = require('svg-captcha'); var jwt = require('jsonwebtoken'); -var wav = require('wav'); var text2wav = require('text2wav'); var arrayBufferToBuffer = require('arraybuffer-to-buffer'); -var streamifier = require('streamifier'); -var lame = require('lamejs'); function captchaInit(options) { @@ -33,7 +30,7 @@ function captchaInit(options) { return decrypted; } - function createCaptcha(nonce){ + function createCaptcha(nonce) { var captcha = svgCaptcha.create({ size: 6, // size of random string ignoreChars: '0Oo1iIl', // filter out some characters like 0o1i @@ -92,105 +89,78 @@ function captchaInit(options) { } function getAudio(body, req) { - return new Promise(function(resolve, reject) { - try { - // pull out encrypted answer - var validation = body.validation; - - // decrypt payload to get captcha text - var decryptedBody = decrypt(validation); - - // Insert leading text and commas to slow down reader - var captchaCharArray = decryptedBody.answer.toString().split(''); - let language = 'en'; - if (body.translation) { - if (typeof body.translation == 'string') { - if (voicePromptLanguageMap.hasOwnProperty(body.translation)) { - language = body.translation; + return new Promise(function (resolve, reject) { + try { + // pull out encrypted answer + var validation = body.validation; + + // decrypt payload to get captcha text + var decryptedBody = decrypt(validation); + + // Insert leading text and commas to slow down reader + var captchaCharArray = decryptedBody.answer.toString().split(''); + let language = 'en'; + if (body.translation) { + if (typeof body.translation == 'string') { + if (voicePromptLanguageMap.hasOwnProperty(body.translation)) { + language = body.translation; + } + } else if ( + body.translation === true && + req && + req.headers['accept-language'] + ) { + let lang = req.headers['accept-language'] + .split(',') + .map(e => e.split(';')[0].split('-')[0]) + .find(e => voicePromptLanguageMap.hasOwnProperty(e)); + if (lang) { + language = lang; + } } - } else if ( - body.translation === true && - req && - req.headers['accept-language'] + } + var spokenCatpcha = voicePromptLanguageMap[language] + ': '; + for (var i = 0; i < captchaCharArray.length; i++) { + spokenCatpcha += captchaCharArray[i] + ', '; + } + getMp3DataUriFromText(spokenCatpcha, language).then(function ( + audioDataUri ) { - let lang = req.headers['accept-language'] - .split(',') - .map(e => e.split(';')[0].split('-')[0]) - .find(e => voicePromptLanguageMap.hasOwnProperty(e)); - if (lang) { - language = lang; + if (!audioDataUri) { + resolve({ + error: 'unknown' + }); + return; } - } - } - var spokenCatpcha = voicePromptLanguageMap[language] + ': '; - for (var i = 0; i < captchaCharArray.length; i++) { - spokenCatpcha += captchaCharArray[i] + ', '; - } - getMp3DataUriFromText(spokenCatpcha, language).then(function( - audioDataUri - ) { - // Now pass back the full payload , + + // Now pass back the full payload. + resolve({ + audio: audioDataUri + }); + }); + } catch (e) { + console.log('Error getting audio:', e); resolve({ - audio: audioDataUri + error: 'unknown' }); - }); - } catch (e) { - console.log('Error getting audio:', e); - resolve({ - error: 'unknown' - }); - } - }); + } + }); } - //////////////////////////////////////////////////////// /* * Audio routines */ //////////////////////////////////////////////////////// function getMp3DataUriFromText(text, language = 'en') { - return new Promise(async function(resolve) { - // init wave reader, used to convert WAV to PCM - var reader = new wav.Reader(); - - // we have to wait for the "format" event before we can start encoding - reader.on('format', function(format) { - // init encoder - var encoder = new lame.Mp3Encoder(format); - - // Pipe Wav reader to the encoder and capture the output stream - // As the stream is encoded, convert the mp3 array buffer chunks into base64 string with mime type - var dataUri = 'data:audio/mp3;base64,'; - encoder.on('data', function(arrayBuffer) { - if (!dataUri) { - return; - } - dataUri += arrayBuffer.toString('base64'); - // by observation encoder hung before finish due to event loop being empty - // setTimeout injects an event to mitigate the issue - setTimeout(() => {}, 0); - }); - - // When encoding is complete, callback with data uri - encoder.on('finish', function() { - resolve(dataUri); - dataUri = undefined; - }); - reader.pipe(encoder); - }); - - // Generate audio, Base64 encoded WAV in DataUri format including mime type header - text2wav(text, { voice: language }).then(function(audioArrayBuffer) { - // convert to buffer + return text2wav(text, { voice: language }) + .then(function (audioArrayBuffer) { var audioBuffer = arrayBufferToBuffer(audioArrayBuffer); - - // Convert ArrayBuffer to Streamable type for input to the encoder - var audioStream = streamifier.createReadStream(audioBuffer); - - // once all events setup we can the pipeline - audioStream.pipe(reader); + return 'data:audio/wav;base64,' + audioBuffer.toString('base64'); + }) + .catch(function (error) { + console.log('Error generating audio CAPTCHA:', error); + return null; }); - }); } function verifyJWTResponse(token, nonce) { @@ -208,29 +178,29 @@ function captchaInit(options) { } return { - getCaptcha: function(req, res, next) { + getCaptcha: function (req, res, next) { var fullReponse = createCaptcha(req.body.nonce); res.send(fullReponse); next(); }, - verifyCaptcha: function(req, res, next) { + verifyCaptcha: function (req, res, next) { var ret = verifyCaptchaInternal(req.body); res.send(ret); next(); }, - getCaptchaAudio: function(req, res, next) { - getAudio(req.body, req).then(function(ret) { + getCaptchaAudio: function (req, res, next) { + getAudio(req.body, req).then(function (ret) { res.send(ret); next(); }); }, - verifyJWTResponseMiddleware: function(req, res, next) { + verifyJWTResponseMiddleware: function (req, res, next) { // if there is a captcha header , it shud be validated by captcha verifier - if(req.isAuthorised && req.userDetails && !req.headers[CAPTCHA_NONCE_HEADER]) { + if (req.isAuthorised && req.userDetails && !req.headers[CAPTCHA_NONCE_HEADER]) { console.log('Request is already validated'); return next(); } - + var token = req.headers[CAPTCHA_TOKEN_HEADER.toLowerCase()] || ''; token = token.replace('Bearer ', ''); var nonce = req.headers[CAPTCHA_NONCE_HEADER]; @@ -239,7 +209,7 @@ function captchaInit(options) { console.log('Captcha validated in apiCaptcha'); next(); } else { - res. send(401, 'Not Authorized'); + res.send(401, 'Not Authorized'); next('Invalid Captcha Token'); } } diff --git a/api/apiCustomFunctions.js b/api/apiCustomFunctions.js index 726b5534..f9f910af 100644 --- a/api/apiCustomFunctions.js +++ b/api/apiCustomFunctions.js @@ -16,6 +16,12 @@ const MAX_ATTACH_MB = 5; const maxAttachBytes = MAX_ATTACH_MB * 1024 * 1024; const submitFoiRequest = async (server, req, res, next) => { + console.log( + '[API]', + new Date().toISOString(), + 'submitFoiRequest API called' + ); + console.trace('submitFoiRequest API'); const apiUrl = `${foiRequestAPIBackend}/foirawrequests`; @@ -48,9 +54,9 @@ const submitFoiRequest = async (server, req, res, next) => { console.log("calling RAW FOI Request"); const response = await requestAPI.invokeRequestAPI(JSON.stringify(data.params), apiUrl); - console.log(`API response = ${response.status}`); + console.log(`API response = DATA: ${JSON.stringify(response.data)}, STATUS: ${response.status}`); - if(response.status === 200 && response.data.status ) { + if(response.status === 200 && response.data.status) { console.log(`response id: ${response.data.id}`); // if request needs payment, return earlier to prevent sending email as it will be sent after payment. if(needsPayment) { @@ -69,24 +75,35 @@ const submitFoiRequest = async (server, req, res, next) => { pendingPayment: false }); - } - else { + } else { req.log.info('Failed:', response); const unavailable = new restifyErrors.ServiceUnavailableError('Service is unavailable.'); return next(unavailable); } } - catch(error){ - console.log(`${error}`); - console.log("FOI API STATUS:", error.response.status); - console.log("FOI API DATA:", error.response.data) - req.log.info('Failed:', error); - const unavailable = new restifyErrors.ServiceUnavailableError(error.message || 'Service is unavailable.'); - return next(unavailable); + catch(error) { + console.log(`${error}`); + console.log("FOI API STATUS:", error.response.status); + console.log("FOI API DATA:", error.response.data); + req.log.info('Failed:', error); + let unavailable = ""; + if (error.response.status === 409) { + // Handle duplicate request + unavailable = new restifyErrors.ConflictError(error.response.data.message); + } else { + unavailable = new restifyErrors.ServiceUnavailableError(error.message || 'Service is unavailable.'); + } + return next(unavailable); } } const submitFoiRequestEmail = async (server, req, res, next) => { + console.log( + '[API]', + new Date().toISOString(), + 'submitFoiRequestEmail API called' + ); + console.trace('submitFoiRequestEmail API'); req.params.requestData = JSON.parse(req.params.requestData); diff --git a/web/src/app/route-components/ministry-confirmation/ministry-confirmation.component.html b/web/src/app/route-components/ministry-confirmation/ministry-confirmation.component.html index 6a4149da..98be7e82 100644 --- a/web/src/app/route-components/ministry-confirmation/ministry-confirmation.component.html +++ b/web/src/app/route-components/ministry-confirmation/ministry-confirmation.component.html @@ -4,6 +4,13 @@

What ministry or agency has the records you are looking for?

*A non-refundable application fee of $10 is required for all General FOI requests, for every public body selected.
+ +

+ Personal FOI requests can only be submitted to one ministry at a time. If you need to make a request to multiple + ministries, please submit a separate request for each ministry. +

+
+
diff --git a/web/src/app/route-components/ministry-confirmation/ministry-confirmation.component.ts b/web/src/app/route-components/ministry-confirmation/ministry-confirmation.component.ts index 9266c4e9..47f7fbf6 100644 --- a/web/src/app/route-components/ministry-confirmation/ministry-confirmation.component.ts +++ b/web/src/app/route-components/ministry-confirmation/ministry-confirmation.component.ts @@ -27,61 +27,108 @@ export class MinistryConfirmationComponent implements OnInit { isENVministry: boolean = false; constructor(private fb: FormBuilder, private dataService: DataService, private route: Router) { } - mcfdOnlyTopicValues: string[] = [ - "adoption", - "childprotectionchild", - "childprotectionparent", - "fosterparent", - "youthincarechild", - "youthincareparent", - ]; - - isMcfdOnlyRequest: boolean = false; - readonly mcfdMinistryCode: string = "MCF"; - - private hasMcfdOnlyTopicSelected(): boolean { - const currentUrl = this.route.url || ""; + readonly personalTopicMinistryCodeMap: { [key: string]: string } = { + publicServiceEmployment: "PSA", + correctionalFacility: "PSSG", + incomeAssistance: "MSD", + adoption: "MCF", + childprotectionchild: "MCF", + childprotectionparent: "MCF", + fosterparent: "MCF", + youthincarechild: "MCF", + youthincareparent: "MCF", + }; + + readonly otherPersonalTopicValue: string = "anotherTopic"; + + isPersonalRequest: boolean = false; + isLockedPersonalMinistryRequest: boolean = false; + isOtherPersonalRequest: boolean = false; + lockedPersonalMinistryCode: string = null; + + private getRequestType(): string { + const requestType = this.foiRequest?.requestData?.requestType; + return requestType?.requestType || requestType; + } + + private getCurrentRequestTopicValue(): string { const currentRequestTopic = this.foiRequest?.requestData?.requestTopic?.value; - const matchesTopicSpecificRoute = this.mcfdOnlyTopicValues.some((topicValue) => - currentUrl.includes(`/${topicValue}/ministry-confirmation`) - ); + if (currentRequestTopic) { + return currentRequestTopic; + } - const matchesCurrentRequestTopic = - !!currentRequestTopic && this.mcfdOnlyTopicValues.includes(currentRequestTopic); + const currentUrl = this.route.url || ""; + const personalRouteMatch = currentUrl.match(/\/personal\/([^\/]+)\/ministry-confirmation/); - return matchesTopicSpecificRoute || matchesCurrentRequestTopic; + return personalRouteMatch ? personalRouteMatch[1] : null; } + private applyPersonalMinistryRules(ministries: any[]): any[] { + if (this.isLockedPersonalMinistryRequest) { + ministries.forEach((m) => { + const isLockedMinistry = m.code === this.lockedPersonalMinistryCode; + m.selected = isLockedMinistry; + m.defaulted = isLockedMinistry; + m.disabled = true; + }); - private isMcfdMinistry(m: any): boolean { - return m?.code === this.mcfdMinistryCode; + return ministries; + } + + if (this.isOtherPersonalRequest) { + return this.applyOtherPersonalMinistryRules(ministries); + } + + ministries.forEach((m) => { + m.disabled = false; + }); + + return ministries; } - private applyMcfdOnlyMinistryRules(ministries: any[]): any[] { - if (!this.isMcfdOnlyRequest) { + private applyOtherPersonalMinistryRules(ministries: any[]): any[] { + const selectedMinistries = ministries.filter((m) => m.selected); + + if (selectedMinistries.length > 1) { + const selectedMinistry = selectedMinistries[0]; + ministries.forEach((m) => { - m.disabled = false; + m.selected = m.code === selectedMinistry.code; }); - return ministries; } + const hasSelectedMinistry = ministries.some((m) => m.selected); + ministries.forEach((m) => { - const isMcfd = this.isMcfdMinistry(m); - m.selected = isMcfd; - m.defaulted = isMcfd; - m.disabled = !isMcfd; + m.defaulted = false; + m.disabled = hasSelectedMinistry && !m.selected; }); return ministries; } + private refreshSelectedMinistryFlags(ministries: any[]): void { + this.isEAOministry = !!ministries.find((m) => m.code === "EAO" && m.selected); + this.isENVministry = !!ministries.find((m) => m.code === "ENV" && m.selected); + this.isforestministry = !!ministries.find((m) => m.code === "FOR" && m.selected); + } + + + ngOnInit() { this.foiRequest = this.dataService.getCurrentState(this.targetKey); this.defaultMinistry = this.foiRequest.requestData[this.targetKey].defaultMinistry; let selectedMinistry = this.foiRequest.requestData[this.targetKey].selectedMinistry; this.requiresPayment = this.foiRequest.requestData.requestType.requestType === "general"; - this.isMcfdOnlyRequest = this.hasMcfdOnlyTopicSelected(); + const currentRequestTopicValue = this.getCurrentRequestTopicValue(); + + this.isPersonalRequest = this.getRequestType() === "personal" || this.route.url.includes("/personal/"); + this.lockedPersonalMinistryCode = this.isPersonalRequest + ? this.personalTopicMinistryCodeMap[currentRequestTopicValue] + : null; + this.isLockedPersonalMinistryRequest = !!this.lockedPersonalMinistryCode; + this.isOtherPersonalRequest = this.isPersonalRequest && currentRequestTopicValue === this.otherPersonalTopicValue; // Fetch Ministries from the data service. this.ministries$ = this.dataService.getMinistries().pipe( @@ -106,7 +153,11 @@ export class MinistryConfirmationComponent implements OnInit { this.isforestministry = false; } }); - return this.applyMcfdOnlyMinistryRules(ministries); + + ministries = this.applyPersonalMinistryRules(ministries); + this.refreshSelectedMinistryFlags(ministries); + + return ministries; }), map((ministries) => { this.base.continueDisabled = !ministries.find((m) => m.selected); @@ -126,25 +177,17 @@ export class MinistryConfirmationComponent implements OnInit { } selectMinistry(m: any) { - if (this.isMcfdOnlyRequest) { + if (this.isLockedPersonalMinistryRequest) { return; } + m.selected = !m.selected; - if (m.code === "EAO" && m.selected === true) { - this.isEAOministry = true; - } else if (m.code === "EAO" && m.selected === false) { - this.isEAOministry = false; - } - if (m.code === "ENV" && m.selected === true) { - this.isENVministry = true; - } else if (m.code === "ENV" && m.selected === false) { - this.isENVministry = false; - } - if (m.code === "FOR" && m.selected === true) { - this.isforestministry = true; - } else if (m.code === "FOR" && m.selected === false) { - this.isforestministry = false; + + if (this.isOtherPersonalRequest) { + this.applyOtherPersonalMinistryRules(this.ministries); } + + this.refreshSelectedMinistryFlags(this.ministries); this.setContinueDisabled(); } diff --git a/web/src/app/route-components/payment-complete/payment-complete.component.ts b/web/src/app/route-components/payment-complete/payment-complete.component.ts index 5d019ecb..07a014c7 100644 --- a/web/src/app/route-components/payment-complete/payment-complete.component.ts +++ b/web/src/app/route-components/payment-complete/payment-complete.component.ts @@ -110,8 +110,15 @@ export class PaymentCompleteComponent implements OnInit { } submitEmail() { + console.log( + '[PAYMENT-COMPLETED COMPONENT]', + new Date().toISOString(), + 'Payment completed request submitted' + ); + console.trace('PAYMENT EMAIL COMPONENT'); this.dataService.submitRequest(this.authToken, null, this.foiRequest, true).subscribe( - (result) => { + (response) => { + const result = response.body; if (!result.EmailSuccess) { alert("Temporarily unable to complete your request. Please contact us to complete your request."); } diff --git a/web/src/app/route-components/request-topic/request-topic.component.html b/web/src/app/route-components/request-topic/request-topic.component.html index 2836c48b..a06fe1b5 100644 --- a/web/src/app/route-components/request-topic/request-topic.component.html +++ b/web/src/app/route-components/request-topic/request-topic.component.html @@ -1,21 +1,22 @@

What kinds of records are you looking for?

-
+
-

Select the corresponding categories of records you are requesting (the more you select the larger your resulting request will be)

+

Select the corresponding categories of records you are requesting (the more you select the larger your + resulting request will be)

Are you looking for these types of records?

-
+
Police Records from a Police visit?
@@ -42,8 +43,10 @@
Your own health records?

-
Court Records and Transcripts?
- We do not hold these records. You can get copies of your court records at https://justice.gov.bc.ca/cso/index.do +
Court Records and Transcripts?
+ We do not hold these records. You can get copies of your court records at https://justice.gov.bc.ca/cso/index.do
diff --git a/web/src/app/route-components/request-topic/request-topic.component.scss b/web/src/app/route-components/request-topic/request-topic.component.scss index 342a5485..b2849b7b 100644 --- a/web/src/app/route-components/request-topic/request-topic.component.scss +++ b/web/src/app/route-components/request-topic/request-topic.component.scss @@ -18,4 +18,9 @@ .optiontext{ display: inline; padding-left: 5px;; +} + +.disabled-topic-option { + opacity: 0.5; + cursor: not-allowed; } \ No newline at end of file diff --git a/web/src/app/route-components/request-topic/request-topic.component.ts b/web/src/app/route-components/request-topic/request-topic.component.ts index 7604e221..d40c8c9a 100644 --- a/web/src/app/route-components/request-topic/request-topic.component.ts +++ b/web/src/app/route-components/request-topic/request-topic.component.ts @@ -29,6 +29,50 @@ export class RequestTopicComponent implements OnInit { "youthincarechild", "youthincareparent", ]; + + childrenAndFamilyTopicValues: string[] = [ + "adoption", + "childprotectionchild", + "childprotectionparent", + "fosterparent", + "youthincarechild", + "youthincareparent", + ]; + + singleRouteTopicValues: string[] = [ + "publicServiceEmployment", + "correctionalFacility", + "incomeAssistance", + "anotherTopic", + ]; + + private isChildrenAndFamilyTopic(topic: any): boolean { + return this.childrenAndFamilyTopicValues.includes(topic?.value); + } + + private getSelectedTopics(): any[] { + return this.topics ? this.topics.filter((topic) => topic.selected) : []; + } + + isTopicDisabled(topic: any): boolean { + const selectedTopics = this.getSelectedTopics(); + + if (selectedTopics.length === 0) { + return false; + } + + const selectedChildrenAndFamily = selectedTopics.some((selectedTopic) => + this.isChildrenAndFamilyTopic(selectedTopic) + ); + + if (selectedChildrenAndFamily) { + return !this.isChildrenAndFamilyTopic(topic); + } + + const selectedTopic = selectedTopics[0]; + return topic.value !== selectedTopic.value; + } + constructor( private fb: FormBuilder, private dataService: DataService, @@ -98,6 +142,10 @@ export class RequestTopicComponent implements OnInit { } selecttopic(item: any, _checked) { + if (this.isTopicDisabled(item) && !item.selected) { + return; + } + item.selected = !item.selected; let current = this.foiRequest.requestData.selectedtopics.find((st) => st.value === item.value); const itemindex: number = this.foiRequest.requestData.selectedtopics.indexOf(current); @@ -158,4 +206,4 @@ export class RequestTopicComponent implements OnInit { this.foiRequest.requestData.selectedtopics = this.yourselftopics; this.base.goFoiBack(); } -} +} \ No newline at end of file diff --git a/web/src/app/route-components/review-submit-complete/review-submit-complete.component.ts b/web/src/app/route-components/review-submit-complete/review-submit-complete.component.ts index 3064e078..b27c946a 100644 --- a/web/src/app/route-components/review-submit-complete/review-submit-complete.component.ts +++ b/web/src/app/route-components/review-submit-complete/review-submit-complete.component.ts @@ -26,18 +26,13 @@ export class ReviewSubmitCompleteComponent implements OnInit { this.foiRequest.requestData.requestType.youthincareparent = []; // Clear the current state! - const blankState: FoiRequest = { - requestData: {}, - }; - this.dataService.setCurrentState(blankState); - this.dataService.removeChildFileAttachment(); - this.dataService.removePersonFileAttachment(); - this.dataService.removeAdoptionFileAttachment(); + this.dataService.clearState(); this.dataService.removeAuthToken(); } submitAnotherRequest() { - this.router.navigate(["getting-started2"]); + // this.router.navigate(["getting-started2"]); + window.location.href = '/'; return false; } } diff --git a/web/src/app/route-components/review-submit/review-submit.component.html b/web/src/app/route-components/review-submit/review-submit.component.html index 789a8ada..d2a96e2b 100644 --- a/web/src/app/route-components/review-submit/review-submit.component.html +++ b/web/src/app/route-components/review-submit/review-submit.component.html @@ -26,7 +26,7 @@

Review your request

diff --git a/web/src/app/route-components/review-submit/review-submit.component.ts b/web/src/app/route-components/review-submit/review-submit.component.ts index 415e0bd4..9b9a0687 100644 --- a/web/src/app/route-components/review-submit/review-submit.component.ts +++ b/web/src/app/route-components/review-submit/review-submit.component.ts @@ -61,32 +61,57 @@ export class ReviewSubmitComponent implements OnInit { } doContinue() { + console.log( + '[REVIEW-SUBMIT COMPONENT]', + new Date().toISOString(), + 'Submitting request' + ); + if (this.isBusy) { + console.trace('REQUEST-SUBMIT COMPONENT BLOCKED'); + return; + } + console.trace('REQUEST-SUBMIT COMPONENT ALLOWED'); + this.isBusy = true; this.dataService.submitRequest(this.authToken, this.captchaNonce, this.foiRequest).subscribe( - (result) => { + (response) => { + const result = response.body; + this.foiRequest.requestData.requestId = result.id; this.dataService.setCurrentState(this.foiRequest); this.dataService.saveAuthToken(this.authToken); + // this.isBusy = false; - this.isBusy = false; // If the user is authenticated, logout the user if (this.keycloakService.isAuthenticated()) { - this.keycloakService.logout(); + this.dataService.clearState() + this.keycloakService.logout(window.location.origin + '/personal/submit-complete'); } else { this.base.goFoiForward(); } }, (error) => { + // Handle duplicate request + if (error.status === 409) { + alert( + "Duplicate request identified. Request was not processed.\n\n" + + "This request has already been submitted. To prevent duplicates, you cannot resubmit the same request for 30 minutes.\n\n" + + "Go back to the homepage and start a new request to try again." + ); + this.dataService.clearState(); + if (this.keycloakService.isAuthenticated()) { + this.keycloakService.logout(window.location.origin + '/'); + return; + } else { + this.base.goHome(); + return; + } + } + this.isBusy = false; - alert("Temporarily unable to submit your request. Please try again in a few minutes."); this.captchaComponent.forceRefresh(); this.captchaComplete = false; - // if (this.keycloakService.isAuthenticated()) { - // this.keycloakService.logout(); - // } else { - // this.base.goFoiForward(); - // } } ); } diff --git a/web/src/app/services/data.service.ts b/web/src/app/services/data.service.ts index 19748a8e..ffab93e9 100644 --- a/web/src/app/services/data.service.ts +++ b/web/src/app/services/data.service.ts @@ -143,6 +143,14 @@ export class DataService { return foi; } + clearState(newData = {}) { + const blankState: FoiRequest = { requestData: newData }; + this.setCurrentState(blankState); + this.removeChildFileAttachment(); + this.removePersonFileAttachment(); + this.removeAdoptionFileAttachment(); + } + setChildFileAttachment(f: File): Observable { return new Observable((observer) => { const reader: FileReader = new FileReader(); @@ -293,6 +301,12 @@ export class DataService { * @param foiRequest - A structure containing the complete request */ submitRequest(authToken: string, nonce: string, foiRequest: FoiRequest, sendEmailOnly?: boolean): Observable { + console.log( + '[DATA SERVICE]', + new Date().toISOString(), + 'submitRequest called' + ); + console.trace('submitRequest FUNCTION'); this.apiClient.setHeader("Authorization", "Bearer " + authToken); if (nonce) { diff --git a/web/src/app/services/keycloak.service.ts b/web/src/app/services/keycloak.service.ts index 274c4929..dde1b0be 100644 --- a/web/src/app/services/keycloak.service.ts +++ b/web/src/app/services/keycloak.service.ts @@ -121,24 +121,25 @@ export class KeycloakService { return token !== undefined && token.sub !== undefined; } - logout() { + logout(redirectUrl: string) { if (this.isAuthenticated()) { if (!this.keycloakAuth) { this.keycloakAuth = new Keycloak(this.keycloakConfig); this.keycloakAuth.init({token: sessionStorage.getItem('KC_TOKEN'), onLoad: 'check-sso'}) .then(authenticated => { if (authenticated) { - this.logoutUser(); + this.logoutUser(redirectUrl); } }); } else { - this.logoutUser(); + this.logoutUser(redirectUrl); } } } - logoutUser() { - const redirectUrl = window.location.origin + '/general/submit-complete'; + logoutUser(redirectUrl: string) { + sessionStorage.removeItem('KC_TOKEN'); + sessionStorage.removeItem('KC_REFRESH'); this.keycloakAuth.logout({ redirectUri: redirectUrl }); } } diff --git a/web/src/app/transom-api-client.service.ts b/web/src/app/transom-api-client.service.ts index 97d8ea95..1e7af9c9 100644 --- a/web/src/app/transom-api-client.service.ts +++ b/web/src/app/transom-api-client.service.ts @@ -94,6 +94,12 @@ export class TransomApiClientService { * @param body The body to post to the request. */ postFoiRequest(foiRequest: FoiRequest, sendEmailOnly?: boolean): Observable { + console.log( + '[TRANSOM LAYER]', + new Date().toISOString(), + 'postFoiRequest called' + ); + console.trace('postFoiRequest FUNCTION'); const functionName = sendEmailOnly ? "submitFoiRequestEmail" : "submitFoiRequest"; const url = this.baseUrl + `/fx/${functionName}`; @@ -105,7 +111,8 @@ export class TransomApiClientService { } const obs = this.http.post(url, body, { - headers: this.headers + headers: this.headers, + observe: 'response' }); return this.handleResponse(obs); } diff --git a/web/src/app/utils-components/base/base.component.ts b/web/src/app/utils-components/base/base.component.ts index 9bd8e130..03625be4 100644 --- a/web/src/app/utils-components/base/base.component.ts +++ b/web/src/app/utils-components/base/base.component.ts @@ -74,6 +74,10 @@ export class BaseComponent implements OnInit { // } } + goHome() { + this.router.navigate(['']) + } + goSkipForward() { this.foiRouter.progress({ direction: 2 }); } diff --git a/web/src/app/utils-components/captcha/captcha.component.ts b/web/src/app/utils-components/captcha/captcha.component.ts index 02380516..6503e817 100644 --- a/web/src/app/utils-components/captcha/captcha.component.ts +++ b/web/src/app/utils-components/captcha/captcha.component.ts @@ -21,7 +21,6 @@ import { CaptchaDataService } from "src/app/services/captcha-data.service"; export class CaptchaComponent implements AfterViewInit, OnInit { @ViewChild("image", { static: true }) imageContainer: ElementRef; @ViewChild("answer", { static: true }) userAnswerRef: ElementRef; - @ViewChild("audioElement", { static: true }) audioElement: ElementRef; @Input("apiBaseUrl") apiBaseUrl: string; @Input("nonce") nonce: string; @Output() onValidToken = new EventEmitter(); @@ -51,7 +50,7 @@ export class CaptchaComponent implements AfterViewInit, OnInit { public fetchingAudioInProgress = false; - constructor(private dataService: CaptchaDataService, private cd: ChangeDetectorRef, private ngZone: NgZone) {} + constructor(private dataService: CaptchaDataService, private cd: ChangeDetectorRef, private ngZone: NgZone) { } ngOnInit() { this.forceRefresh.bind(this); @@ -161,12 +160,19 @@ export class CaptchaComponent implements AfterViewInit, OnInit { public playAudio(playImmediately: boolean = true) { if (this.audio && this.audio.length > 0) { - this.audioElement.nativeElement.play(); + this.playAudioDataUri(); } else { this.fetchAudio(playImmediately); } } + private playAudioDataUri() { + const audio = new Audio(this.audio); + audio.play().catch((error) => { + console.log("Error playing audio CAPTCHA: %o", error); + }); + } + private fetchAudio(playImmediately: boolean = false) { if (!this.fetchingAudioInProgress) { this.fetchingAudioInProgress = true; @@ -175,8 +181,9 @@ export class CaptchaComponent implements AfterViewInit, OnInit { this.fetchingAudioInProgress = false; this.audio = response.body.audio; this.cd.detectChanges(); - if (playImmediately) { - this.audioElement.nativeElement.play(); + + if (playImmediately && this.audio && this.audio.length > 0) { + this.playAudioDataUri(); } }, (error) => {