Skip to content

Commit

Permalink
feat(console): add Ruby app guide
Browse files Browse the repository at this point in the history
  • Loading branch information
gao-sun committed Jun 18, 2024
1 parent d210f4f commit ead51e5
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .changeset/large-gifts-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@logto/console": minor
---

add Ruby app guide
3 changes: 1 addition & 2 deletions packages/console/src/assets/docs/guides/generate-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ const data = await Promise.all(
return;
}

// Add `.png` later
const logo = ['logo.svg'].find((logo) => existsSync(`${directory}/${logo}`));
const logo = ['logo.webp', 'logo.svg', 'logo.png'].find((logo) => existsSync(`${directory}/${logo}`));

const config = existsSync(`${directory}/config.json`)
? await import(`./${directory}/config.json`, { assert: { type: 'json' } }).then(
Expand Down
81 changes: 44 additions & 37 deletions packages/console/src/assets/docs/guides/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,50 @@

import { lazy } from 'react';

import apiExpress from './api-express/index';
import apiPython from './api-python/index';
import apiSpringBoot from './api-spring-boot/index';
import m2mGeneral from './m2m-general/index';
import nativeAndroid from './native-android/index';
import nativeCapacitor from './native-capacitor/index';
import { type Guide } from './types';
import webNextAppRouter from './web-next-app-router/index';
import nativeExpo from './native-expo/index';
import nativeFlutter from './native-flutter/index';
import nativeIosSwift from './native-ios-swift/index';
import spaAngular from './spa-angular/index';
import spaReact from './spa-react/index';
import spaVanilla from './spa-vanilla/index';
import spaVue from './spa-vue/index';
import spaWebflow from './spa-webflow/index';
import thirdPartyOidc from './third-party-oidc/index';
import { type Guide } from './types';
import webDotnetCore from './web-dotnet-core/index';
import webDotnetCoreBlazorServer from './web-dotnet-core-blazor-server/index';
import webDotnetCoreBlazorWasm from './web-dotnet-core-blazor-wasm/index';
import webDotnetCoreMvc from './web-dotnet-core-mvc/index';
import m2mGeneral from './m2m-general/index';
import webExpress from './web-express/index';
import webGo from './web-go/index';
import webGptPlugin from './web-gpt-plugin/index';
import webJavaSpringBoot from './web-java-spring-boot/index';
import webNext from './web-next/index';
import webNextAppRouter from './web-next-app-router/index';
import webSveltekit from './web-sveltekit/index';
import webGo from './web-go/index';
import webNextAuth from './web-next-auth/index';
import webJavaSpringBoot from './web-java-spring-boot/index';
import webGptPlugin from './web-gpt-plugin/index';
import spaVue from './spa-vue/index';
import nativeIosSwift from './native-ios-swift/index';
import nativeAndroid from './native-android/index';
import spaVanilla from './spa-vanilla/index';
import webNuxt from './web-nuxt/index';
import webOutline from './web-outline/index';
import webPhp from './web-php/index';
import webRuby from './web-ruby/index';
import spaWebflow from './spa-webflow/index';
import webWordpress from './web-wordpress/index';
import webPython from './web-python/index';
import nativeCapacitor from './native-capacitor/index';
import webRemix from './web-remix/index';
import webSveltekit from './web-sveltekit/index';
import webWordpress from './web-wordpress/index';
import nativeFlutter from './native-flutter/index';
import webDotnetCore from './web-dotnet-core/index';
import webDotnetCoreMvc from './web-dotnet-core-mvc/index';
import webDotnetCoreBlazorServer from './web-dotnet-core-blazor-server/index';
import webDotnetCoreBlazorWasm from './web-dotnet-core-blazor-wasm/index';
import webOutline from './web-outline/index';
import apiExpress from './api-express/index';
import apiPython from './api-python/index';
import apiSpringBoot from './api-spring-boot/index';
import thirdPartyOidc from './third-party-oidc/index';

const guides: Readonly<Guide[]> = Object.freeze([
{
order: 1,
id: 'web-next-app-router',
Logo: lazy(async () => import('./web-next-app-router/logo.svg')),
Component: lazy(async () => import('./web-next-app-router/README.mdx')),
metadata: webNextAppRouter,
},
{
order: 1.1,
id: 'native-expo',
Expand All @@ -59,13 +67,6 @@ const guides: Readonly<Guide[]> = Object.freeze([
Component: lazy(async () => import('./spa-react/README.mdx')),
metadata: spaReact,
},
{
order: 1.1,
id: 'web-next-app-router',
Logo: lazy(async () => import('./web-next-app-router/logo.svg')),
Component: lazy(async () => import('./web-next-app-router/README.mdx')),
metadata: webNextAppRouter,
},
{
order: 1.2,
id: 'm2m-general',
Expand Down Expand Up @@ -164,6 +165,13 @@ const guides: Readonly<Guide[]> = Object.freeze([
Component: lazy(async () => import('./web-php/README.mdx')),
metadata: webPhp,
},
{
order: 2,
id: 'web-ruby',
Logo: lazy(async () => import('./web-ruby/logo.webp')),
Component: lazy(async () => import('./web-ruby/README.mdx')),
metadata: webRuby,
},
{
order: 2.1,
id: 'spa-webflow',
Expand Down Expand Up @@ -242,33 +250,32 @@ const guides: Readonly<Guide[]> = Object.freeze([
metadata: webOutline,
},
{
order: Number.POSITIVE_INFINITY,
order: Infinity,
id: 'api-express',
Logo: lazy(async () => import('./api-express/logo.svg')),
Component: lazy(async () => import('./api-express/README.mdx')),
metadata: apiExpress,
},
{
order: Number.POSITIVE_INFINITY,
order: Infinity,
id: 'api-python',
Logo: lazy(async () => import('./api-python/logo.svg')),
Component: lazy(async () => import('./api-python/README.mdx')),
metadata: apiPython,
},
{
order: Number.POSITIVE_INFINITY,
order: Infinity,
id: 'api-spring-boot',
Logo: lazy(async () => import('./api-spring-boot/logo.svg')),
Component: lazy(async () => import('./api-spring-boot/README.mdx')),
metadata: apiSpringBoot,
},
{
order: Number.POSITIVE_INFINITY,
order: Infinity,
id: 'third-party-oidc',
Logo: lazy(async () => import('./third-party-oidc/logo.svg')),
Component: lazy(async () => import('./third-party-oidc/README.mdx')),
metadata: thirdPartyOidc,
},
]);
},]);

export default guides;
193 changes: 193 additions & 0 deletions packages/console/src/assets/docs/guides/web-ruby/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import UriInputField from '@/mdx-components/UriInputField';
import InlineNotification from '@/ds-components/InlineNotification';
import { generateStandardSecret } from '@logto/shared/universal';
import Steps from '@/mdx-components/Steps';
import Step from '@/mdx-components/Step';

<Steps>

<Step
title="Add Logto SDK as a dependency"
subtitle="Use your preferred method of adding gems"
>

```bash
bundle add logto
```

Or whatever your preferred method of adding gems is.

</Step>

<Step
title="Initialize Logto client"
subtitle="1 step"
>

<InlineNotification>

The following demonstration is for Ruby on Rails. However, you can apply the same steps to other Ruby frameworks.

</InlineNotification>

In the file where you want to initialize the Logto client (e.g. a base controller or a middleware), add the following code:

<pre>
<code className="language-ruby">
{`require "logto/client"
@client = LogtoClient.new(
config: LogtoClient::Config.new(
endpoint: "${props.endpoint}",
app_id: "${props.app.id}",
app_secret: "${props.app.secret}"
),
navigate: ->(uri) { a_redirect_method(uri) },
storage: LogtoClient::SessionStorage.new(the_session_object)
)
end`}
</code>
</pre>

For instance, in a Rails controller, the code might look like this:

<pre>
<code className="language-ruby">
{`# app/controllers/sample_controller.rb
require "logto/client"
class SampleController < ApplicationController
before_action :initialize_logto_client
private
def initialize_logto_client
@client = LogtoClient.new(
config: LogtoClient::Config.new(
endpoint: "${props.endpoint}",
app_id: "${props.app.id}",
app_secret: "${props.app.secret}"
),
# Allow the client to redirect to other hosts (i.e. your Logto tenant)
navigate: ->(uri) { redirect_to(uri, allow_other_host: true) },
# Controller has access to the session object
storage: LogtoClient::SessionStorage.new(session)
)
end
end`}
</code>
</pre>

</Step>

<Step
title="Configure redirect URIs"
subtitle="2 URIs"
>

First, let's enter your redirect URI. E.g. `http://localhost:3000/callback`. [Redirect URI](https://www.oauth.com/oauth2-servers/redirect-uris/) is an OAuth 2.0 concept which implies the location should redirect after authentication.

<UriInputField name="redirectUris" />

After signing out, it'll be great to redirect user back to your website. For example, add `http://localhost:3000` as the post sign-out redirect URI below.

<UriInputField name="postLogoutRedirectUris" />

</Step>

<Step
title="Handle the callback"
subtitle="1 step"
>

<p>
Since the redirect URI has been set to <code>{props.redirectUris[0] || 'http://localhost:3000/callback'}</code>, it needs to be handled it in our application. In a Rails controller, you can add the following code:
</p>

<pre>
<code className="language-ruby">
{`# app/controllers/sample_controller.rb
class SampleController < ApplicationController
def ${props.redirectUris[0]?.split('/').pop() || 'callback'}
@client.handle_sign_in_callback(url: request.original_url)
end
end`}
</code>
</pre>

And configure the route in `config/routes.rb`:

<pre>
<code className="language-ruby">
{`Rails.application.routes.draw do
get "${new URL(props.redirectUris[0] || 'http://localhost:3000/callback').pathname}", to: "sample#${props.redirectUris[0]?.split('/').pop() || 'callback'}"
end`}
</code>
</pre>

</Step>

<Step
title="Invoke sign-in and sign-out"
>

There are various ways to invoke sign-in and sign-out in your application. For example, you can implement two routes in your Rails application:

<pre>
<code className="language-ruby">
{`# app/controllers/sample_controller.rb
class SampleController < ApplicationController
def sign_in
@client.sign_in(redirect_uri: request.base_url + "${new URL(props.redirectUris[0] || 'http://localhost:3000/callback').pathname}")
end
def sign_out
@client.sign_out(post_logout_redirect_uri: request.base_url)
end
# ...
end`}
</code>
</pre>

```ruby
# config/routes.rb
Rails.application.routes.draw do
get "/sign_in", to: "sample#sign_in"
get "/sign_out", to: "sample#sign_out"

# ...
end
```

Then you can create buttons or links in your views to trigger these actions. For example:

```erb
<!-- app/views/sample/index.html.erb -->
<% if @client.is_authenticated? %>
<a href="<%= sign_out_path %>">Sign out</a>
<% else %>
<a href="<%= sign_in_path %>">Sign in</a>
<% end %>
```

</Step>

<Step title="Display user information">

To display the user's information, you can use the `@client.id_token_claims` method. For example, in a view:

```erb
<!-- app/views/sample/index.html.erb -->
<% if @client.is_authenticated? %>
<p>Welcome, <%= @client.id_token_claims["name"] %></p>
<% else %>
<p>Please sign in</p>
<% end %>
```

Please refer to the `#id_token_claims` method in the [gemdocs](https://gemdocs.org/gems/logto/latest) for more information.

</Step>

</Steps>
3 changes: 3 additions & 0 deletions packages/console/src/assets/docs/guides/web-ruby/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"order": 2
}
12 changes: 12 additions & 0 deletions packages/console/src/assets/docs/guides/web-ruby/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApplicationType } from '@logto/schemas';

import { type GuideMetadata } from '../types';

const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Ruby',
description:
'Ruby is a dynamic, open-source programming language with a focus on simplicity and productivity.',
target: ApplicationType.Traditional,
});

export default metadata;
Binary file not shown.

0 comments on commit ead51e5

Please sign in to comment.