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`} />