diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..22863b7cc --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,6 @@ +FROM mcr.microsoft.com/devcontainers/base:ubuntu +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends jq && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..46f0f29ad --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +{ + "name": "Fauxton & CouchDB", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/sshd:1": {}, + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + }, + "postCreateCommand": "./.devcontainer/postCreate.sh", + "postStartCommand": "./.devcontainer/postStart.sh", + "customizations": { + "vscode": { + "extensions": [ + "bierner.github-markdown-preview", + "bierner.markdown-mermaid", + "bpruitt-goddard.mermaid-markdown-syntax-highlighting", + "github.copilot", + "github.copilot-chat", + "ms-azuretools.vscode-docker", + "shengchen.vscode-checkstyle", + "visualstudioexptteam.vscodeintellicode", + "wix.vscode-import-cost" + ], + "settings": { + "prettier.enable": false, + "editor.formatOnSave": false + } + } + }, + "forwardPorts": [ + 8000, + 5984, + 8080, + 8090 + ] +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..fd48737d2 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,46 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ../..:/workspaces:cached + command: sleep infinity + networks: [devnet] + env_file: + - .env + depends_on: + - couchdb + + couchdb: + image: couchdb:latest + restart: unless-stopped + environment: + - COUCHDB_USER=${COUCHDB_USER} + - COUCHDB_PASSWORD=${COUCHDB_PASSWORD} + - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN} + - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} + networks: [devnet] + env_file: + - .env + + keycloak: + image: quay.io/keycloak/keycloak:latest + restart: unless-stopped + environment: + - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN} + - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} + - KC_DB=dev-file + - KC_HTTP_ENABLED=true + - KC_HOSTNAME_STRICT=false + - KC_HOSTNAME_STRICT_HTTPS=false + networks: [devnet] + env_file: + - .env + command: start-dev --http-port=8090 + profiles: + - idp + - keycloak + +networks: + devnet: diff --git a/.devcontainer/jwks2couch.mjs b/.devcontainer/jwks2couch.mjs new file mode 100644 index 000000000..a916de7ec --- /dev/null +++ b/.devcontainer/jwks2couch.mjs @@ -0,0 +1,242 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +import crypto from 'crypto'; + +// This data +const config = { + "sourceUrl": `${process.env.KEYCLOAK || 'http://localhost:8090'}/realms/empire/.well-known/openid-configuration`, + "targetUrl": `${process.env.SRV ||'http://localhost:5984'}/_node/nonode@nohost/_config/jwt_keys`, + "adminCredentials": { + "username": process.env.COUCHDB_USER || "admin", + "password": process.env.COUCHDB_PASSWORD ||"password" + } +}; + +/** + * Converts RSA JWK to proper PEM format using Node.js crypto + */ +const rsaJwkToPem = (n, e) => { + try { + // Create RSA public key from JWK components + const keyObject = crypto.createPublicKey({ + key: { + kty: 'RSA', + n: n, + e: e + }, + format: 'jwk' + }); + + // Export as PEM + return keyObject.export({ + type: 'spki', + format: 'pem' + }); + } catch (error) { + throw new Error(`Failed to convert RSA JWK to PEM: ${error.message}`); + } +}; + +/** + * Converts EC JWK to proper PEM format using Node.js crypto + */ +const ecJwkToPem = (x, y, crv) => { + try { + // Create EC public key from JWK components + const keyObject = crypto.createPublicKey({ + key: { + kty: 'EC', + x: x, + y: y, + crv: crv + }, + format: 'jwk' + }); + + // Export as PEM + return keyObject.export({ + type: 'spki', + format: 'pem' + }); + } catch (error) { + throw new Error(`Failed to convert EC JWK to PEM: ${error.message}`); + } +}; + +/** + * Creates basic auth header for CouchDB + */ +const createAuthHeader = (username, password) => { + const credentials = Buffer.from(`${username}:${password}`).toString('base64'); + return `Basic ${credentials}`; +}; + +/** + * Makes a fetch request with error handling + */ +const fetchWithErrorHandling = async (url, options = {}) => { + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'JWT-Magic-Script/1.0', + ...options.headers + }, + ...options + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return await response.json(); + } + + return await response.text(); +}; + +/** + * Main function to process JWT keys + */ +const processJwtMagic = async () => { + try { + console.log('Starting JWT Magic script...'); + + console.log(`Source URL: ${config.sourceUrl}`); + console.log(`Target URL: ${config.targetUrl}`); + + // Fetch OpenID configuration + console.log('Fetching OpenID configuration...'); + const oidcData = await fetchWithErrorHandling(config.sourceUrl); + + if (!oidcData.jwks_uri) { + throw new Error('jwks_uri not found in OpenID configuration'); + } + + console.log(`JWKS URI found: ${oidcData.jwks_uri}`); + + // Fetch JWKS data + console.log('Fetching JWKS data...'); + const jwksData = await fetchWithErrorHandling(oidcData.jwks_uri); + + if (!jwksData.keys || !Array.isArray(jwksData.keys)) { + throw new Error('Invalid JWKS response: keys array not found'); + } + + console.log(`Found ${jwksData.keys.length} keys in JWKS`); + + // Process keys with "sig" use + const sigKeys = jwksData.keys.filter(key => key.use === 'sig'); + console.log(`Found ${sigKeys.length} signing keys`); + + if (sigKeys.length === 0) { + console.log('No signing keys found. Exiting.'); + return; + } + + // Prepare auth header for CouchDB + const authHeader = createAuthHeader( + config.adminCredentials.username, + config.adminCredentials.password + ); + + // Process each signing key + for (const key of sigKeys) { + try { + console.log(`Processing key: ${key.kid || 'unknown'}`); + + if (!key.kty) { + console.log(`Skipping key ${key.kid}: missing kty (key type)`); + continue; + } + + if (!key.kid) { + console.log(`Skipping key: missing kid (key ID)`); + continue; + } + + // Convert JWK to PEM format as required by CouchDB + let pemKey; + try { + if (key.kty === 'RSA') { + if (!key.n || !key.e) { + console.log(`Skipping RSA key ${key.kid}: missing n or e components`); + continue; + } + pemKey = rsaJwkToPem(key.n, key.e); + } else if (key.kty === 'EC') { + if (!key.x || !key.y) { + console.log(`Skipping EC key ${key.kid}: missing x or y coordinates`); + continue; + } + pemKey = ecJwkToPem(key.x, key.y, key.crv); + } else { + console.log(`Skipping key ${key.kid}: unsupported key type ${key.kty}`); + continue; + } + } catch (keyConvertError) { + console.log(`Skipping key ${key.kid}: error converting to PEM - ${keyConvertError.message}`); + continue; + } + + // Construct target URL: targetUrl + "/" + lowercase(kty) + ":" + kid + const documentId = `${key.kty.toLowerCase()}:${key.kid}`; + const targetDocUrl = `${config.targetUrl.replace(/\/$/, '')}/${documentId}`; + + console.log(`Posting to: ${targetDocUrl}`); + + // Store PEM format as single line with escaped newlines for CouchDB _config + const pemSingleLine = pemKey.replace(/\n/g, '\\n'); + const jsonValue = JSON.stringify(pemSingleLine); + + console.log(`Posting PEM key (single line): ${jsonValue}`); + + // Post to CouchDB _config endpoint + const response = await fetch(targetDocUrl, { + method: 'PUT', + headers: { + 'Authorization': authHeader, + 'Content-Type': 'application/json' + }, + body: jsonValue + }); + + if (response.ok) { + const responseData = await response.json(); + console.log(`✓ Successfully posted key ${key.kid} (${response.status})`); + if (responseData.rev) { + console.log(` Document revision: ${responseData.rev}`); + } + } else { + const errorText = await response.text(); + console.log(`⚠ Error posting key ${documentId}: ${response.status}`); + console.log(` Response: ${errorText}`); + } + + } catch (keyError) { + console.error(`✗ Error processing key ${key.kid || 'unknown'}:`, keyError.message); + } + } + + console.log('JWT Magic script completed successfully!'); + + } catch (error) { + console.error('Error in JWT Magic script:', error.message); + process.exit(1); + } +}; + +// Run the script +processJwtMagic(); \ No newline at end of file diff --git a/.devcontainer/populate_couchdb.sh b/.devcontainer/populate_couchdb.sh new file mode 100755 index 000000000..0f7c0f157 --- /dev/null +++ b/.devcontainer/populate_couchdb.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Use to create CouchDB system databases and one demo database + +# Wait until CouchDB answers +until curl -fsS "http://localhost:5984/_up" >/dev/null 2>&1; do sleep 2; done + +# Variables needed +now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# Check required CouchDB environment variables +if [ -z "${COUCHDB_USER}" ] || [ -z "${COUCHDB_PASSWORD}" ]; then + echo "Error: COUCHDB_USER and COUCHDB_PASSWORD environment variables must be set" + exit 1 +fi +# Populate CouchDB +COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} +COUCHDB_PORT=5984 +SRV=http://localhost:${COUCHDB_PORT} + +echo Couch $SRV + +#SYSTEM databases +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_users +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_replicator +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_global_changes + +#DEMO database +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/demo + +# Status +curl -u "${COUCHDB_USRPWD}" ${SRV} | jq + +# Session +curl -u "${COUCHDB_USRPWD}" ${SRV}/_session | jq + +echo DONE \ No newline at end of file diff --git a/.devcontainer/populate_keycloak.sh b/.devcontainer/populate_keycloak.sh new file mode 100755 index 000000000..7d7f0d9a9 --- /dev/null +++ b/.devcontainer/populate_keycloak.sh @@ -0,0 +1,224 @@ +#!/bin/bash +# Populates Keycloak with the realm "empire" and users +# and all settings for CouchDB to trust the IdP's certs + +# Wait until CouchDB answers +until curl -fsS "http://localhost:5984/_up" >/dev/null 2>&1; do sleep 2; done +# Wait until Keycloak answers +until curl -fsS "http://localhost:8090/realms/master" >/dev/null 2>&1; do sleep 2; done + +# Variables needed +now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +REALM=empire + +# Check required CouchDB environment variables +if [ -z "${COUCHDB_USER}" ] || [ -z "${COUCHDB_PASSWORD}" ]; then + echo "Error: COUCHDB_USER and COUCHDB_PASSWORD environment variables must be set" + exit 1 +fi +# Populate CouchDB +COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} +COUCHDB_PORT=5984 +SRV=http://localhost:${COUCHDB_PORT} +KEYCLOAK=http://localhost:8090 + +echo Couch $SRV +echo keycloak $KEYCLOAK + +# Check required Keycloak environment variables +if [ -z "${KEYCLOAK_ADMIN}" ] || [ -z "${KEYCLOAK_ADMIN_PASSWORD}" ]; then + echo "Error: KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD environment variables must be set" + exit 1 +fi + +# Populate Keycloak +curl ${KEYCLOAK} + +echo Admin login +KEYCLOAK_ACCESS_TOKEN=$(curl -X POST ${KEYCLOAK}/realms/master/protocol/openid-connect/token \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data client_id=admin-cli \ + --data "username=${KEYCLOAK_ADMIN}" \ + --data "password=${KEYCLOAK_ADMIN_PASSWORD}" \ + --data grant_type=password \ + --silent | jq -r '.access_token') + +echo Create realm +curl -X POST ${KEYCLOAK}/admin/realms \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header "content-type: application/json" \ + --data '{ + "id": "'"${REALM}"'", + "realm": "'"${REALM}"'", + "displayName": "The mighty realm of '"${REALM}"'", + "enabled": true, + "sslRequired": "NONE", + "registrationAllowed": true, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": true, + "bruteForceProtected": true +}' + +echo Create client fauxton +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/clients \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "clientId": "fauxton", + "name" : "CouchDB Fauxton", + "enabled": true, + "publicClient": true, + "directAccessGrantsEnabled": true, + "redirectUris":["http://localhost:8000","http://localhost:8000/"], + "webOrigins": ["localhost:8000"], + "protocolMappers": [ + { + "name": "email to sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "sub", + "jsonType.label": "String" + } + } + ] +}' + +echo Create role _admin +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "name":"_admin", + "description":"Full access" +}' + +echo retrive id for _admin +ROLE_ADMIN=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?search=_admin" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') +echo ADMIN_ID $ROLE_ADMIN + +echo create role role1 +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "name":"role1", + "description":"Member of role1" +}' + +echo retrieve id for role1 +ROLE_ROLE1=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?search=role1" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Create user Hari Seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "requiredActions": [], + "username": "hariseldon", + "enabled": true, + "firstName": "Hari", + "lastName": "Seldon", + "email": "hari.seldon@terminus.empire", + "emailVerified": true, + "credentials": [ + { + "type": "password", + "value": "password", + "temporary": false + } + ] +}' + +echo retrieve ID for user harisedon +USER_SELDON=$(curl "${KEYCLOAK}/admin/realms/${REALM}/users?username=hariseldon" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Create user Gaal Dornick +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "requiredActions": [], + "username": "gaaldornick", + "enabled": true, + "firstName": "Gaal", + "lastName": "Dornick", + "email": "gaal.dornick@terminus.empire", + "emailVerified": true, + "credentials": [ + { + "type": "password", + "value": "password", + "temporary": false + } + ] +}' + +echo retrive ID for user gaaldornick +USER_DORNICK=$(curl "${KEYCLOAK}/admin/realms/${REALM}/users?username=gaaldornick" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Assign role _admin to seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_SELDON}"/role-mappings/realm/"${ROLE_ADMIN}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ADMIN}\",\"name\":\"_admin\"}]" + +echo Assign role role1 to seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_SELDON}"/role-mappings/realm/"${ROLE_ROLE1}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ROLE1}\",\"name\":\"role1\"}]" + +echo Assign role role1 to gaaldornick +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_DORNICK}"/role-mappings/realm/"${ROLE_ROLE1}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ROLE1}\",\"name\":\"role1\"}]" + +echo enable CouchDB JWT login +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/chttpd/authentication_handlers" \ +-H "Content-Type: text/plain" \ +-d '"{chttpd_auth, cookie_authentication_handler}, {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, default_authentication_handler}"' + +echo point to idp_host +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/jwt_auth/idp_host" \ + --header 'content-type: text/plain' \ + --data "\"${KEYCLOAK}/realms/${REALM}\"" + +echo set require exp,iat +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/jwt_auth/required_claims" \ + --header 'content-type: text/plain' \ + --data "\"exp,iat\"" + +echo ADD PUblic key +node .devcontainer/jwks2couch.mjs || { + echo "jwks2couch failed" >&2 + exit 1 +} + +echo Set path for role resolution +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path" \ +-H "Content-Type: text/plain" \ +-d "\"realm_access.roles\"" + +echo Restart CouchDB +curl -u "${COUCHDB_USRPWD}" -X POST "${SRV}/_node/_local/_restart" + +echo DONE \ No newline at end of file diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100755 index 000000000..431c2ad70 --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Runs after dev container creation +npm install +echo DONE \ No newline at end of file diff --git a/.devcontainer/postStart.sh b/.devcontainer/postStart.sh new file mode 100755 index 000000000..0c121fe4f --- /dev/null +++ b/.devcontainer/postStart.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Runs after dev container startup +# Wait until CouchDB answers +until curl -fsS "http://localhost:5984/_up" >/dev/null 2>&1; do sleep 2; done + +now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} +COUCHDB_PORT=5984 +SRV=http://localhost:${COUCHDB_PORT} + +curl -u "${COUCHDB_USRPWD}" -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postStart\"}" | jq + + +npm run dev \ No newline at end of file diff --git a/.gitignore b/.gitignore index 96b40103b..d6541a671 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ coverage # IDEs .idea/ .vscode +.quarkus + +# Env variables +.env diff --git a/readme.md b/readme.md index bad0cb160..c43e0ef89 100644 --- a/readme.md +++ b/readme.md @@ -16,20 +16,22 @@ See `fauxton --help` for extra options. ## Setting up Fauxton +(alternative see below: Running Fauxton in a devcontainer) + Please note that [node.js](http://nodejs.org/) and npm is required. Specifically, Fauxton requires at least Node 6 and npm 3. 1. Fork this repo (see [GitHub help](https://help.github.com/articles/fork-a-repo/) for details) 1. Clone your fork: `git clone https://github.com/YOUR-USERNAME/couchdb-fauxton.git` 1. Go to your cloned copy: `cd couchdb-fauxton` -1. Set up the upstream repo: +1. Set up the upstream repo: * `git remote add upstream https://github.com/apache/couchdb-fauxton.git` * `git fetch upstream` * `git branch --set-upstream-to=upstream/main main` 1. Download all dependencies: `npm install` 1. Make sure you have CouchDB installed. - Option 1 (**recommended**): Use `npm run docker:up` to start a Docker container running CouchDB with user `tester` and password `testerpass`. - - You need to have [Docker](https://docs.docker.com/engine/installation/) installed to use this option. - - Option 2: Follow instructions + - You need to have [Docker](https://docs.docker.com/engine/installation/) installed to use this option. + - Option 2: Follow instructions [found here](http://couchdb.readthedocs.org/en/latest/install/index.html) @@ -52,7 +54,7 @@ You should be able to access Fauxton at `http://localhost:8000` ### Preparing a Fauxton Release -Follow the "Setting up Fauxton" section above, then edit the `settings.json` variable root where the document will live, +Follow the "Setting up Fauxton" section above, then edit the `settings.json` variable root where the document will live, e.g. `/_utils/`. Then type: ``` @@ -82,9 +84,63 @@ part of the deployable release artifact. # Or fully compiled install npm run couchdb +## Running Fauxton in a devcontainer + +This repository contains a folder `.devcontainer` that hold a container definition following the [devcontainer standard](https://containers.dev). + +It allows to start Fauxton and CouchDB together with a predefinded configuration. It also optionally runs a Keycloak instance to be able to test (a future) IdP integration. The instances are ephidermal, so rebuilding containers (see below) gets you back to a defined pristine state. + +Using the devcontainer is your choice and optional. + +### Prerequisites + +- a container runtime installed: Docker desktop, Rancher deskop, Orbstack etc. +- a compatible Ide: VS-Code, IntelliJ etc +- a file `.devcontainer/.env` (not version controlled) + +```env +# CouchDB +COUCHDB_USER=admin +COUCHDB_PASSWORD=password + +# Keycloak +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=password +``` + +Follow the instructions of your Ide to build and start the container. In VS-Code select "Dev Containers: Rebuild Container". + +As result your container gets build and you have access to thses URLs: + +- http://localhost:8000 The Fauxton UI +- http://localhost:5984 The CouchDB + +### Running the devcontainer with keycloak + +Since the keycloak IdP is not nescesary unless you want to use JWT related operations, it doesn't automatically start. You have to use a terminal/commandline after you started the dev container. + + +```bash +cd .devcontainer +docker compose --profile idp up +``` + +You then gain an additional endpoint: + +- http://localhost:8090 The Keycloak IdP + +### Configure CouchDB and Keycloak + +The `.devcontainer` folder contains helper scripts you can use to configure CouchDB and Keycloak. Since you might have different ideas how the environment should be configured, the scripts don't run automatically, but need to be called from a terminal inside the devcontainer. + +* `populate_couchdb.sh`: create databases `_users`, `_relicator`, `_global_changes` and `demo` +* `populate_keycloak.sh`: configure the keycloak server with the realm `empire`, a client `fauxton` and the users `hariseldon`, `gaaldormick`. Furthermore extract the public key from the JWKS, convert it to PEM and configure JWT authentication in CouchDB including trusting that key + +### Resetting the containers +Both side containers (CouchDB, keycloak) don't use volumes to store their data, deleting the container will trigger the rebuild with a clean slate. See [`docker compose down`](https://docs.docker.com/reference/cli/docker/compose/down/) -## More information +## More information Check out the following pages for a lot more information about Fauxton: