This is a monorepo for granite, an online consent management platform.
- Clone the repo:
git clone git@github.com:nicolaspearson/granite.git
- Install Yarn2
- Switch to the latest stable version:
yarn set version stable
- Install dependencies:
yarn install
- Ensure that you have docker installed and running.
Optionally install yamllint:
brew install yamllint
Note: Required if you are a contributor.
- Download and install Visual Studio Code
- Open the project
- Install the extensions listed in
.vscode/extensions.json
- Switch to the
granite
workspace by opening.vscode/granite.code-workspace
, and clicking on the "Open Workspace" button.
This is a monorepo project, which means it allows multiple packages (workspaces) in a single repository. The packages are divided into the following domains:
backend
: Backend packages and support packages.frontend
: Frontend packages and support packages.shared
: Support packages that are common between thefrontend
andbackend
packages.
This repository uses:
.dockerignore
: ignores the listed files and directories when using the docker COPY command..eslintignore
: ignores the listed files and directories when running ESLint..eslintrc.js
: defines the global ESLint configuration..pnp.cjs
: automatically generated by Yarn2..prettierignore
: ignores the listed files and directories when running Prettier..prettierrc
: defines the global Prettier configuration..yarnrc.yaml
: yarn2 configuration.docker-compose.yaml
: defines docker image for local testingjest.config.js
: defines the global Jest configuration which is inherited by each package.tsconfig.json
: defines the global TypeScript configuration which is inherited by each package.
- commitlint: to enforce the conventional commit style.
- husky: commit hooks that run commitlint, yarn and prettier to ensure quality before pushing.
- eslint: JavaScript and TypeScript linter.
- prettier: code auto formatter.
This monorepo takes advantage of new Yarn 2 features, e.g. Plug 'n Play and zero-installs, to prevent traditional problems caused by such setups.
Project configuration:
- Common
devDependencies
to all packages are defined asdependencies
in the rootpackage.json
. - Common scripts are defined in the root
package.json
, all packages can inherit them. - Each package has its own specific
scripts
,dependencies
anddevDependencies
. - There is no
node_modules
directory, dependencies are committed to the repo via.yarn/cache
. This greatly speeds up CI builds when the monorepo starts to grow.
The preference / consent management service. Unit and integration test coverage for this package is locked in at 100%.
# Build the package
yarn workspace svc-consents build
# Lint the package
yarn workspace svc-consents lint
# Start the service in dev mode
yarn workspace svc-consents start:dev
# Execute the unit tests for the package
yarn workspace svc-consents test:unit
# Execute the integration tests for the package
yarn workspace svc-consents test:integration
The project can also be built and started using docker:
Note: Using docker will start the services in production mode, which excludes database fixtures.
# Build the package using docker
yarn workspace svc-consents docker:build
# Build and start the package using docker
yarn workspace svc-consents docker:start
# Attach to the docker logs for the package
yarn workspace svc-consents docker:logs
There are 4 jobs in the continuous integration workflow for this package
(.github/workflows/backend.svc-consents.yaml
):
- Build and lint the package.
- Run the unit tests.
- Run the integration tests.
- Build the docker image and upload it to the Github docker registry (only on the
main
branch).
Swagger documentation is served on localhost. Requests can be executed directly from the Swagger user interface. The example documentation contains valid fixtures that are automatically populated on application start-up.
This directory contains a REST client file that allows developers to execute REST calls from visual studio code. Alternatively use the Swagger user interface.
- Install the REST client extension for vscode:
humao.rest-client
. - Execute requests from the
consents.http
file.
The section serves to describe the RESTful
endpoints, and in some cases why the implementation
diverges from the requirements in the
code challenge.
I implemented an authentication flow to ensure that only an authorized user is allowed to interact with their data. There are pre-populated fixtures in the database which can be used for testing purposes:
{
"email": "john.doe@example.com",
"password": "myS3cretP@55w0rd!"
}
A list of possible improvements:
- Check password strength in the password validator.
- Use
redis
to whitelist JWT's (described in the delete endpoint section below). - Add architecture diagrams.
- Document
authentication
,event
, anduser
flows usingmermaid.js
.
Description:
Registers a new user in the database. An email address and password must be provided.
A user accepts only one required field (email) that must be a valid email address and unique. If any of the requirements are not satisfied, the API must return a 422 response.
I deviated from the requirement above, and instead return a 400
due to the reasons documented
below.
Exceptions:
- A
400
is returned if an invalid email address is provided or the password contains illegal characters, is too short or too long. - A
400
is also returned if the user already exists. This was done in order to avoid user enumeration attacks on an unauthenticated endpoint. - A
500
is returned if an unexpected error occurs.
Sample response:
{
"id": "343c6ac5-2b72-4c41-a9eb-28f5ae49af80",
"email": "john.doe@example.com",
"consents": []
}
Description:
Authenticates a user and returns a JWT which can be used to interact with the authenticated endpoints.
Exceptions:
- A
400
is returned if an invalid email address is provided or the password contains illegal characters, is too short or too long. - A
404
is returned if the user does not exist. - A
404
is also returned if the user provides invalid credentials. This was done in order to avoid user enumeration attacks on an unauthenticated endpoint. - A
500
is returned if an unexpected error occurs.
Sample response:
{
"token": "<REDACTED>"
}
Description:
Creates a new consent event for the authenticated user.
Exceptions:
- A
400
is returned if the request payload does not pass the validation rules. Theid
must be eitheremail_notifications
orsms_notifications
, and enabled must be aboolean
. - A
401
is returned if the user does not provide a valid JWT in the authorization header. - A
422
is returned if the user does not exist. - A
500
is returned if an unexpected error occurs.
Sample response:
{
"user": {
"id": "343c6ac5-2b72-4c41-a9eb-28f5ae49af80"
},
"consents": [
{
"id": "email_notifications",
"enabled": true
}
]
}
Description:
A generic health check endpoint that indicates whether or not the service is up
.
Exceptions:
None.
Sample response:
{
"status": "OK"
}
Description:
Retrieves the authenticated user's profile. It was assumed that only the latest version of each
event type should be included, e.g. if a user has 5 email_notifications
, and 2
sms_notifications
events only the last entry for each will be returned.
Exceptions:
- A
401
is returned if the user does not provide a valid JWT in the authorization header. - A
404
is returned if the user does not exist. - A
500
is returned if an unexpected error occurs.
Sample response:
{
"id": "343c6ac5-2b72-4c41-a9eb-28f5ae49af80",
"email": "john.doe@example.com",
"consents": [
{
"id": "email_notifications",
"enabled": true
}
]
}
Description:
Deletes the authenticated user from the database.
After the user is deleted their JWT will still be valid. As an improvement I would use redis
to
whitelist JWT's, when a user is deleted their JWT's would be removed from the whitelist. An
additional step would be added to the JwtAuthGuard
that will do a lookup in redis
to ensure the
provided JWT exists
before allowing a request to proceed.
Exceptions:
- A
401
is returned if the user does not provide a valid JWT in the authorization header. - A
500
is returned if an unexpected error occurs.
Sample response:
Returns a 204
if the user was successfully deleted.
This package uses TypeORM and PostgreSQL.
To generate the missing migrations TypeORM applies existing migrations, and uses the diff between the database schema and the TypeORM entities to create a migration file.
# Replace <MigrationName> with a descriptive name for the generated migration
yarn workspace svc-consents db:migration:generate:missing <MigrationName>
The database structure can be seen below (documented using mermaid.js
):
erDiagram
USER {
uuid uuid
string email
string password
date created_at
date updated_at
}
USER ||--o{ EVENT : userUuid
EVENT {
integer id
string type
boolean enabled
date created_at
uuid userUuid
}