diff --git a/.github/workflows/PR-validation.yml b/.github/workflows/PR-validation.yml new file mode 100644 index 00000000..ca26694c --- /dev/null +++ b/.github/workflows/PR-validation.yml @@ -0,0 +1,53 @@ +name: PR validation +on: + pull_request: + types: [synchronize, opened, reopened, edited] + branches: + - master +jobs: + test-api: + permissions: + id-token: write # Required for authentication through OIDC to AWS + runs-on: ubuntu-22.04 + steps: + - name: Report workflow details + run: | + echo "Repository ${{ github.repository }}." + echo "Trigger ref ${{ github.ref }}, base-ref ${{ github.base_ref }}, head_ref ${{ github.head_ref }}." + - name: Check out repository code + uses: actions/checkout@v3 + - name: Report files updated in PR + run: | + git fetch -q origin ${{ github.base_ref }} ${{ github.head_ref }} + git diff --name-only origin/${{ github.base_ref }} origin/${{ github.head_ref }} + - name: Set up JDK 8 + uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'temurin' + - name: Install clojure and clojure cli (clj) + uses: DeLaGuardo/setup-clojure@12.3 + with: + cli: 1.10.1.536 + - name: Report runtime details + run: | + echo "Github runner OS: ${{ runner.os }}" + - name: AWS credentials configuration + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{secrets.GH_ACTIONS_AWS_ROLE}} + role-session-name: gh-actions-${{github.run_id}}.${{github.run_number}}.${{github.run_attempt}}-test-api + aws-region: us-east-1 + - name: Download and install Datomic Pro + run: | + aws s3 cp s3://wormbase/datomic-pro/distro/datomic-pro-1.0.6165.zip ./ + unzip datomic-pro-1.0.6165.zip + cd datomic-pro-1.0.6165/ + bin/maven-install + - name: Generate pom file + run: | + clojure -Spom + - name: Run Integration tests + run: | + make run-tests GOOGLE_APP_PROFILE=dev + #TODO: add UI and API build and container packaging test diff --git a/Makefile b/Makefile index 789028bb..27c3ae62 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ ECR_REPO_NAME := wormbase/names EB_APP_ENV_FILE := app-env.config -PROJ_NAME ?= wormbase-names -LOCAL_GOOGLE_REDIRECT_URI = "http://lvh.me:3000" +PROJ_NAME ?= wormbase-names-dev +LOCAL_GOOGLE_REDIRECT_URI := "http://lvh.me:3000" ifeq ($(PROJ_NAME), wormbase-names) WB_DB_URI ?= "datomic:ddb://us-east-1/WSNames/wormbase" GOOGLE_REDIRECT_URI ?= "https://names.wormbase.org" @@ -11,7 +11,7 @@ else ifeq ($(PROJ_NAME), wormbase-names-test) GOOGLE_REDIRECT_URI ?= "https://test-names.wormbase.org" GOOGLE_APP_PROFILE ?= "prod" else - WB_DB_URI ?= "datomic:ddb://us-east-1/WSNames-test-14/wormbase" + WB_DB_URI ?= "datomic:ddb-local://localhost:8000/WBNames_local/wormbase" # Ensure GOOGLE_REDIRECT_URI is defined appropriately as an env variable or CLI argument # if intended for AWS deployment (default is set for local execution) GOOGLE_REDIRECT_URI ?= ${LOCAL_GOOGLE_REDIRECT_URI} @@ -235,14 +235,17 @@ run-tests: google-oauth2-secrets \ @ export API_GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID} && \ export API_GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET} && \ export GOOGLE_REDIRECT_URI=${LOCAL_GOOGLE_REDIRECT_URI} && \ - clj -A:datomic-pro:webassets:dev:test:run-tests + clojure -A:datomic-pro:logging:webassets:dev:test:run-tests .PHONY: run-dev-server +run-dev-webserver: PORT := 4010 run-dev-webserver: google-oauth2-secrets \ $(call print-help,run-dev-webserver PORT= WB_DB_URI= \ GOOGLE_REDIRECT_URI=,\ Run a local development webserver.) - @ export API_GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID} && \ + @ export WB_DB_URI=${WB_DB_URI} && export PORT=${PORT} && \ + export GOOGLE_REDIRECT_URI=${GOOGLE_REDIRECT_URI} && \ + export API_GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID} && \ export API_GOOGLE_OAUTH_CLIENT_SECRET=${GOOGLE_OAUTH_CLIENT_SECRET} && \ clj -A:logging:datomic-pro:webassets:dev -m wormbase.names.service @@ -259,7 +262,7 @@ run-dev-ui: google-oauth2-secrets\ .PHONY: google-oauth2-secrets google-oauth2-secrets: \ $(call print-help,google-oauth2-secrets,\ - Store the Google oauth2 client details in a secrets file.) + Store the Google oauth2 client details as env variables.) $(eval GOOGLE_OAUTH_CLIENT_ID = $(shell aws ssm get-parameter --name "/name-service/${GOOGLE_APP_PROFILE}/google-oauth2-app-config/client-id" --query "Parameter.Value" --output text --with-decryption)) $(call check_defined, GOOGLE_OAUTH_CLIENT_ID) $(eval GOOGLE_OAUTH_CLIENT_SECRET = $(shell aws ssm get-parameter --name "/name-service/${GOOGLE_APP_PROFILE}/google-oauth2-app-config/client-secret" --query "Parameter.Value" --output text --with-decryption)) diff --git a/README.md b/README.md index 4004c7c1..c3aa6a57 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ * [REST API](#rest-api) - [Tools](#tools) * [Running a Clojure REPL](#running-a-clojure-repl) + * [Debug printing](#debug-printing) * [Client app (web interface)](#client-app-web-interface) - [Testing](#testing) - [Release & deployment](#release--deployment) @@ -152,6 +153,28 @@ This can be done via the following command: clj -A:outdated ``` +#### Debug printing +Standard logging is done using `ch.qos.logback/logback` (`-classic` and `-core`), but this is not flexible +for limited-scale (local) debug printing during coding and debugging. To enable more flexible debug printing: + + 1. In the file you want to enable temporary debug printing, replace the loading of the `clojure.tools.logging` library + with the `taoensso.timbre` library instead (using the same `:log` alias). + + 2. Add the following line below the library loading section + ```clojure + (log/merge-config! {:level :debug}) + ``` + This will enable all log printing up to debug level within that file, + but leave the application-default logging outside of it, enabling a + more focused log inspection. + + 3. Add any additional debug logging with the standard `(log/debug "string" object)` + +`taoensso.timbre` will by default print logs to the following format: +``` + [] - +``` + ### Client app (web interface) Correct functionality of the client app can be tested in two ways: - Running a client development server (during development), to test individual functionality. See [instructions below](#local-rest). @@ -276,7 +299,7 @@ To deploy an update for the main application, change your working dir to the repository root dir and execute the following commands (bash): ```bash # Build the client application to ensure no errors occur. -make ui-build +make ui-build GOOGLE_APP_PROFILE=prod # Generate the pom.xml file (not version-controlled) # to ensure no errors occur (in API code) @@ -309,12 +332,12 @@ sudo service docker start # NOTE: To deploy a tagged or branched codeversion that does not equal your (potentially dirty) working-dir content, # use the additional argument REF_NAME= # E.g. make release AWS_PROFILE=wormbase REF_NAME=wormbase-names-1.4.7 -make release [AWS_PROFILE=] +make release [AWS_PROFILE=] GOOGLE_APP_PROFILE=prod # Deploy the application to an EB environmnent. # Before execution: -# * Ensure to specify the correct EB environment name, in order to prevent -# accidental deployments to the production environment! +# * Ensure to specify the correct EB environment name, +# (otherwise deployment to non-existing dev environment will be attempted) # * Check if the hard-coded WB_DB_URI default (see MakeFile) applies. # If not, define WB_DB_URI to point to the appropriate datomic DB. # * Ensure to define the correct GOOGLE_REDIRECT_URI for google authentication (http://lvh.me:3000 when developing locally) diff --git a/client/package-lock.json b/client/package-lock.json index fafd0ff0..9ef97ea1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1585,9 +1585,9 @@ } }, "@react-oauth/google": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.8.0.tgz", - "integrity": "sha512-xmIhYvvaBSblOzo3Fqv762lqub92SOFR8eRR7QpPyFPZEXZcOymaMuEkRSUKUrbu+9wfB8qtOaGMRJzrN7U2PA==" + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", + "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==" }, "@samverschueren/stream-to-observable": { "version": "0.3.0", diff --git a/client/package.json b/client/package.json index 7575e7db..15931d51 100644 --- a/client/package.json +++ b/client/package.json @@ -15,7 +15,7 @@ "dependencies": { "@material-ui/core": "3.9.2", "@material-ui/icons": "3.0.2", - "@react-oauth/google": "^0.8.0", + "@react-oauth/google": "0.12.x", "classnames": "2.2.6", "copy-to-clipboard": "3.2.0", "react-copy-to-clipboard": "5.1.0", diff --git a/client/src/App.js b/client/src/App.js index 2000f5b1..a353fdf9 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -7,8 +7,10 @@ import Main from './containers/Main'; import { theme, MuiThemeProvider } from './components/elements'; export default () => ( - - + + @@ -18,6 +20,6 @@ export default () => ( - - + + ); diff --git a/client/src/components/elements/SpeciesSelect.js b/client/src/components/elements/SpeciesSelect.js index b44c8a31..88e33df1 100644 --- a/client/src/components/elements/SpeciesSelect.js +++ b/client/src/components/elements/SpeciesSelect.js @@ -7,7 +7,7 @@ import { useDataFetch } from '../../containers/Authenticate'; const SpeciesSelect = (props) => { const memoizedFetchFunc = useCallback( - (authorizedFetch) => + (fetchFn) => mockFetchOrNot( (mockFetch) => { return mockFetch.get('*', [ @@ -27,7 +27,7 @@ const SpeciesSelect = (props) => { ]); }, () => - authorizedFetch(`/api/species`, { + fetchFn(`/api/species`, { method: 'GET', }) ), diff --git a/client/src/containers/Authenticate/Login.js b/client/src/containers/Authenticate/Login.js index d7b7d79a..421b1eef 100644 --- a/client/src/containers/Authenticate/Login.js +++ b/client/src/containers/Authenticate/Login.js @@ -13,7 +13,7 @@ class Login extends Component {
{errorMessage}
) : null} - diff --git a/client/src/containers/Authenticate/Logout.js b/client/src/containers/Authenticate/Logout.js index 4cca0cc2..b7c5d26d 100644 --- a/client/src/containers/Authenticate/Logout.js +++ b/client/src/containers/Authenticate/Logout.js @@ -6,7 +6,7 @@ import { Button } from '../../components/elements'; const Logout = (props) => { return ( - - - - -
{props.children}
+
{props.children}
); }; @@ -32,7 +19,6 @@ Profile.propTypes = { name: PropTypes.string.isRequired, email: PropTypes.string.isRequired, id: PropTypes.string.isRequired, - onLogout: PropTypes.func.isRequired, children: PropTypes.element, }; @@ -44,8 +30,8 @@ const styles = (theme) => ({ justifyContent: 'center', alignItems: 'center', }, - logout: { - margin: theme.spacing.unit * 6, + actions: { + margin: theme.spacing.unit * 4, }, }); diff --git a/client/src/containers/Authenticate/TokenMgmt.js b/client/src/containers/Authenticate/TokenMgmt.js new file mode 100644 index 00000000..4c856b9f --- /dev/null +++ b/client/src/containers/Authenticate/TokenMgmt.js @@ -0,0 +1,171 @@ +import React, { useContext, useReducer, useEffect } from 'react'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; + +import { Button } from '../../components/elements'; +import AuthorizationContext from '../../containers/Authenticate/AuthorizationContext'; +import { useCallback } from 'react'; + +const ACTION_STORE = 'STORE'; +const ACTION_REVOKE = 'REVOKE'; +const UPDATE_METADATA = 'UPDATE_METADATA'; + +function tokenMetaDataReducer(state, action) { + let newState = { ...state }; + + switch (action.type) { + case UPDATE_METADATA: + console.log('Metadata update trigerred.'); + newState = { ...action['payload'] }; + break; + default: + console.log('Invalid action type detected:'); + console.log(action.type); + throw new Error(); + } + + return newState; +} + +function tokenReducer(state, action) { + const newState = { ...state }; + + switch (action.type) { + case ACTION_STORE: + newState['apiToken'] = action.payload; + break; + case ACTION_REVOKE: + newState['apiToken'] = null; + break; + default: + console.log('Invalid action type detected:'); + console.log(action.type); + throw new Error(); + } + + return newState; +} + +function TokenMgmt() { + const { authorizedFetch, user } = useContext(AuthorizationContext); + + const [tokenState, dispatchTokenState] = useReducer(tokenReducer, { + apiToken: null, + }); + + const [tokenMetaDataState, dispatchTokenMetaData] = useReducer( + tokenMetaDataReducer, + { + 'token-stored?': false, + 'last-used': null, + } + ); + + const updateTokenMetadata = useCallback( + () => { + authorizedFetch('/api/auth/token-metadata', { method: 'GET' }) + .then((response) => { + if (response.ok) { + return response.json(); + } else { + console.log( + 'Error while retrieving token metadata. Returned response: ', + response + ); + throw new Error('Error while retrieving token metadata'); + } + }) + .then((data) => { + console.log('token-metadata result received:', data); + + dispatchTokenMetaData({ type: UPDATE_METADATA, payload: data }); + }) + .catch((error) => { + console.log( + 'Error caught on authorizedFetch for token-metadata:', + error + ); + }); + }, + [authorizedFetch] + ); + + const noTokenInstructions = + 'No stored ID token to display.\n' + + "Click the 'Store token' button below to store the current ID token and display it here."; + const newTokenInstructions = + 'Stored tokens can not be retrieved for display after storage.\n' + + "Click the 'Store token' button below to store a new token (invalidating the current stored token) and display it here."; + + function storeTokenHandler() { + console.log('storeTokenHandler triggered.'); + + authorizedFetch(`/api/auth/token`, { + method: 'POST', + }).then((response) => { + if (response.ok) { + dispatchTokenState({ type: ACTION_STORE, payload: user.id_token }); + } else { + console.log('Error returned by /auth/token POST endpoint.'); + throw new Error('API endpoint for token storage returned error.'); + } + }); + } + + function revokeTokenHandler() { + console.log('revokeTokenHandler triggered.'); + + authorizedFetch(`/api/auth/token`, { + method: 'DELETE', + }).then((response) => { + if (response.ok) { + dispatchTokenState({ type: ACTION_REVOKE }); + } else { + console.log('Error returned by /auth/token DELETE endpoint.'); + throw new Error('API endpoint for token revoking returned error.'); + } + }); + } + + useEffect( + () => { + updateTokenMetadata(); + }, + [tokenState, updateTokenMetadata] + ); + + return ( +
+ + Token stored?: {tokenMetaDataState['token-stored?'] ? 'Yes' : 'No'} + +
+ Token last used: {tokenMetaDataState['last-used'] || 'Never'} +
+