diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 223a6b7d9..16954b1b1 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -17,6 +17,8 @@ The `.devcontainer` folder contains the `devcontainer.json` file which defines t In order to run CHEFS you require Keycloak (configured), Postgresql (seeded) and the CHEFS backend/API and frontend/UX. Previously, this was a series of downloads and configuration updates and numerous commands to run. See `.devcontainer/chefs_local` files. +**NODE_CONFIG_DIR** to simplify loading a default configuration to the CHEFS infrastructure (Keycloak, Postgresql, etc), we set an environment variable [`NODE_CONFIG_DIR`](https://github.com/node-config/node-config/wiki/Environment-Variables#node_config_dir). This supercedes the files found under `app/config`. Running node apps and commands (ex. knex, launch configurations) will use this environment variable and load configuration from `.devcontainer/chefs_local`. + Also included are convenient launch tasks to run and debug CHEFS. ## Open CHEFS in the devcontainer @@ -72,6 +74,37 @@ _Notes_ If you are developing the formio components, you should build and redeploy them before running your local debug instances of CHEFS. Use tasks `Components build` and `Components Deploy`. +## KNEX - Database tools +[knex](https://knexjs.org) is installed globally and should be run from the `/app` directory where the knex configuration is located. Use knex to stub out migrations or to rollback migrations as you are developing. + +### create a migration file +This will create a stub file with a timestamp. You will populate the up and down methods to add/update/delete database objects. + +``` +cd app +knex migrate:make my_new_migration_script +> Created Migration: /workspaces/common-hosted-form-service/app/src/db/migrations/20240119172630_my_new_migration_script.js +``` + +### rollback previous migration +When developing your migrations, you may find it useful to run the migration and roll it back if it isn't exactly what you expect to happen. + +#### run the migration(s) +``` +cd app +knex migrate:latest +> Batch 2 run: 1 migrations +``` + +#### rollback the migration(s) +``` +cd app +knex migrate:rollback +> Batch 2 rolled back: 1 migrations +``` + +Please review the [knex](https://knexjs.org) for more detail and how to leverage the tool. + ## Troubleshooting All development machines are unique and here we will document problems that have been encountered and how to fix them. diff --git a/.devcontainer/chefs_local/local.json.sample b/.devcontainer/chefs_local/local.json.sample index d5a02db02..ebbeaf9c7 100644 --- a/.devcontainer/chefs_local/local.json.sample +++ b/.devcontainer/chefs_local/local.json.sample @@ -30,21 +30,24 @@ "frontend": { "apiPath": "api/v1", "basePath" : "/app", - "keycloak": { - "clientId": "chefs-frontend-local", - "realm": "chefs", - "serverUrl": "http://localhost:8082" + "oidc": { + "clientId": "chefs-frontend-localhost-5300", + "realm": "standard", + "serverUrl": "https://dev.loginproxy.gov.bc.ca/auth", + "logoutUrl": "https://logon7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https%3A%2F%2Fdev.loginproxy.gov.bc.ca%2Fauth%2Frealms%2Fstandard%2Fprotocol%2Fopenid-connect%2Flogout" } }, "server": { "apiPath": "/api/v1", "basePath" : "/app", "bodyLimit": "30mb", - "keycloak": { - "clientId": "chefs", - "realm": "chefs", - "serverUrl": "http://localhost:8082", - "clientSecret": "XXXXXXXXXXXX" + "oidc": { + "realm": "standard", + "serverUrl": "https://dev.loginproxy.gov.bc.ca/auth", + "jwksUri": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/certs", + "issuer": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard", + "audience": "chefs-frontend-localhost-5300", + "maxTokenAge": "300" }, "logLevel": "http", "port": "8080", diff --git a/.devcontainer/chefs_local/test.json b/.devcontainer/chefs_local/test.json new file mode 100644 index 000000000..6aa8fd1da --- /dev/null +++ b/.devcontainer/chefs_local/test.json @@ -0,0 +1,92 @@ +{ + "db": { + "database": "chefs", + "host": "localhost", + "port": "5432", + "username": "app", + "password": "admin" + }, + "files": { + "uploads": { + "enabled": "true", + "fileCount": "1", + "fileKey": "files", + "fileMaxSize": "25MB", + "fileMinSize": "0KB", + "path": "files" + }, + "permanent": "localStorage", + "localStorage": { + "path": "myfiles" + }, + "objectStorage": { + "accessKeyId": "bcgov-citz-ccft", + "bucket": "chefs", + "endpoint": "https://commonservices.objectstore.gov.bc.ca", + "key": "chefs/dev/", + "secretAccessKey": "anything" + } + }, + "frontend": { + "apiPath": "api/v1", + "basePath": "/app", + "oidc": { + "clientId": "chefs-frontend-localhost-5300", + "realm": "standard", + "serverUrl": "https://dev.loginproxy.gov.bc.ca/auth", + "logoutUrl": "https://logon7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https%3A%2F%2Fdev.loginproxy.gov.bc.ca%2Fauth%2Frealms%2Fstandard%2Fprotocol%2Fopenid-connect%2Flogout" + } + }, + "server": { + "apiPath": "/api/v1", + "basePath": "/app", + "bodyLimit": "30mb", + "oidc": { + "realm": "standard", + "serverUrl": "https://dev.loginproxy.gov.bc.ca/auth", + "jwksUri": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/certs", + "issuer": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard", + "audience": "chefs-frontend-localhost-5300", + "maxTokenAge": "300" + }, + "logLevel": "http", + "port": "8080", + "rateLimit": { + "public": { + "windowMs": "900000", + "max": "100" + } + } + }, + "serviceClient": { + "commonServices": { + "ches": { + "endpoint": "https://ches-dev.api.gov.bc.ca/api", + "tokenEndpoint": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", + "clientId": "CHES_CLIENT_ID", + "clientSecret": "CHES_CLIENT_SECRET" + }, + "cdogs": { + "endpoint": "https://cdogs-dev.api.gov.bc.ca/api", + "tokenEndpoint": "https://dev.loginproxy.gov.bc.ca/auth/realms/comsvcauth/protocol/openid-connect/token", + "clientId": "CDOGS_CLIENT_ID", + "clientSecret": "CDOGS_CLIENT_SECRET" + } + } + }, + "customBcAddressFormioComponent": { + "apikey": "xxxxxxxxxxxxxxx", + "bcAddressURL": "https://geocoder.api.gov.bc.ca/addresses.json", + "queryParameters": { + "echo": false, + "brief": true, + "minScore": 55, + "onlyCivic": true, + "maxResults": 15, + "autocomplete": true, + "matchAccuracy": 100, + "matchPrecision": "occupant, unit, site, civic_number, intersection, block, street, locality, province", + "precisionPoints": 100 + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6f7cebd58..f0ce7abf1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -48,8 +48,12 @@ "editor.formatOnSave": true } } - } + }, // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - //"remoteUser": "root" + //"remoteUser": "root", + + "containerEnv": { + "NODE_CONFIG_DIR": "${containerWorkspaceFolder}/.devcontainer/chefs_local" + } } diff --git a/.devcontainer/post-install.sh b/.devcontainer/post-install.sh index c6d2ea823..46eec3560 100644 --- a/.devcontainer/post-install.sh +++ b/.devcontainer/post-install.sh @@ -5,6 +5,9 @@ set -ex WORKSPACE_DIR=$(pwd) CHEFS_LOCAL_DIR=${WORKSPACE_DIR}/.devcontainer/chefs_local +npm install knex -g +npm install jest -g + # install app libraries, prepare for app development and debugging... cd app npm install diff --git a/.vscode/launch.json b/.vscode/launch.json index 95afba61f..c62380825 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,7 @@ "runtimeExecutable": "npm", "type": "node", "env": { - "NODE_CONFIG_DIR": "${workspaceFolder}/.devcontainer/chefs_local", + "NODE_CONFIG_DIR": "${workspaceFolder}/.devcontainer/chefs_local" } }, { @@ -32,7 +32,7 @@ "request": "launch", "runtimeArgs": ["run", "dev"], "runtimeExecutable": "npm", - "type": "node", + "type": "node" }, { "name": "CHEFS Frontend - chrome", @@ -41,7 +41,24 @@ "url": "http://localhost:5173/app", "enableContentValidation": false, "webRoot": "${workspaceFolder}/app/frontend/src", - "pathMapping": {"url": "//src/", "path": "${webRoot}/"} + "pathMapping": { "url": "//src/", "path": "${webRoot}/" } + }, + { + "type": "node", + "request": "launch", + "name": "Jest: current file", + //"env": { "NODE_ENV": "test" }, + "program": "${workspaceFolder}/app/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--config", + "${workspaceFolder}/app/jest.config.js", + "--coverage=false" + ], + "console": "integratedTerminal", + "windows": { + "program": "${workspaceFolder}/app/node_modules/jest/bin/jest" + } } ], "version": "0.2.0" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 85aa0490c..de14b394e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -62,14 +62,16 @@ { "label": "chefs_local up", "type": "shell", - "command": "docker-compose -f ${workspaceFolder}/.devcontainer/chefs_local/docker-compose.yml up -d", + "command": "docker-compose", + "args": ["-f", "${workspaceFolder}/.devcontainer/chefs_local/docker-compose.yml", "up", "-d"], "isBackground": true, "problemMatcher": [], }, { "label": "chefs_local down", "type": "shell", - "command": "docker-compose -f ${workspaceFolder}/.devcontainer/chefs_local/docker-compose.yml down", + "command": "docker-compose", + "args": ["-f", "${workspaceFolder}/.devcontainer/chefs_local/docker-compose.yml", "down"], "isBackground": true, "problemMatcher": [], }, diff --git a/app/app.js b/app/app.js index f960debc5..e2e202789 100644 --- a/app/app.js +++ b/app/app.js @@ -5,7 +5,6 @@ const path = require('path'); const Problem = require('api-problem'); const querystring = require('querystring'); -const keycloak = require('./src/components/keycloak'); const log = require('./src/components/log')(module.filename); const httpLogger = require('./src/components/log').httpLogger; const middleware = require('./src/forms/common/middleware'); @@ -40,9 +39,6 @@ if (process.env.NODE_ENV !== 'test') { app.use(httpLogger); } -// Use Keycloak OIDC Middleware -app.use(keycloak.middleware()); - // Block requests until service is ready app.use((_req, res, next) => { if (state.shutdown) { @@ -178,11 +174,16 @@ function initializeConnections() { .then((results) => { state.connections.data = results[0]; - if (state.connections.data) log.info('DataConnection Reachable', { function: 'initializeConnections' }); + if (state.connections.data) + log.info('DataConnection Reachable', { + function: 'initializeConnections', + }); }) .catch((error) => { log.error(`Initialization failed: Database OK = ${state.connections.data}`, { function: 'initializeConnections' }); - log.error('Connection initialization failure', error.message, { function: 'initializeConnections' }); + log.error('Connection initialization failure', error.message, { + function: 'initializeConnections', + }); if (!state.ready) { process.exitCode = 1; shutdown(); @@ -191,7 +192,9 @@ function initializeConnections() { .finally(() => { state.ready = Object.values(state.connections).every((x) => x); if (state.ready) { - log.info('Service ready to accept traffic', { function: 'initializeConnections' }); + log.info('Service ready to accept traffic', { + function: 'initializeConnections', + }); // Start periodic 10 second connection probe check probeId = setInterval(checkConnections, 10000); } @@ -211,7 +214,10 @@ function checkConnections() { Promise.all(tasks).then((results) => { state.connections.data = results[0]; state.ready = Object.values(state.connections).every((x) => x); - if (!wasReady && state.ready) log.info('Service ready to accept traffic', { function: 'checkConnections' }); + if (!wasReady && state.ready) + log.info('Service ready to accept traffic', { + function: 'checkConnections', + }); log.verbose(state); if (!state.ready) { process.exitCode = 1; diff --git a/app/config/custom-environment-variables.json b/app/config/custom-environment-variables.json index 00143dbae..00ba319a0 100755 --- a/app/config/custom-environment-variables.json +++ b/app/config/custom-environment-variables.json @@ -32,22 +32,24 @@ "adminDashboardUrl": "VITE_ADMIN_DASHBOARD_URL", "apiPath": "FRONTEND_APIPATH", "basePath": "VITE_FRONTEND_BASEPATH", - "keycloak": { - "clientId": "FRONTEND_KC_CLIENTID", - "realm": "FRONTEND_KC_REALM", - "serverUrl": "FRONTEND_KC_SERVERURL" + "oidc": { + "clientId": "OIDC_CLIENTID", + "realm": "OIDC_REALM", + "serverUrl": "OIDC_SERVERURL", + "logoutUrl": "OIDC_LOGOUTURL" } }, "server": { "apiPath": "SERVER_APIPATH", "basePath": "SERVER_BASEPATH", "bodyLimit": "SERVER_BODYLIMIT", - "keycloak": { - "clientId": "SERVER_KC_CLIENTID", - "clientSecret": "SERVER_KC_CLIENTSECRET", - "publicKey": "SERVER_KC_PUBLICKEY", - "realm": "SERVER_KC_REALM", - "serverUrl": "SERVER_KC_SERVERURL" + "oidc": { + "realm": "OIDC_REALM", + "serverUrl": "OIDC_SERVERURL", + "jwksUri": "OIDC_JWKSURI", + "issuer": "OIDC_ISSUER", + "audience": "OIDC_CLIENTID", + "maxTokenAge": "OIDC_MAXTOKENAGE" }, "logFile": "SERVER_LOGFILE", "logLevel": "SERVER_LOGLEVEL", diff --git a/app/config/default.json b/app/config/default.json index 3fadfbf56..31d28ee3f 100644 --- a/app/config/default.json +++ b/app/config/default.json @@ -30,21 +30,25 @@ "adminDashboardUrl": "", "apiPath": "api/v1", "basePath": "/app", - "keycloak": { - "clientId": "chefs-frontend", - "realm": "chefs", - "serverUrl": "https://dev.loginproxy.gov.bc.ca/auth" + "oidc": { + "clientId": "chefs-frontend-localhost-5300", + "realm": "standard", + "serverUrl": "https://dev.loginproxy.gov.bc.ca/auth", + "logoutUrl": "https://logon7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl=https%3A%2F%2Fdev.loginproxy.gov.bc.ca%2Fauth%2Frealms%2Fstandard%2Fprotocol%2Fopenid-connect%2Flogout" } }, "server": { "apiPath": "/api/v1", "basePath": "/app", "bodyLimit": "30mb", - "keycloak": { - "clientId": "chefs", - "realm": "chefs", - "serverUrl": "https://dev.loginproxy.gov.bc.ca/auth" - }, + "oidc": { + "realm": "standard", + "serverUrl": "https://dev.loginproxy.gov.bc.ca/auth", + "jwksUri": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect/certs", + "issuer": "https://dev.loginproxy.gov.bc.ca/auth/realms/standard", + "audience": "chefs-frontend-localhost-5300", + "maxTokenAge": "300" + }, "logLevel": "http", "port": "8080", "rateLimit": { diff --git a/app/config/test.json b/app/config/test.json index 23510c2e3..4382649e5 100755 --- a/app/config/test.json +++ b/app/config/test.json @@ -26,7 +26,7 @@ }, "server": { "emailRecipients": "foo@bar.com,baz@boo.com", - "keycloak": { + "oidc": { "clientSecret": "password" }, "logLevel": "silent" diff --git a/app/frontend/src/components/admin/AdministerUser.vue b/app/frontend/src/components/admin/AdministerUser.vue index f91cf4a2b..cd47ebf15 100644 --- a/app/frontend/src/components/admin/AdministerUser.vue +++ b/app/frontend/src/components/admin/AdministerUser.vue @@ -16,9 +16,6 @@ export default { ...mapState(useAppStore, ['config']), ...mapState(useAdminStore, ['user']), ...mapState(useFormStore, ['lang']), - userUrl() { - return `${this.config.keycloak.serverUrl}/admin/${this.config.keycloak.realm}/console/#/realms/${this.config.keycloak.realm}/users/${this.user.keycloakId}`; - }, }, async mounted() { await this.readUser(this.userId); @@ -34,15 +31,5 @@ export default {

{{ user.fullName }}

{{ $t('trans.administerUser.userDetails') }}

{{ user }}
- - - {{ $t('trans.administerUser.openSSOConsole') }} - diff --git a/app/frontend/src/components/base/BaseSecure.vue b/app/frontend/src/components/base/BaseSecure.vue index 0a27df9e3..d75aa7761 100755 --- a/app/frontend/src/components/base/BaseSecure.vue +++ b/app/frontend/src/components/base/BaseSecure.vue @@ -2,6 +2,7 @@ import { mapActions, mapState } from 'pinia'; import { useAuthStore } from '~/store/auth'; import { useFormStore } from '~/store/form'; +import { useIdpStore } from '~/store/identityProviders'; export default { props: { @@ -9,8 +10,8 @@ export default { type: Boolean, default: false, }, - idp: { - type: Array, + permission: { + type: String, default: undefined, }, }, @@ -19,10 +20,10 @@ export default { 'authenticated', 'identityProvider', 'isAdmin', - 'isUser', 'ready', ]), ...mapState(useFormStore, ['lang']), + ...mapState(useIdpStore, ['hasPermission']), mailToLink() { return `mailto:${ import.meta.env.VITE_CONTACT @@ -34,54 +35,40 @@ export default { return import.meta.env.VITE_CONTACT; }, }, - methods: mapActions(useAuthStore, ['login']), + methods: { + ...mapActions(useAuthStore, ['login']), + }, };