Skip to content

Commit

Permalink
Merge pull request #6046 from logto-io/gao-add-ruby
Browse files Browse the repository at this point in the history
  • Loading branch information
gao-sun authored Jun 19, 2024
2 parents 061a30a + 282f091 commit 82e702f
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 23 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
24 changes: 17 additions & 7 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 All @@ -42,20 +41,31 @@ const metadata = data
.sort((a, b) => a.order - b.order);

const camelCase = (value) => value.replaceAll(/-./g, (x) => x[1].toUpperCase());
const filename = 'index.ts';
const filename = 'index.tsx';

await fs.writeFile(
filename,
"// This is a generated file, don't update manually.\n\nimport { lazy } from 'react';\n\nimport { type Guide } from './types';\n"
);

for (const { name } of metadata) {
for (const { name, logo } of metadata) {
// eslint-disable-next-line no-await-in-loop
await fs.appendFile(filename, `import ${camelCase(name)} from './${name}/index';\n`);

if (logo && !logo.endsWith('.svg')) {
// eslint-disable-next-line no-await-in-loop
await fs.appendFile(filename, `import ${camelCase(name)}Logo from './${name}/${logo}';\n`);
}
}

await fs.appendFile(filename, '\n');
await fs.appendFile(filename, 'const guides: Readonly<Guide[]> = Object.freeze([');
await fs.appendFile(filename, 'export const guides: Readonly<Guide[]> = Object.freeze([');

const getLogo = ({ name, logo }) => {
if (!logo) return 'undefined';
if (logo.endsWith('.svg')) return `lazy(async () => import('./${name}/${logo}'))`;
return `({ className }: { readonly className?: string }) => <img src={${camelCase(name)}Logo} alt="${name}" className={className} />`;
};

for (const { name, logo, order } of metadata) {
// eslint-disable-next-line no-await-in-loop
Expand All @@ -65,11 +75,11 @@ for (const { name, logo, order } of metadata) {
{
order: ${order},
id: '${name}',
Logo: ${logo ? `lazy(async () => import('./${name}/${logo}'))` : 'undefined'},
Logo: ${getLogo({ name, logo })},
Component: lazy(async () => import('./${name}/README.mdx')),
metadata: ${camelCase(name)},
},`
);
}

await fs.appendFile(filename, ']);\n\nexport default guides;\n');
await fs.appendFile(filename, ']);\n');
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,19 @@ import webOutline from './web-outline/index';
import webPhp from './web-php/index';
import webPython from './web-python/index';
import webRemix from './web-remix/index';
import webRuby from './web-ruby/index';
import webRubyLogo from './web-ruby/logo.webp';
import webSveltekit from './web-sveltekit/index';
import webWordpress from './web-wordpress/index';

const guides: Readonly<Guide[]> = Object.freeze([
export 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 +68,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 +166,15 @@ const guides: Readonly<Guide[]> = Object.freeze([
Component: lazy(async () => import('./web-php/README.mdx')),
metadata: webPhp,
},
{
order: 2,
id: 'web-ruby',
Logo: ({ className }: { readonly className?: string }) => (
<img src={webRubyLogo} alt="web-ruby" className={className} />
),
Component: lazy(async () => import('./web-ruby/README.mdx')),
metadata: webRuby,
},
{
order: 2.1,
id: 'spa-webflow',
Expand Down Expand Up @@ -270,5 +281,3 @@ const guides: Readonly<Guide[]> = Object.freeze([
metadata: thirdPartyOidc,
},
]);

export default guides;
5 changes: 4 additions & 1 deletion packages/console/src/assets/docs/guides/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ export type GuideMetadata = {

/** The guide instance to build in the console. */
export type Guide = {
order: number;
/** The unique identifier of the guide. */
id: string;
Logo: LazyExoticComponent<SvgComponent>;
Logo:
| LazyExoticComponent<SvgComponent>
| ((props: { readonly className?: string }) => JSX.Element);
Component: LazyExoticComponent<FunctionComponent<MDXProps>>;
metadata: Readonly<GuideMetadata>;
};
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.
10 changes: 10 additions & 0 deletions packages/console/src/assets/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,13 @@ declare module '*.svg' {
const value: SvgComponent;
export default value;
}

declare module '*.png' {
const value: string;
export default value;
}

declare module '*.webp' {
const value: string;
export default value;
}
2 changes: 1 addition & 1 deletion packages/console/src/components/Guide/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react';

import guides from '@/assets/docs/guides';
import { guides } from '@/assets/docs/guides';
import { type Guide } from '@/assets/docs/guides/types';
import {
thirdPartyAppCategory,
Expand Down
6 changes: 4 additions & 2 deletions packages/console/src/components/Guide/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MDXProvider } from '@mdx-js/react';
import classNames from 'classnames';
import { type LazyExoticComponent, Suspense, createContext, useContext } from 'react';

import guides from '@/assets/docs/guides';
import { guides } from '@/assets/docs/guides';
import { type GuideMetadata } from '@/assets/docs/guides/types';
import Button from '@/ds-components/Button';
import CodeEditor from '@/ds-components/CodeEditor';
Expand All @@ -17,7 +17,9 @@ import * as styles from './index.module.scss';

export type GuideContextType = {
metadata: Readonly<GuideMetadata>;
Logo?: LazyExoticComponent<SvgComponent>;
Logo?:
| LazyExoticComponent<SvgComponent>
| ((props: { readonly className?: string }) => JSX.Element);
isCompact: boolean;
app?: ApplicationResponse;
endpoint?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type Resource } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext, useMemo } from 'react';

import guides from '@/assets/docs/guides';
import { guides } from '@/assets/docs/guides';
import Guide, { GuideContext, type GuideContextType } from '@/components/Guide';
import { AppDataContext } from '@/contexts/AppDataProvider';
import useCustomDomain from '@/hooks/use-custom-domain';
Expand Down
Loading

0 comments on commit 82e702f

Please sign in to comment.