Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fb0d30f
Update Access & Info branch name
mdesgov Jun 15, 2026
fa1ea1b
Merge pull request #518 from bcgov/release/FOIMOD-5099-to-main
mdesgov Jun 16, 2026
89534a7
Merge pull request #521 from bcgov/dev
Amritpal-Nijjar Jun 17, 2026
e57b3ba
fix: restrict personal request route and ministry selection
Amritpal-Nijjar Jul 6, 2026
a29b2d1
Merge pull request #529 from bcgov/cherry-pick/personal-single-route-…
Amritpal-Nijjar Jul 9, 2026
aaddd29
fix: update personal ministry selection copy
Amritpal-Nijjar Jul 9, 2026
ee0cc12
Merge pull request #532 from bcgov/fix/personal-ministry-copy
Aman-Hundal Jul 10, 2026
4118a5a
Merge pull request #523 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jun 30, 2026
96e50ea
Merge pull request #524 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jun 30, 2026
841997e
Merge pull request #525 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jun 30, 2026
d0f0c5e
Merge pull request #526 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 2, 2026
f687be6
Merge pull request #530 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 17, 2026
5df8b4f
Merge pull request #533 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 17, 2026
87c59e9
Merge pull request #534 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 17, 2026
b729b17
Merge pull request #535 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 17, 2026
08c9b47
Merge pull request #536 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 18, 2026
c6562cd
Merge pull request #537 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 18, 2026
c34218b
Merge pull request #538 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 18, 2026
4380112
Merge pull request #539 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 21, 2026
0870770
Merge pull request #540 from bcgov/dev-AH-FOIMOD-2470
Aman-Hundal Jul 22, 2026
e0759b5
removal of log from payment-completed component
Aman-Hundal Jul 23, 2026
0494fd0
Remove debug log from request-submit component
Aman-Hundal Jul 23, 2026
a940eac
Merge pull request #541 from bcgov/release/FOIMOD-release25.13-to-main
Aman-Hundal Jul 23, 2026
969b764
Merge pull request #542 from bcgov/dev-AH-FOIMOD-2470-qafixes
Aman-Hundal Jul 27, 2026
78fe7e8
Merge pull request #543 from bcgov/dev-AH-FOIMOD-2470-qafixes
Aman-Hundal Jul 27, 2026
f5a3409
Merge pull request #544 from bcgov/dev-AH-FOIMOD-2470-qafixes
Aman-Hundal Jul 28, 2026
7a672da
Merge pull request #545 from bcgov/release/FOIMOD-release25.13-to-mai…
Aman-Hundal Jul 28, 2026
c66a10c
adjusted redirect url for logout on duplicate request handling
Aman-Hundal Jul 28, 2026
5696b9e
Merge pull request #547 from bcgov/release/FOIMOD-release25.13-to-mai…
Aman-Hundal Jul 28, 2026
9f8d32f
Merge pull request #528 from bcgov/adamkroonbcgov-patch-3
adamkroonbcgov Jul 7, 2026
7e19494
Merge pull request #548 from bcgov/cgroup-update
ManishSihag Jul 29, 2026
72f1e3d
fix(5183): restore captcha audio playback
Amritpal-Nijjar Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,4 +19,4 @@ RUN chgrp -R 0 /app && \

USER node

ENTRYPOINT ["./container-entrypoint.sh"]
ENTRYPOINT ["./container-entrypoint.sh"]
164 changes: 67 additions & 97 deletions api/apiCaptcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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];
Expand All @@ -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');
}
}
Expand Down
39 changes: 28 additions & 11 deletions api/apiCustomFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`;

Expand Down Expand Up @@ -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) {
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ <h2>What ministry or agency has the records you are looking for?</h2>
<h6 *ngIf="requiresPayment" class="warning">
*A non-refundable application fee of $10 is required for all General FOI requests, for every public body selected.
</h6>
<app-alert-info *ngIf="isPersonalRequest">
<p>
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.
</p>
</app-alert-info>


<div *ngIf="ministries$ | async as minis">
<div class="form-group">
Expand Down
Loading