Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
1896f18
Add yarn-error.log to .gitignore
whymarrh Oct 10, 2020
22badc6
Reorg endpoints for testing
whymarrh Oct 10, 2020
95d58cb
Added a number of prototype routes as well as the basic config for
michaelgburton Nov 15, 2020
b567174
Added the post for businesses
michaelgburton Nov 25, 2020
99cf387
Moved Firestore data manipulation into ProductionDataLayer
michaelgburton Nov 27, 2020
f18fb7a
Adding some missing done() callbacks
michaelgburton Nov 27, 2020
8a6e4b4
A bunch of new stuff related to deployment. There'll be more to come,
michaelgburton Nov 30, 2020
2c8f5cc
Added CRUD operations and related tests for businesses and regions
michaelgburton Dec 3, 2020
e7f4ae1
Added docker login to the deploy script
michaelgburton Dec 3, 2020
98cf744
Creating a deployment workflow
BurtonTechnical Dec 4, 2020
05f6860
Updating deployment
BurtonTechnical Dec 4, 2020
318146c
Merge branch 'main' into api-routes
BurtonTechnical Dec 4, 2020
10a0b1c
Fixing the build-arg issue. Again.
BurtonTechnical Dec 4, 2020
34a66a8
debug code
BurtonTechnical Dec 4, 2020
2b49fef
Merge branch 'api-routes' into main
michaelgburton Dec 4, 2020
17672cb
Adding region to gcloud auth
BurtonTechnical Dec 4, 2020
1e6450f
Fixing the repository name
BurtonTechnical Dec 4, 2020
32976c9
Fixing gcloud run command
BurtonTechnical Dec 4, 2020
a4e9e45
Fixing line continuations, part 2
BurtonTechnical Dec 4, 2020
41c307d
Fixing compilation error and adding TypeScript compilation to the deploy
michaelgburton Dec 4, 2020
39c4c2d
Merge branch 'main' of https://github.com/BurtonTechnical/RAnLab-api …
michaelgburton Dec 4, 2020
3af63a7
Dockerfile wasn't actually building the project, which was breaking the
michaelgburton Dec 4, 2020
e35da22
Implemented auth0 and related tests
michaelgburton Dec 21, 2020
8c1a25f
Moved the data clearing for the data layer tests to happen before the
michaelgburton Dec 21, 2020
38d12db
Added admin region endpoint and data layer functionality
michaelgburton Dec 21, 2020
0e46d6e
Started the paging for businesses
mikeb-celtx Dec 30, 2020
fdd2478
Resolved merge conflicts between two Christmas worksets
michaelgburton Dec 30, 2020
ebdc27f
Adding single region GET endpoint + test
michaelgburton Jan 14, 2021
5307ddf
Fixing up filters
michaelgburton Jan 16, 2021
82a571c
Adding CORS handling and tests
michaelgburton Jan 17, 2021
8a69d28
Fixing build & test errors
michaelgburton Jan 17, 2021
88eefa4
Fixing cors handling for localhost
michaelgburton Jan 20, 2021
a022768
Fixing the route for business creation
michaelgburton Jan 22, 2021
c28cb35
Adding swagger docs to the api at [server]/documentation
michaelgburton Jan 31, 2021
54e4fbc
Opening up the CORS origins a bit for testing
michaelgburton Feb 2, 2021
2a78a75
Fixing up the auth0 tests and code.
michaelgburton Feb 6, 2021
6ee8d7e
Fixed a bunch of tests that broke in the last couple of updates
michaelgburton Feb 7, 2021
538efe8
Changes based on conversation with Lesley over the weekend:
michaelgburton Feb 8, 2021
95e6c54
Temporarily disabling auth0
michaelgburton Feb 12, 2021
7cde9a7
Fixing syntax errors from previous commit
michaelgburton Feb 12, 2021
f026530
Revert "Temporarily disabling auth0"
michaelgburton Feb 13, 2021
6fe5f77
Modified Auth0 configuration to work with opaque user access tokens a…
michaelgburton Feb 14, 2021
dbacd55
Added "all industries" endpoint for admin users
michaelgburton Feb 20, 2021
e7446d5
Adding more edit request functionality and testing
michaelgburton Feb 27, 2021
d15a32e
Adding the new edit endpoint to the production server.
michaelgburton Feb 27, 2021
1bde112
Adding auth0 client secrets to gcloud deployment
BurtonTechnical Mar 6, 2021
084cb3a
Adding the new management client ID to .env and making a couple of QoL
michaelgburton Mar 6, 2021
0d7534e
Merge branch 'main' of https://github.com/BurtonTechnical/RAnLab-api …
michaelgburton Mar 6, 2021
55d1b64
Syntax fix
BurtonTechnical Mar 6, 2021
2a4e57d
Attempting to fix the secrets
BurtonTechnical Mar 6, 2021
8d76520
Adding all of the required environment variables
BurtonTechnical Mar 7, 2021
f6dc897
Adding endpoint for getting a single edit request by id along with
michaelgburton Mar 7, 2021
e964eb6
Added update endpoint for edit requests and related tests
michaelgburton Mar 8, 2021
6cf79b4
Merge branch 'main' of https://github.com/BurtonTechnical/RAnLab-api …
michaelgburton Mar 8, 2021
403eb17
Adding queries by pending and reviewed statuses
michaelgburton Mar 8, 2021
2e2e0a3
Fixing swagger docs for new edit request queries
michaelgburton Mar 8, 2021
18d020d
Added edit request previews
michaelgburton Mar 9, 2021
ffee21e
Added pagination and query-by-status on /edits/all
michaelgburton Mar 12, 2021
00cbb92
Added CSV export (/businesses/export)
michaelgburton Mar 14, 2021
1b9a361
Fixing up a bit of pagination, fixing related tests, and rearranging
michaelgburton Mar 14, 2021
1389a79
Adding Auth0 user endpoints and custom page sizes for paginated requests
michaelgburton Mar 15, 2021
795579e
Fixing documentation
michaelgburton Mar 15, 2021
cfd85f2
Adding reviewer
michaelgburton Mar 18, 2021
3742ac9
Implemented Approval flow for edit requests
michaelgburton Mar 19, 2021
3b7859c
Fixes:
michaelgburton Apr 18, 2021
ea6d8f0
Fixing compile errors
michaelgburton Apr 18, 2021
a96f914
Adding global list of industries in addition to regional ones
michaelgburton Apr 29, 2021
f65f6ef
Added edit request count to paginated endpoints
michaelgburton Apr 30, 2021
2166d8e
Fixing docs and removing deprecated API method.
michaelgburton Apr 30, 2021
97a96db
Merge branch 'main' into mvp-final-branch
BurtonTechnical Apr 30, 2021
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: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AUTH0_DOMAIN=lesleychard.auth0.com
AUTH0_CLIENT_ID=bnyMiWB15Rz5CwsxTNnrN9v5ftHcdabj
AUTH0_MGMT_CLIENT_ID=mxNP8B5JnQ4v049Vt2mR80z9rvov5yfj
AUTH0_CLAIMS_NAMESPACE=https://lesleychard
52 changes: 52 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Deploy to Google Cloud Run
on:
release:
types: [published]
jobs:
build:
environment: "GC Run"
name: Build image
runs-on: ubuntu-latest
env:
HASH: $(git rev-parse --short "$GITHUB_SHA")
BRANCH: ${GITHUB_REF##*/}
SERVICE_NAME: ${{ secrets.SERVICE_NAME }}
PROJECT_ID: ${{ secrets.PROJECT_ID }}
AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_CLIENT_SECRET }}
AUTH0_MGMT_CLIENT_SECRET: ${{ secrets.AUTH0_MGMT_CLIENT_SECRET }}
AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }}
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
AUTH0_MGMT_CLIENT_ID: ${{ secrets.AUTH0_MGMT_CLIENT_ID }}
AUTH0_CLAIMS_NAMESPACE: ${{ secrets.AUTH0_CLAIMS_NAMESPACE }}
steps:
- name: Checkout
uses: actions/checkout@v2
# Setup gcloud CLI
- uses: google-github-actions/github-actions/setup-gcloud@master
with:
service_account_key: ${{ secrets.GCR_DEVOPS_SERVICE_ACCOUNT_KEY }}
project_id: ${{ secrets.PROJECT_ID }}
export_default_credentials: true
- run: |
ls -la
# Build docker image
- name: Build Docker Image
run: |-
docker build -t northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest .
# Configure docker to use the gcloud command-line tool as a credential helper
- run: |
gcloud auth configure-docker -q northamerica-northeast1-docker.pkg.dev
# Push image to Google Container Registry
- name: Push Image to GCR
run: |-
docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest
- name: Deploy Container
run: |-
gcloud run \
deploy ranlab-api-mvp \
--quiet \
--platform=managed \
--region=northamerica-northeast1 \
--allow-unauthenticated \
--image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest \
--set-env-vars AUTH0_MGMT_CLIENT_SECRET=$AUTH0_MGMT_CLIENT_SECRET,AUTH0_CLIENT_SECRET=$AUTH0_CLIENT_SECRET,AUTH0_DOMAIN=$AUTH0_DOMAIN,AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID,AUTH0_MGMT_CLIENT_ID=$AUTH0_MGMT_CLIENT_ID,AUTH0_CLAIMS_NAMESPACE=$AUTH0_CLAIMS_NAMESPACE
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
yarn-error.log

.idea/
node_modules/

*.tsbuildinfo
Expand Down
Empty file added .npmignore
Empty file.
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM node:15

# Create and change to the app directory.
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
# Copying this separately prevents re-running npm install on every code change.
COPY package*.json ./

# Install production dependencies.
RUN yarn install --only=production

# Copy local code to the container image.
COPY . .

RUN yarn build

# Run the web service on container startup.
CMD [ "yarn", "start" ]
6 changes: 6 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
+ '@babel/preset-typescript',
],
};
10 changes: 10 additions & 0 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Prerequisite Setup Steps:
# gcloud artifacts repositories create ranlab-api-mvp --repository-format docker --location northamerica-northeast1 # create the artifact repo
# gcloud auth configure-docker northamerica-northeast1-docker.pkg.dev

docker build -t ranlab-mvp-api:latest .
docker tag ranlab-mvp-api:latest northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest
gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://gcr.io
docker push northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest
gcloud run --platform=managed --region=northamerica-northeast1 deploy ranlab-api-mvp --image northamerica-northeast1-docker.pkg.dev/ranlab-mvp-295423/ranlab-api-mvp/ranlab-api-mvp:latest

19 changes: 19 additions & 0 deletions dev/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

set -u
set -o pipefail

readonly all_files=$( find src -type f -name '*.test.js' )

for f in ${1:-$all_files}
do
output=$(node "$f")
status="$?"
if [[ "$status" -eq 0 ]]
then
printf '%s' "${output}"
else
printf '%s\n' "${output}"
exit "$status"
fi
done
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ["/node_modules/","/build/"]
};
15 changes: 15 additions & 0 deletions jsdoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"opts": {
"template": "node_modules/better-docs"
},
"tags": {
"allowUnknownTags": ["optional", "category"]
},
"plugins": [
"node_modules/better-docs/typescript",
"node_modules/better-docs/category"
],
"source": {
"includePattern": ".+\\.(jsx|js|ts|tsx)$"
}
}
27 changes: 23 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"name": "@ranlab/api",
"version": "0.0.0",
"version": "0.1.0",
"license": "UNLICENSED",
"private": true,
"description": "API for the RAnLab app",
"main": "src/index.js",
"main": "build/src/index.js",
"scripts": {
"build": "tsc --project .",
"test": ":",
"test": "dev/test",
"jest": "jest",
"start": "node ."
},
"repository": {
Expand All @@ -22,10 +23,28 @@
"node": ">=12"
},
"dependencies": {
"fastify": "^3.5.1"
"@auth0/auth0-spa-js": "^1.13.6",
"@google-cloud/firestore": "^4.9.9",
"@types/node-fetch": "^2.5.8",
"body-parser": "^1.19.0",
"fastify": "^3.5.1",
"fastify-auth0-verify": "^0.4.2",
"fastify-authz-jwks": "^1.1.11",
"fastify-cors": "^5.1.0",
"fastify-jwt": "^2.1.3",
"fastify-sensible": "^3.1.0",
"fastify-swagger": "^4.0.1",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^1.12.0",
"jwt-decode": "^3.1.2",
"node-fetch": "^2.6.1"
},
"devDependencies": {
"@types/jest": "^26.0.15",
"@types/node": "^12.12.67",
"baretest": "^2.0.0",
"jest": "^26.6.3",
"ts-jest": "^26.4.4",
"typescript": "^4.0.3"
}
}
165 changes: 165 additions & 0 deletions src/auth0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { FastifyRequest} from "fastify";
import fetch from "node-fetch";

let moduleAdminToken: string;

async function getAdminToken(refresh : boolean) {
if(refresh || !moduleAdminToken) {
let mgmtDomain = process.env.AUTH0_DOMAIN;

let refreshResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, {
"method": "post",
"headers": {
"Content-Type": "application/json"
},
body: JSON.stringify({
"grant_type": "client_credentials",
"client_id": process.env.AUTH0_MGMT_CLIENT_ID,
"client_secret": process.env.AUTH0_MGMT_CLIENT_SECRET,
"audience": `https://${mgmtDomain}/api/v2/`,
"scope": "read:users update:users read:users_app_metadata update:users_app_metadata"
})
});
let refreshJson = await refreshResponse.json();
moduleAdminToken = refreshJson.access_token
}
return moduleAdminToken;
}

export interface Auth0UserInfo {
user_id: string, // Should be URL encoded since it may contain characters that do not work well in a URL.
username?: string,
email?: string
email_verified?: boolean,
phone_number?: string
phone_verified?: boolean,
created_at?: string,
updated_at?: string,
identities?: [
{
connection?: string,
user_id: string,
provider: string,
isSocial?: boolean
}
],
app_metadata?: any,
user_metadata?: any,
picture?: string,
name?: string,
nickname?: string,
multifactor?: string[],
last_ip?: string,
last_login?: string,
logins_count?: number,
blocked?: boolean,
given_name?: string,
family_name?: string
}

export interface UserInfoPatch {
blocked?: boolean,
email_verified?: boolean,
email?: string,
phone_number?: string,
phone_verified?: boolean,
user_metadata?: any,
app_metadata?: any,
given_name?: string,
family_name?: string,
name?: string,
nickname?: string,
picture?: string,
verify_email?: boolean,
verify_phone_number?: boolean,
password?: string,
connection?: string,
client_id?: string,
username?: string
}

async function getUserRole(userId: string) : Promise<{role:string}> {
let app_metadata = (await getUserById(userId)).app_metadata;
return {role: app_metadata.role};
}

/*
Get user by ID: https://auth0.com/docs/api/management/v2#!/Users/get_users_by_id
Get all users: https://auth0.com/docs/api/management/v2#!/Users/get_users
Update a user: https://auth0.com/docs/api/management/v2#!/Users/patch_users_by_id
*/

export async function getUserById(userId: string) : Promise<Auth0UserInfo> {
userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`;
return callManagementApi<Auth0UserInfo>(`/users/${userId}`);
}

export async function getAllUsers(per_page? : number, page?: number) {
let querystring = "";
if(!!per_page || !!page) {
querystring = "?";
let perPageClause = !per_page ? "" : `per_page=${per_page}`
let pageClause = !page ? "" : `page=${page}`;
querystring = `?${perPageClause}&${pageClause}`;
}
return callManagementApi<Auth0UserInfo[]>(`/users${querystring}`);
}

export async function updateUser(userId: string, userInfoPatch: UserInfoPatch) {
userId = userId.startsWith("auth0|") ? userId : `auth0|${userId}`;
return callManagementApi<Auth0UserInfo>(`/users/${userId}`, "PATCH", JSON.stringify(userInfoPatch));
}

async function callManagementApi<T>(path: string, method = "GET", body = "", refresh = false) : Promise<T> {
let mgmtDomain = process.env.AUTH0_DOMAIN;
let adminToken = await getAdminToken(refresh);
let url = encodeURI(`https://${mgmtDomain}/api/v2${path}`);
let options : any = {
method,
"headers": {
"Authorization": `Bearer ${adminToken}`
}
};
if(!!body) {
options.headers["Content-Type"] = "application/json";
options.body = body;
}
let metaResponse = await fetch(url, options);
if(metaResponse.status === 200) {
let json = await metaResponse.json()
return json;
} else if (!refresh && metaResponse.status === 401) {
return await callManagementApi<T>(path, method, body,true);
} else {
throw new Error(JSON.stringify(metaResponse));
}
}


export async function getUserInfo(authHeader: string) {
try {
let userResponse = await fetch(`https://${process.env.AUTH0_DOMAIN}/userinfo`, {
"method": "GET",
"headers": {"Authorization": authHeader}
});
let payload: any = await userResponse.json();
let userId : string = !payload.sub ? "" : payload.sub;
return {userId};
} catch (e) {
throw e;
}
}

export type Auth0JwtVerifier = (request: FastifyRequest) => Promise<{userAppId: string, admin: boolean, role: string}>;
export async function verifyJwt(request: FastifyRequest) {
let authHeader = !request.headers.authorization ? "" : request.headers.authorization
if(!authHeader) {
return {userAppId: "", admin: false, role: ""};
} else {
let {userId} = await getUserInfo(authHeader);
let {role} = await getUserRole(userId);
let admin: boolean = role === "admin"
let userAppId = userId.indexOf("|") > 0 ? userId.split("|")[1] : userId;
return {userAppId, admin, role};
}
}
10 changes: 10 additions & 0 deletions src/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {FastifyInstance} from "fastify";
import fastifyCors from "fastify-cors";

export function registerCorsHandler(server: FastifyInstance) {
server.register(fastifyCors, {
origin: [/^https?:\/\/localhost/, /^https?:\/\/ranlab-app-phzez.ondigitalocean.app/],
credentials: true,
strictPreflight: true
});
}
3 changes: 3 additions & 0 deletions src/database/firestore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {Firestore} from "@google-cloud/firestore";

export const productionFirestore = new Firestore();
Loading