Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assign a user to submission #9

Merged
merged 8 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![Maintainability](https://api.codeclimate.com/v1/badges/77078c9bd93bd99d5840/maintainability)](https://codeclimate.com/github/bcgov/nr-permitconnect-navigator-service/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/77078c9bd93bd99d5840/test_coverage)](https://codeclimate.com/github/bcgov/nr-permitconnect-navigator-service/test_coverage)

A clean Vue 3 frontend & backend scaffold example
NR PermitConnect Navigator Service

To learn more about the **Common Services** available visit the [Common Services Showcase](https://bcgov.github.io/common-service-showcase/) page.

Expand All @@ -21,6 +21,8 @@ app/ - Application Root
├── src/ - Node.js web application
│ ├── components/ - Components Layer
│ ├── controllers/ - Controller Layer
│ ├── db/ - Database Layer
│ ├── interfaces/ - Typescript interface definitions
│ ├── middleware/ - Middleware Layer
│ ├── routes/ - Routes Layer
│ ├── services/ - Services Layer
Expand All @@ -39,7 +41,7 @@ frontend/ - Frontend Root
│ ├── types/ - Typescript type definitions
│ ├── utils/ - Utility components
│ └── views/ - View Layer
└── tests/ - Node.js web application tests
└── tests/ - Vitest web application tests
CODE-OF-CONDUCT.md - Code of Conduct
COMPLIANCE.yaml - BCGov PIA/STRA compliance status
CONTRIBUTING.md - Contributing Guidelines
Expand All @@ -50,18 +52,23 @@ SECURITY.md - Security Policy and Reporting

## Documentation

- [Application Readme](frontend/README.md)
- [Application Readme](app/README.md)
- [Frontend Readme](frontend/README.md)
- [Product Roadmap](https://github.com/bcgov/nr-permitconnect-navigator-service/wiki/Product-Roadmap)
- [Product Wiki](https://github.com/bcgov/nr-permitconnect-navigator-service/wiki)
- [Security Reporting](SECURITY.md)

## Quick Start Dev Guide

You can quickly run this application in development mode after cloning by opening two terminal windows and running the following commands (assuming you have already set up local configuration as well). Refer to the [Application Readme](app/README.md) and [Frontend Readme](app/frontend/README.md) for more details.
You can quickly run this application in development mode after cloning by opening two terminal windows and running the following commands (assuming you have already set up local configuration as well). Refer to the [Application Readme](app/README.md) and [Frontend Readme](/frontend/README.md) for more details.

- Create `.env` in the root directory with the following
- `DATABASE_URL="your_connection_string"`

```
cd app
npm i
npm run prisma:migrate
npm run serve
```

Expand Down
8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Security Policies and Procedures

This document outlines security procedures and general policies for the NR Permitting Navigator Service
This document outlines security procedures and general policies for the NR PermitConnect Navigator Service
project.

- [Supported Versions](#supported-versions)
Expand All @@ -10,7 +10,7 @@ project.

## Supported Versions

At this time, only the latest version of NR Permitting Navigator Service is supported.
At this time, only the latest version of NR PermitConnect Navigator Service is supported.

| Version | Supported |
| ------- | ------------------ |
Expand All @@ -19,8 +19,8 @@ At this time, only the latest version of NR Permitting Navigator Service is supp

## Reporting a Bug

The `CSS` team and community take all security bugs in `NR Permitting Navigator Service` seriously.
Thank you for improving the security of `NR Permitting Navigator Service`. We appreciate your efforts and
The `CSS` team and community take all security bugs in `NR PermitConnect Navigator Service` seriously.
Thank you for improving the security of `NR PermitConnect Navigator Service`. We appreciate your efforts and
responsible disclosure and will make every effort to acknowledge your
contributions.

Expand Down
13 changes: 12 additions & 1 deletion app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,18 @@ app.use(compression());
app.use(cors(DEFAULTCORS));
app.use(express.json({ limit: config.get('server.bodyLimit') }));
app.use(express.urlencoded({ extended: true }));
app.use(helmet());
app.use(
helmet({
contentSecurityPolicy: {
directives: {
'default-src': [
"'self'", // eslint-disable-line
new URL(config.get('frontend.oidc.authority')).origin
]
}
}
})
);

// Skip if running tests
if (process.env.NODE_ENV !== 'test') {
Expand Down
4 changes: 3 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
"migrate:up": "knex migrate:up",
"postmigrate:up": "npm run prisma:sync",
"seed": "knex seed:run",
"prisma:sync": "prisma db pull"
"prisma:sync": "prisma db pull",
"postprisma:sync": "npm run prisma:generate",
"prisma:generate": "prisma generate"
},
"dependencies": {
"@prisma/client": "^5.7.0",
Expand Down
59 changes: 57 additions & 2 deletions app/src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,25 @@ import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

import { getLogger } from './log';
import { ChefsFormConfig, ChefsFormConfigData } from '../types/ChefsFormConfig';
import { YRN } from '../types/YRN';

import type { ChefsFormConfig, ChefsFormConfigData, YRN } from '../types';

const log = getLogger(module.filename);

/**
* @function addDashesToUuid
* Yields a lowercase uuid `str` that has dashes inserted, or `str` if not a string.
* @param {string} str The input string uuid
* @returns {string} The string `str` but with dashes inserted, or `str` if not a string.
*/
export function addDashesToUuid(str: string): string {
if (str.length === 32) {
Dismissed Show dismissed Hide dismissed
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
20
)}`.toLowerCase();
} else return str;
}

/**
* @function fromYrn
* Converts a YRN to boolean
Expand Down Expand Up @@ -77,6 +92,46 @@ export function isTruthy(value: unknown) {
return value === true || value === 1 || (isStr && trueStrings.includes(value.toLowerCase()));
}

/**
* @function mixedQueryToArray
* Standardizes query params to yield an array of unique string values
* @param {string|string[]} param The query param to process
* @returns {string[]} A unique, non-empty array of string values, or undefined if empty
*/
export function mixedQueryToArray(param: string | Array<string>): Array<string> | undefined {
// Short circuit undefined if param is falsy
if (!param) return undefined;

const parsed = Array.isArray(param) ? param.flatMap((p) => parseCSV(p)) : parseCSV(param);
const unique = [...new Set(parsed)];

return unique.length ? unique : undefined;
}

/**
* @function parseCSV
* Converts a comma separated value string into an array of string values
* @param {string} value The CSV string to parse
* @returns {string[]} An array of string values, or `value` if it is not a string
*/
export function parseCSV(value: string): Array<string> {
return value.split(',').map((s) => s.trim());
}

/**
* @function parseIdentityKeyClaims
* Returns an array of strings representing potential identity key claims
* Array will always end with the last value as 'sub'
* @returns {string[]} An array of string values, or `value` if it is not a string
*/
export function parseIdentityKeyClaims(): Array<string> {
const claims: Array<string> = [];
if (config.has('server.oidc.identityKey')) {
claims.push(...parseCSV(config.get('server.oidc.identityKey')));
}
return claims.concat('sub');
}

/**
* @function readIdpList
* Acquires the list of identity providers to be used
Expand Down
9 changes: 3 additions & 6 deletions app/src/controllers/chefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { IdentityProvider } from '../components/constants';

import type { NextFunction, Request, Response } from 'express';
import type { JwtPayload } from 'jsonwebtoken';
import type { ChefsFormConfig, ChefsFormConfigData } from '../types/ChefsFormConfig';
import type { ChefsSubmissionFormExport } from '../types/ChefsSubmissionFormExport';
import type { ChefsFormConfig, ChefsFormConfigData, ChefsSubmissionFormExport } from '../types';

const controller = {
getFormExport: async (req: Request, res: Response, next: NextFunction) => {
Expand Down Expand Up @@ -52,17 +51,15 @@ const controller = {

getSubmission: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await chefsService.getSubmission(req.query.formId as string, req.params.submissionId);
res.status(200).send(response);
res.status(200).send(await chefsService.getSubmission(req.query.formId as string, req.params.submissionId));
kyle1morel marked this conversation as resolved.
Show resolved Hide resolved
} catch (e: unknown) {
next(e);
}
},

updateSubmission: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await chefsService.updateSubmission(req.body);
res.status(200).send(response);
res.status(200).send(await chefsService.updateSubmission(req.body));
} catch (e: unknown) {
next(e);
}
Expand Down
1 change: 1 addition & 0 deletions app/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as chefsController } from './chefs';
export { default as userController } from './user';
29 changes: 29 additions & 0 deletions app/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { addDashesToUuid, mixedQueryToArray, isTruthy } from '../components/utils';
import { userService } from '../services';

import type { NextFunction, Request, Response } from 'express';

const controller = {
searchUsers: async (req: Request, res: Response, next: NextFunction) => {
try {
const userIds = mixedQueryToArray(req.query.userId as string);

const response = await userService.searchUsers({
userId: userIds ? userIds.map((id) => addDashesToUuid(id)) : userIds,
identityId: mixedQueryToArray(req.query.identityId as string),
idp: mixedQueryToArray(req.query.idp as string),
username: req.query.username as string,
email: req.query.email as string,
firstName: req.query.firstName as string,
fullName: req.query.fullName as string,
lastName: req.query.lastName as string,
active: isTruthy(req.query.active as string)
});
res.status(200).send(response);
} catch (e: unknown) {
next(e);
}
}
};

export default controller;
26 changes: 26 additions & 0 deletions app/src/db/dataConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import config from 'config';
import { PrismaClient } from '@prisma/client';

let prisma: PrismaClient;

const db = {
host: config.get('server.db.host'),
user: config.get('server.db.username'),
password: config.get('server.db.password'),
database: config.get('server.db.database'),
port: config.get('server.db.port'),
poolMax: config.get('server.db.poolMax')
};

// @ts-expect-error 2458
if (!prisma) {
const datasourceUrl = `postgresql://${db.user}:${db.password}@${db.host}:${db.port}/${db.database}?&connection_limit=${db.poolMax}`;
prisma = new PrismaClient({
// TODO: https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging#event-based-logging
log: ['error', 'warn'],
errorFormat: 'pretty',
datasourceUrl: datasourceUrl
});
}

export default prisma;
6 changes: 3 additions & 3 deletions app/src/db/migrations/20231212000000_init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function up(knex: Knex): Promise<void> {
knex.schema.createTable('submission', (table) => {
table.uuid('submissionId').primary();
table.uuid('assignedToUserId').references('userId').inTable('user').onUpdate('CASCADE').onDelete('CASCADE');
table.text('confirmationId');
table.text('confirmationId').notNullable();
table.text('contactEmail');
table.text('contactPhoneNumber');
table.text('contactFirstName');
Expand All @@ -53,8 +53,8 @@ export async function up(knex: Knex): Promise<void> {
table.text('relatedPermits');
table.boolean('updatedAai');
table.text('waitingOn');
table.timestamp('submittedAt', { useTz: true });
table.text('submittedBy');
table.timestamp('submittedAt', { useTz: true }).notNullable();
table.text('submittedBy').notNullable();
table.timestamp('bringForwardDate', { useTz: true });
table.text('notes');
stamps(knex, table);
Expand Down
3 changes: 3 additions & 0 deletions app/src/db/models/disconnectRelation.ts
kyle1morel marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
disconnect: true
};
26 changes: 26 additions & 0 deletions app/src/db/models/identity_provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Prisma } from '@prisma/client';

import type { IStamps } from '../../interfaces/IStamps';
import type { IdentityProvider } from '../../types';

// Define a type
const _identityProvider = Prisma.validator<Prisma.identity_providerDefaultArgs>()({});
type DBIdentityProvider = Omit<Prisma.identity_providerGetPayload<typeof _identityProvider>, keyof IStamps>;

export default {
toDBModel(input: IdentityProvider): DBIdentityProvider {
return {
idp: input.idp,
active: input.active
};
},

fromDBModel(input: DBIdentityProvider | null): IdentityProvider | null {
if (!input) return null;

return {
idp: input.idp,
active: input.active
};
}
};
4 changes: 4 additions & 0 deletions app/src/db/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as disconnectRelation } from './disconnectRelation';
export { default as identity_provider } from './identity_provider';
export { default as submission } from './submission';
export { default as user } from './user';
Loading
Loading