0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-17 22:31:28 -05:00

Merge pull request #6046 from logto-io/gao-add-ruby

This commit is contained in:
Gao Sun 2024-06-19 10:12:47 +08:00 committed by GitHub
commit 82e702fbde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 270 additions and 23 deletions

View file

@ -0,0 +1,5 @@
---
"@logto/console": minor
---
add Ruby app guide

View file

@ -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(
@ -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
@ -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');

View file

@ -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',
@ -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',
@ -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',
@ -270,5 +281,3 @@ const guides: Readonly<Guide[]> = Object.freeze([
metadata: thirdPartyOidc,
},
]);
export default guides;

View file

@ -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>;
};

View file

@ -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>

View file

@ -0,0 +1,3 @@
{
"order": 2
}

View file

@ -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.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -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;
}

View file

@ -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,

View file

@ -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';
@ -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;

View file

@ -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';

View file

@ -2,7 +2,7 @@ import { type ApplicationResponse } 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';