Skip to content

Commit

Permalink
Merge pull request #585 from bcgov/ccfri-3756-afs-update
Browse files Browse the repository at this point in the history
CCFRI-3756 - finishing AFS pages
  • Loading branch information
vietle-cgi authored Nov 28, 2024
2 parents ff30faa + 844e64f commit 049fe5b
Show file tree
Hide file tree
Showing 21 changed files with 1,045 additions and 162 deletions.
162 changes: 90 additions & 72 deletions backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const organizationRouter = require('./routes/organization');
const publicRouter = require('./routes/public');
const configRouter = require('./routes/config');
const applicationRouter = require('./routes/application');
const documentRouter = require('./routes/document');
const fundingRouter = require('./routes/funding');
const messageRouter = require('./routes/message');
const licenseUploadRouter = require('./routes/licenseUpload');
Expand All @@ -44,42 +45,48 @@ app.set('trust proxy', 1);
app.use(cors());
app.use(helmet());
app.use(noCache());
app.use(promMid({
metricsPath: '/api/prometheus',
collectDefaultMetrics: true,
requestDurationBuckets: [0.1, 0.5, 1, 1.5]
}));
app.use(
promMid({
metricsPath: '/api/prometheus',
collectDefaultMetrics: true,
requestDurationBuckets: [0.1, 0.5, 1, 1.5],
}),
);
//tells the app to use json as means of transporting data
app.use(bodyParser.json({ limit: '50mb', extended: true }));
app.use(bodyParser.urlencoded({
extended: true,
limit: '50mb'
}));
app.use(
bodyParser.urlencoded({
extended: true,
limit: '50mb',
}),
);

const logStream = {
write: (message) => {
log.info(message);
}
},
};

const dbSession = getRedisDbSession();
const cookie = {
secure: true,
httpOnly: true,
maxAge: 1800000 //30 minutes in ms. this is same as session time. DO NOT MODIFY, IF MODIFIED, MAKE SURE SAME AS SESSION TIME OUT VALUE.
maxAge: 1800000, //30 minutes in ms. this is same as session time. DO NOT MODIFY, IF MODIFIED, MAKE SURE SAME AS SESSION TIME OUT VALUE.
};
if ('local' === config.get('environment')) {
cookie.secure = false;
}
//sets cookies for security purposes (prevent cookie access, allow secure connections only, etc)
app.use(session({
name: 'ccof_cookie',
secret: config.get('oidc:clientSecret'),
resave: false,
saveUninitialized: true,
cookie: cookie,
store: dbSession
}));
app.use(
session({
name: 'ccof_cookie',
secret: config.get('oidc:clientSecret'),
resave: false,
saveUninitialized: true,
cookie: cookie,
store: dbSession,
}),
);

app.use(require('./routes/health-check').router);
//initialize routing and session. Cookies are now only reachable via requests (not js)
Expand All @@ -101,29 +108,34 @@ function getRedisDbSession() {
}

function addLoginPassportUse(discovery, strategyName, callbackURI, kc_idp_hint, clientId, clientSecret) {
passport.use(strategyName, new OidcStrategy({
issuer: discovery.issuer,
authorizationURL: discovery.authorization_endpoint,
tokenURL: discovery.token_endpoint,
userInfoURL: discovery.userinfo_endpoint,
clientID: config.get(clientId),
clientSecret: config.get(clientSecret),
callbackURL: callbackURI,
scope: 'openid',
kc_idp_hint: kc_idp_hint
}, (_issuer, profile, _context, _idToken, accessToken, refreshToken, done) => {
if ((typeof (accessToken) === 'undefined') || (accessToken === null) ||
(typeof (refreshToken) === 'undefined') || (refreshToken === null)) {
return done('No access token', null);
}

//set access and refresh tokens
profile.jwtFrontend = auth.generateUiToken();
profile.jwt = accessToken;
profile._json = parseJwt(accessToken);
profile.refreshToken = refreshToken;
return done(null, profile);
}));
passport.use(
strategyName,
new OidcStrategy(
{
issuer: discovery.issuer,
authorizationURL: discovery.authorization_endpoint,
tokenURL: discovery.token_endpoint,
userInfoURL: discovery.userinfo_endpoint,
clientID: config.get(clientId),
clientSecret: config.get(clientSecret),
callbackURL: callbackURI,
scope: 'openid',
kc_idp_hint: kc_idp_hint,
},
(_issuer, profile, _context, _idToken, accessToken, refreshToken, done) => {
if (typeof accessToken === 'undefined' || accessToken === null || typeof refreshToken === 'undefined' || refreshToken === null) {
return done('No access token', null);
}

//set access and refresh tokens
profile.jwtFrontend = auth.generateUiToken();
profile.jwt = accessToken;
profile._json = parseJwt(accessToken);
profile.refreshToken = refreshToken;
return done(null, profile);
},
),
);
}

const parseJwt = (token) => {
Expand All @@ -134,44 +146,49 @@ const parseJwt = (token) => {
}
};
//initialize our authentication strategy
utils.getOidcDiscovery().then(discovery => {
utils.getOidcDiscovery().then((discovery) => {
//OIDC Strategy is used for authorization
addLoginPassportUse(discovery, 'oidcIdir', config.get('server:frontend') + '/api/auth/callback_idir', 'keycloak_bcdevexchange_idir', 'oidc:clientIdIDIR', 'oidc:clientSecretIDIR');
addLoginPassportUse(discovery, 'oidcBceid', config.get('server:frontend') + '/api/auth/callback', 'keycloak_bcdevexchange_bceid', 'oidc:clientId', 'oidc:clientSecret');

//JWT strategy is used for authorization keycloak_bcdevexchange_idir
passport.use('jwt', new JWTStrategy({
algorithms: ['RS256'],
// Keycloak 7.3.0 no longer automatically supplies matching client_id audience.
// If audience checking is needed, check the following SO to update Keycloak first.
// Ref: https://stackoverflow.com/a/53627747
audience: config.get('server:frontend'),
issuer: config.get('tokenGenerate:issuer'),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('tokenGenerate:publicKey'),
ignoreExpiration: true
}, (jwtPayload, done) => {
if ((typeof (jwtPayload) === 'undefined') || (jwtPayload === null)) {
return done('No JWT token', null);
}

done(null, {
email: jwtPayload.email,
familyName: jwtPayload.family_name,
givenName: jwtPayload.given_name,
jwt: jwtPayload,
name: jwtPayload.name,
user_guid: jwtPayload.user_guid,
realmRole: jwtPayload.realm_role
});
}));
passport.use(
'jwt',
new JWTStrategy(
{
algorithms: ['RS256'],
// Keycloak 7.3.0 no longer automatically supplies matching client_id audience.
// If audience checking is needed, check the following SO to update Keycloak first.
// Ref: https://stackoverflow.com/a/53627747
audience: config.get('server:frontend'),
issuer: config.get('tokenGenerate:issuer'),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('tokenGenerate:publicKey'),
ignoreExpiration: true,
},
(jwtPayload, done) => {
if (typeof jwtPayload === 'undefined' || jwtPayload === null) {
return done('No JWT token', null);
}

done(null, {
email: jwtPayload.email,
familyName: jwtPayload.family_name,
givenName: jwtPayload.given_name,
jwt: jwtPayload,
name: jwtPayload.name,
user_guid: jwtPayload.user_guid,
realmRole: jwtPayload.realm_role,
});
},
),
);
});
//functions for serializing/deserializing users
passport.serializeUser((user, next) => next(null, user));
passport.deserializeUser((obj, next) => next(null, obj));


app.use(morgan(config.get('server:morganFormat'), { 'stream': logStream }));
app.use(morgan(config.get('server:morganFormat'), { stream: logStream }));
//set up routing to auth and main API
app.use(/(\/api)?/, apiRouter);

Expand All @@ -180,8 +197,9 @@ apiRouter.use('/user', userRouter);
apiRouter.use('/facility', facilityRouter);
apiRouter.use('/organization', organizationRouter);
apiRouter.use('/public', publicRouter);
apiRouter.use('/config',configRouter);
apiRouter.use('/config', configRouter);
apiRouter.use('/application', applicationRouter);
apiRouter.use('/document', documentRouter);
apiRouter.use('/group/funding', fundingRouter);
apiRouter.use('/messages', messageRouter);
apiRouter.use('/licenseUpload', licenseUploadRouter);
Expand All @@ -194,7 +212,7 @@ app.use((err, _req, res, next) => {
//This is from the ResultValidation
if (err.errors && err.mapped) {
return res.status(400).json({
errors: err.mapped()
errors: err.mapped(),
});
}
log.error(err?.stack);
Expand Down
9 changes: 3 additions & 6 deletions backend/src/components/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,14 @@ async function renewCCOFApplication(req, res) {
}

async function patchCCFRIApplication(req, res) {
let payload = req.body;
payload = new MappableObjectForBack(payload, CCFRIFacilityMappings);
payload = payload.toJSON();

try {
await patchOperationWithObjectId('ccof_applicationccfris', req.params.ccfriId, payload);
const payload = new MappableObjectForBack(req.body, CCFRIFacilityMappings).toJSON();
const response = await patchOperationWithObjectId('ccof_applicationccfris', req.params.ccfriId, payload);
return res.status(HttpStatus.OK).json(response);
} catch (e) {
log.error(e);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
}
return res.status(HttpStatus.OK).json(payload);
}

async function deleteCCFRIApplication(req, res) {
Expand Down
73 changes: 73 additions & 0 deletions backend/src/components/document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
'use strict';
const { MappableObjectForFront, MappableObjectForBack } = require('../util/mapping/MappableObject');
const { ApplicationDocumentsMappings, DocumentsMappings } = require('../util/mapping/Mappings');
const { postApplicationDocument, getApplicationDocument, deleteDocument, patchOperationWithObjectId } = require('./utils');
const HttpStatus = require('http-status-codes');
const log = require('./logger');

const { getFileExtension, convertHeicDocumentToJpg } = require('../util/uploadFileUtils');

async function createApplicationDocuments(req, res) {
try {
const documents = req.body;
for (let document of documents) {
let payload = new MappableObjectForBack(document, ApplicationDocumentsMappings).toJSON();
payload.ccof_applicationid = document.ccof_applicationid;
payload.ccof_facility = document.ccof_facility;
if (getFileExtension(payload.filename) === 'heic') {
log.verbose(`createApplicationDocuments :: heic detected for file name ${payload?.filename} starting conversion`);
payload = await convertHeicDocumentToJpg(payload);
}
await postApplicationDocument(payload);
}
return res.sendStatus(HttpStatus.OK);
} catch (e) {
log.error(e);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
}
}

async function getApplicationDocuments(req, res) {
try {
const documents = [];
const response = await getApplicationDocument(req.params.applicationId);
response?.value?.forEach((document) => documents.push(new MappableObjectForFront(document, ApplicationDocumentsMappings).toJSON()));
return res.status(HttpStatus.OK).json(documents);
} catch (e) {
log.error(e);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
}
}

async function updateDocument(req, res) {
try {
const payload = new MappableObjectForBack(req.body, DocumentsMappings).toJSON();
const response = await patchOperationWithObjectId('annotations', req.params.annotationId, payload);
return res.status(HttpStatus.OK).json(response);
} catch (e) {
log.error(e);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
}
}

async function deleteDocuments(req, res) {
try {
const deletedDocuments = req.body;
await Promise.all(
deletedDocuments.map(async (annotationId) => {
await deleteDocument(annotationId);
}),
);
return res.sendStatus(HttpStatus.OK);
} catch (e) {
log.error(e);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
}
}

module.exports = {
createApplicationDocuments,
getApplicationDocuments,
updateDocument,
deleteDocuments,
};
7 changes: 1 addition & 6 deletions backend/src/routes/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const {
updateECEWEApplication,
updateECEWEFacilityApplication,
getApprovableFeeSchedules,
getCCFRIApplication,
getDeclaration,
submitApplication,
updateStatusForApplicationComponents,
Expand All @@ -26,10 +25,6 @@ router.post('/renew-ccof', passport.authenticate('jwt', { session: false }), isV
/* CREATE or UPDATE an existing CCFRI application for opt-in and out
CCOF application guid and facility guid are defined in the payload
*/
router.get('/ccfri/:ccfriId', passport.authenticate('jwt', { session: false }), isValidBackendToken, [param('ccfriId', 'URL param: [ccfriId] is required').notEmpty().isUUID()], (req, res) => {
validationResult(req).throw();
return getCCFRIApplication(req, res);
});

router.get('/ccfri/:ccfriId/afs', passport.authenticate('jwt', { session: false }), isValidBackendToken, [param('ccfriId', 'URL param: [ccfriId] is required').notEmpty().isUUID()], (req, res) => {
validationResult(req).throw();
Expand Down Expand Up @@ -67,7 +62,7 @@ router.patch('/ccfri', passport.authenticate('jwt', { session: false }), isValid
});

router.patch('/ccfri/:ccfriId/', passport.authenticate('jwt', { session: false }), isValidBackendToken, [param('ccfriId', 'URL param: [ccfriId] is required').notEmpty().isUUID()], (req, res) => {
//validationResult(req).throw();
validationResult(req).throw();
return patchCCFRIApplication(req, res);
});

Expand Down
35 changes: 35 additions & 0 deletions backend/src/routes/document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const express = require('express');
const passport = require('passport');
const router = express.Router();
const auth = require('../components/auth');
const { param, validationResult } = require('express-validator');
const isValidBackendToken = auth.isValidBackendToken();
const { createApplicationDocuments, getApplicationDocuments, deleteDocuments, updateDocument } = require('../components/document');

module.exports = router;

router.post('/application/', passport.authenticate('jwt', { session: false }), isValidBackendToken, createApplicationDocuments);

router.get(
'/application/:applicationId',
passport.authenticate('jwt', { session: false }),
isValidBackendToken,
[param('applicationId', 'URL param: [applicationId] is required').notEmpty().isUUID()],
(req, res) => {
validationResult(req).throw();
return getApplicationDocuments(req, res);
},
);

router.delete('/', passport.authenticate('jwt', { session: false }), isValidBackendToken, deleteDocuments);

router.patch(
'/:annotationId',
passport.authenticate('jwt', { session: false }),
isValidBackendToken,
[param('annotationId', 'URL param: [annotationId] is required').notEmpty().isUUID()],
(req, res) => {
validationResult(req).throw();
return updateDocument(req, res);
},
);
Loading

0 comments on commit 049fe5b

Please sign in to comment.