diff --git a/astro/package-lock.json b/astro/package-lock.json
index 4e39375da5..05b3de2387 100644
--- a/astro/package-lock.json
+++ b/astro/package-lock.json
@@ -19,6 +19,7 @@
"astro": "^2.0.15",
"astro-compress": "^2.0.8",
"astro-index-pages": "src/integrations/astro-index-pages",
+ "jsonpath-plus": "^7.2.0",
"marked": "^7.0.1",
"mermaid": "^9.1.6",
"pagefind": "^0.12.0",
@@ -4378,6 +4379,14 @@
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz",
"integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="
},
+ "node_modules/jsonpath-plus": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz",
+ "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/khroma": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/khroma/-/khroma-2.0.0.tgz",
diff --git a/astro/package.json b/astro/package.json
index 72918ee022..ac6a060ffb 100644
--- a/astro/package.json
+++ b/astro/package.json
@@ -22,6 +22,7 @@
"astro": "^2.0.15",
"astro-compress": "^2.0.8",
"astro-index-pages": "src/integrations/astro-index-pages",
+ "jsonpath-plus": "^7.2.0",
"marked": "^7.0.1",
"mermaid": "^9.1.6",
"pagefind": "^0.12.0",
diff --git a/astro/src/components/RemoteValue/RemoteValue.astro b/astro/src/components/RemoteValue/RemoteValue.astro
new file mode 100644
index 0000000000..e552b22960
--- /dev/null
+++ b/astro/src/components/RemoteValue/RemoteValue.astro
@@ -0,0 +1,107 @@
+---
+import parse, {Parser, SelectorFunction} from './parsers';
+import Code from 'astro/components/Code.astro';
+export {Parser} from './parsers';
+
+/**
+ * Component to fetch values from remote files, optionally passing a selector to extract a value from the file
+ * (e.g. a JSON object).
+ *
+ * Import:
+ * import RemoteValue from 'path/to/components/RemoteValue/RemoteValue.astro';
+ *
+ * Usage syntax:
+ *
+ *
+ * Props:
+ * url: remote URL to fetch the file from
+ * parser: value from the Enum exported by RemoteValue.astro. If not defined, the component will use the file extension
+ * from the URL.
+ * selector: parsed-dependent syntax to extract the value from the remote file. It can also be a callback function that
+ * will be called with the entire file as an argument and should return the value to extract.
+ * E.g. for JSON, we use jsonpath-plus to provide an XPath-based syntax.
+ * defaultValue: default value if we cannot retrieve the element (otherwise, we'll render "not available").
+ * This is recommended in case you ever change the remote file and the selector doesn't work anymore.
+ *
+ * These three examples return the same "clientSecret" element:
+ *
+ *
+ * element.requests.find(e => e.url === '/api/application/#{applicationId}').body.application.oauthConfiguration.clientSecret} />
+ *
+ * If you need to render the value inside a component, you need to pass a `codeRenderer` callback function:
+ * `OP_SECRET_KEY="${value}" bundle exec rails s`} />
+ * You can also pass a `codeLang` prop in case the element doesn't detect it automatically.
+ */
+
+/**
+ * Callback to be used to render elements
+ */
+type CodeRendererFunc = (element: any) => string;
+
+/**
+ * Available props.
+ */
+export interface Props {
+ /**
+ * Url to fetch code from.
+ */
+ url: string;
+
+ /**
+ * Selector to extract the value from the content.
+ * This is parser-specific but is normally some kind of XPath.
+ * E.g. JSONPath syntax for JSON objects (https://www.npmjs.com/package/jsonpath-plus)
+ */
+ selector: string | SelectorFunction;
+
+ /**
+ * Optional parser name. If no one was informed, we'll use the file extension.
+ */
+ parser?: Parser;
+
+ /**
+ * Default value if there's an error retrieving file.
+ */
+ default?: string;
+
+ /**
+ * Callback to be used to render elements.
+ */
+ codeRenderer: CodeRendererFunc;
+
+ /**
+ * Optional `lang` attribute that will be passed to the component.
+ */
+ codeLang: string;
+}
+
+// Extracting props
+const {url, selector, parser, default: defaultValue, codeRenderer, codeLang} = Astro.props as Props;
+
+// Value that will be rendered
+let value;
+
+try {
+ const response = await fetch(url);
+ const content = await response.text();
+
+ // Using the selector to look up the value
+ if ((content) && (selector)) {
+ value = parse(url, content, selector, parser);
+ }
+} catch (e) {
+ console.error(`Error retrieving remote value from ${selector} at ${url}`, e);
+}
+
+// Default value
+if (!value) {
+ value = defaultValue || "not available";
+}
+---
+{(codeRenderer) ? : value}
diff --git a/astro/src/components/RemoteValue/parsers.ts b/astro/src/components/RemoteValue/parsers.ts
new file mode 100644
index 0000000000..cf33c7aecf
--- /dev/null
+++ b/astro/src/components/RemoteValue/parsers.ts
@@ -0,0 +1,77 @@
+import {JSONPath} from 'jsonpath-plus';
+
+/**
+ * Common cache object used by parsers.
+ */
+const CACHE: { [key: string]: any } = {};
+
+/**
+ * Available values for the optional syntax prop.
+ */
+export enum Parser {
+ JSON = 'json',
+}
+
+/**
+ * Custom function to look up the value.
+ */
+export type SelectorFunction = (element: object) => string;
+
+
+/**
+ * JSON Parser
+ *
+ * @param {String} url URL used as a key for the caching system
+ * @param {String} code Actual code to be parsed
+ * @param {String|SelectorFunction} selectorOrFunction A JSONPath selector or a custom function to look up the value.
+ */
+function jsonParser(url: string, code: string, selectorOrFunction: string | SelectorFunction): string | null {
+ // Caching JSON objects
+ if (typeof CACHE[url] === 'undefined') {
+ try {
+ CACHE[url] = JSON.parse(code);
+ } catch (err: any) {
+ return null;
+ }
+ }
+
+ // Strings are treated as JSONPath selector
+ if (typeof selectorOrFunction === 'string') {
+ const result = JSONPath({
+ path: selectorOrFunction,
+ json: CACHE[url],
+ });
+ return ((result) && (result[0])) ? result[0] : null;
+ }
+
+ // If this is a function, we call it passing the JSON object as argument
+ return selectorOrFunction(CACHE[url]);
+}
+
+/**
+ * Parsing content, optionally looking up the value via a selector
+ *
+ * @param {String} url URL used as a key for the caching system
+ * @param {String} content Actual code to be parsed
+ * @param {String|SelectorFunction} selector String or a custom function
+ * @param {String|undefined} parser Optional parser (will detect from the filename extension)
+ * @return {String|null}
+ */
+export default function parse(
+ url: string,
+ content: string,
+ selector: string | SelectorFunction,
+ parser?: string
+): string | null {
+ // If no parser was informed, we use the file extension
+ if (!parser) {
+ parser = url.substring(url.lastIndexOf('.') + 1);
+ }
+
+ switch (parser.toLowerCase()) {
+ case Parser.JSON:
+ return jsonParser(url, content, selector);
+ }
+
+ return null;
+}
diff --git a/astro/src/content/quickstarts/quickstart-ruby-rails-web.mdx b/astro/src/content/quickstarts/quickstart-ruby-rails-web.mdx
index 6aa78ddd60..9728d8ccbd 100644
--- a/astro/src/content/quickstarts/quickstart-ruby-rails-web.mdx
+++ b/astro/src/content/quickstarts/quickstart-ruby-rails-web.mdx
@@ -15,6 +15,7 @@ import Aside from '../../components/Aside.astro';
import LoginAfter from '../../diagrams/quickstarts/login-after.astro';
import LoginBefore from '../../diagrams/quickstarts/login-before.astro';
import RemoteCode from '../../components/RemoteCode.astro';
+import RemoteValue from '../../components/RemoteValue/RemoteValue.astro';
In this quickstart you are going to build an application with Ruby on Rails and integrate it with FusionAuth.
You'll be building it for [ChangeBank](https://www.youtube.com/watch?v=CXDxNCzUspM), a global leader in converting dollars into coins.
@@ -74,10 +75,10 @@ If you ever want to reset the FusionAuth system, delete the volumes created by d
FusionAuth will be initially configured with these settings:
-* Your client Id is `e9fdb985-9173-4e01-9d73-ac2d60d1dc8e`.
-* Your client secret is `super-secret-secret-that-should-be-regenerated-for-production`.
-* Your example username is `richard@example.com` and the password is `password`.
-* Your admin username is `admin@example.com` and the password is `password`.
+* Your client Id is
.
+* Your client secret is
.
+* Your example username is
and the password is
.
+* Your admin username is
and the password is
.
* The base URL of FusionAuth `http://localhost:9011/`.
You can log into the [FusionAuth admin UI](http://localhost:9011/admin) and look around if you want, but with Docker/Kickstart you don't need to.
@@ -98,7 +99,7 @@ We'll use the [OmniAuth Library](https://github.com/omniauth/omniauth), which si
Install the omniauth gem and other supporting gems. Add the following three lines to your `Gemfile`.
-
@@ -161,7 +162,7 @@ lang="ruby" />
This lets you have a nice `logout` method and also handle the callback from omniauth. The latter sets a `session` attribute with user data, which can be used by views later.
-Now, update the application controller at `app/controllers/application_controller.rb`.
+Now, update the application controller at `app/controllers/application_controller.rb`.
@@ -220,9 +221,11 @@ Once you’ve created these files, you can test the application out.
Start up the Rails application using this command:
-```shell
-OP_SECRET_KEY="super-secret-secret-that-should-be-regenerated-for-production" bundle exec rails s
-```
+ `OP_SECRET_KEY="${value}" bundle exec rails s`} />
`OP_SECRET_KEY` is the client secret, which was defined by the [FusionAuth Installation via Docker](#run-fusionauth-via-docker) step. You don't want to commit secrets like this to version control, so use an environment variable.
@@ -252,7 +255,7 @@ FusionAuth gives you the ability to customize just about everything with the use
* I get `This site can’t be reached localhost refused to connect.` when I click the Login button
-Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user, `admin@example.com` with a password of `password` at [http://localhost:9011/admin](http://localhost:9011/admin).
+Ensure FusionAuth is running in the Docker container. You should be able to login as the admin user,
with a password of
at [http://localhost:9011/admin](http://localhost:9011/admin).
* I get an error page when I click on the Login button with message of `"error_reason" : "invalid_client_id"`
@@ -271,12 +274,14 @@ This indicates that Omniauth is unable to call FusionAuth to validate the return
You can always pull down a complete running application and compare what's different.
-```
-git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web.git
+ `git clone https://github.com/FusionAuth/fusionauth-quickstart-ruby-on-rails-web.git
cd fusionauth-quickstart-ruby-on-rails-web
docker-compose up -d
cd complete-app
bundle install
-OP_SECRET_KEY=super-secret-secret-that-should-be-regenerated-for-production bundle exec rails s
-```
+OP_SECRET_KEY=${value} bundle exec rails s`} />