0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): migrate existing guides to v2 (#4330)

* refactor(console): migrate existing guides to v2

* refactor(console): update content

* refactor(console): use context for uri input component
This commit is contained in:
Gao Sun 2023-08-15 22:42:19 +08:00 committed by GitHub
parent 6771f9ed6f
commit e3399cbefe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1768 additions and 26 deletions

View file

@ -19,9 +19,17 @@ The `README.mdx` file contains the actual guide content. The `assets` directory
### Create the guide directory
The guide directory should be named `[target]-name`, where `[target]` is the target of the guide in kebab-case (see `types.ts` for a list of all targets) and `name` is the name of the guide. The name should be kebab-cased and should not contain any special characters.
The guide directory should be named `[target]-name`, where `[target]` is the target of the guide in kebab-case and `name` is the name of the guide. The name should be kebab-cased and should not contain any special characters.
For example, a guide for the `MachineToMachine` target with the name `General` should be placed in the directory `machine-to-machine-general`; a guide for the `SPA` target with the name `React` should be placed in the directory `spa-react`.
Currently we have the following targets:
- `spa`: Single-page application
- `web`: Web application
- `native`: Native application
- `m2m`: Machine-to-machine
- `api`: API resource
For example, a guide for the `MachineToMachine` target with the name `General` should be placed in the directory `m2m-general`; a guide for the `SPA` target with the name `React` should be placed in the directory `spa-react`.
> **Note**
> The directory name will be the unique identifier of the guide.

View file

@ -2,11 +2,39 @@
import { lazy } from 'react';
import nativeAndroidJava from './native-android-java/index';
import nativeAndroidKt from './native-android-kt/index';
import nativeIosSwift from './native-ios-swift/index';
import spaReact from './spa-react/index';
import spaVanilla from './spa-vanilla/index';
import spaVue from './spa-vue/index';
import { type Guide } from './types';
import webExpress from './web-express/index';
import webGo from './web-go/index';
import webNext from './web-next/index';
const guides: Readonly<Guide[]> = Object.freeze([
{
id: 'native-android-java',
Logo: lazy(async () => import('./native-android-java/logo.svg')),
Component: lazy(async () => import('./native-android-java/README.mdx')),
metadata: nativeAndroidJava,
},
{
id: 'native-android-kt',
Logo: lazy(async () => import('./native-android-kt/logo.svg')),
Component: lazy(async () => import('./native-android-kt/README.mdx')),
metadata: nativeAndroidKt,
},
{
id: 'native-ios-swift',
Logo: lazy(async () => import('./native-ios-swift/logo.svg')),
Component: lazy(async () => import('./native-ios-swift/README.mdx')),
metadata: nativeIosSwift,
},
{
id: 'spa-react',
Logo: lazy(async () => import('./spa-react/logo.svg')),
@ -14,12 +42,40 @@ const guides: Readonly<Guide[]> = Object.freeze([
metadata: spaReact,
},
{
id: 'spa-vanilla',
Logo: lazy(async () => import('./spa-vanilla/logo.svg')),
Component: lazy(async () => import('./spa-vanilla/README.mdx')),
metadata: spaVanilla,
},
{
id: 'spa-vue',
Logo: lazy(async () => import('./spa-vue/logo.svg')),
Component: lazy(async () => import('./spa-vue/README.mdx')),
metadata: spaVue,
},
{
id: 'web-express',
Logo: lazy(async () => import('./web-express/logo.svg')),
Component: lazy(async () => import('./web-express/README.mdx')),
metadata: webExpress,
},
{
id: 'web-go',
Logo: lazy(async () => import('./web-go/logo.svg')),
Component: lazy(async () => import('./web-go/README.mdx')),
metadata: webGo,
},
{
id: 'web-next',
Logo: lazy(async () => import('./web-next/logo.svg')),
Component: lazy(async () => import('./web-next/README.mdx')),
metadata: webNext,
},
]);
export default guides;

View file

@ -0,0 +1,121 @@
import UriInputField from '@/mdx-components-v2/UriInputField';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components-v2/Steps';
import Step from '@/mdx-components-v2/Step';
<Steps>
<Step
title="Integrate Logto Android SDK"
subtitle="Add Logto SDK as a dependency"
>
<InlineNotification>The minimum supported Android API is level 24</InlineNotification>
Add the `mavenCentral()` repository to your Gradle project build file:
```kotlin
repositories {
mavenCentral()
}
```
Add Logto Android SDK to your dependencies:
```groovy
dependencies {
implementation 'io.logto.sdk:android:1.0.0'
}
```
</Step>
<Step
title="Init LogtoClient"
subtitle="1 step"
>
<pre>
<code className="language-java">
{`import io.logto.sdk.android.LogtoClient;
import io.logto.sdk.android.type.LogtoConfig;
public class MainActivity extends AppCompatActivity {
private LogtoClient logtoClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LogtoConfig logtoConfig = new LogtoConfig(
"${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
"${props.app.id}",
null,
null,
true
);
logtoClient = new LogtoClient(logtoConfig, getApplication());
}
}`}
</code>
</pre>
</Step>
<Step
title="Sign in"
subtitle="2 steps"
>
### Configure Redirect URI
First, lets configure your redirect URI. E.g. `io.logto.android://io.logto.sample/callback`
<UriInputField name="redirectUris" />
Go back to your IDE/editor, use the following code to implement sign-in:
<pre>
<code className="language-java">
{`logtoClient.signIn(this, "${
props.redirectUris[0] ?? '<your-redirect-uri>'
}", logtoException -> {
// User signed in successfully if \`logtoException\` is null.
});`}
</code>
</pre>
After signing in successfully, `logtoClient.isAuthenticated` will be `true`.
</Step>
<Step
title="Sign out"
subtitle="1 step"
>
Calling `.signOut(completion)` will always clear local credentials even if errors occurred.
```java
logtoClient.signOut(logtoException -> {
// Local credentials are cleared regardless of whether `logtoException` is null.
});
```
</Step>
<Step
title="Further readings"
subtitle="4 articles"
>
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
</Step>
</Steps>

View file

@ -0,0 +1,15 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Android (Java)',
description: 'Android integration for Java.',
target: ApplicationType.Native,
sample: {
repo: 'kotlin',
path: 'android-sample-java',
},
});
export default metadata;

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path
d="M11.622 24.74s-1.23.748.855.962c2.51.32 3.847.267 6.625-.267a10.02 10.02 0 0 0 1.763.855c-6.25 2.672-14.16-.16-9.244-1.55zm-.8-3.473s-1.336 1.015.748 1.23c2.725.267 4.862.32 8.55-.427a3.26 3.26 0 0 0 1.282.801c-7.534 2.244-15.976.214-10.58-1.603zm14.747 6.09s.908.748-1.015 1.336c-3.58 1.07-15.014 1.39-18.22 0-1.122-.48 1.015-1.175 1.7-1.282.695-.16 1.07-.16 1.07-.16-1.23-.855-8.175 1.763-3.526 2.51 12.77 2.084 23.296-.908 19.983-2.404zM12.2 17.633s-5.824 1.39-2.084 1.87c1.603.214 4.755.16 7.694-.053 2.404-.214 4.81-.64 4.81-.64s-.855.374-1.443.748c-5.93 1.55-17.312.855-14.052-.748 2.778-1.336 5.076-1.175 5.076-1.175zm10.42 5.824c5.984-3.1 3.206-6.09 1.282-5.717-.48.107-.695.214-.695.214s.16-.32.534-.427c3.794-1.336 6.786 4.007-1.23 6.09 0 0 .053-.053.107-.16zm-9.83 8.442c5.77.374 14.587-.214 14.8-2.94 0 0-.427 1.07-4.755 1.87-4.916.908-11.007.8-14.587.214 0 0 .748.64 4.542.855z"
fill="#4e7896" />
<path
d="M18.996.001s3.313 3.366-3.152 8.442c-5.183 4.114-1.175 6.465 0 9.137-3.046-2.725-5.236-5.13-3.74-7.373C14.294 6.893 20.332 5.3 18.996.001zm-1.7 15.335c1.55 1.763-.427 3.366-.427 3.366s3.954-2.03 2.137-4.542c-1.656-2.404-2.94-3.58 4.007-7.587 0 0-10.953 2.725-5.717 8.763z"
fill="#f58219" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,113 @@
import UriInputField from '@/mdx-components-v2/UriInputField';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components-v2/Steps';
import Step from '@/mdx-components-v2/Step';
<Steps>
<Step
title="Integrate Logto Android SDK"
subtitle="Add Logto SDK as a dependency"
>
<InlineNotification>The minimum supported Android API is level 24</InlineNotification>
Add the `mavenCentral()` repository to your Gradle project build file:
```kotlin
repositories {
mavenCentral()
}
```
Add Logto Android SDK to your dependencies:
```kotlin
dependencies {
implementation("io.logto.sdk:android:1.0.0")
}
```
</Step>
<Step
title="Init LogtoClient"
subtitle="1 step"
>
<pre>
<code className="language-kotlin">
{`import io.logto.sdk.android.LogtoClient
import io.logto.sdk.android.type.LogtoConfig
class MainActivity : AppCompatActivity() {
val logtoConfig = LogtoConfig(
endpoint = "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
appId = "${props.app.id}",
scopes = null,
resources = null,
usingPersistStorage = true,
)
val logtoClient = LogtoClient(logtoConfig, application)
}`}
</code>
</pre>
</Step>
<Step
title="Sign in"
subtitle="2 steps"
>
### Configure Redirect URI
First, lets configure your redirect URI. E.g. `io.logto.android://io.logto.sample/callback`
<UriInputField name="redirectUris" />
Go back to your IDE/editor, use the following code to implement sign-in:
<pre>
<code className="language-kotlin">
{`logtoClient.signIn(this, "${
props.redirectUris[0] ?? '<your-redirect-uri>'
}") { logtoException: LogtoException? ->
// User signed in successfully if \`logtoException\` is null.
}`}
</code>
</pre>
After signing in successfully, `logtoClient.isAuthenticated` will be `true`.
</Step>
<Step
title="Sign out"
subtitle="1 step"
>
Calling `.signOut(completion)` will always clear local credentials even if errors occurred.
```kotlin
logtoClient.signOut { logtoException: LogtoException? ->
// Local credentials are cleared regardless of whether `logtoException` is null.
}
```
</Step>
<Step
title="Further readings"
subtitle="4 articles"
>
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
</Step>
</Steps>

View file

@ -0,0 +1,15 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Android (Kotlin)',
description: 'Android integration for Kotlin.',
target: ApplicationType.Native,
sample: {
repo: 'kotlin',
path: 'android-sample-kotlin',
},
});
export default metadata;

View file

@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 8.64 8.633">
<defs>
<linearGradient id="A" x1="2.039" y1="11.659" x2="9.95" y2="3.748"
gradientUnits="userSpaceOnUse">
<stop offset=".108" stop-color="#c757bc" />
<stop offset=".173" stop-color="#cd5ca9" />
<stop offset=".492" stop-color="#e8744f" />
<stop offset=".716" stop-color="#f88316" />
<stop offset=".823" stop-color="#ff8900" />
</linearGradient>
<linearGradient id="B" gradientUnits="userSpaceOnUse">
<stop offset=".296" stop-color="#00afff" />
<stop offset=".694" stop-color="#5282ff" />
<stop offset="1" stop-color="#945dff" />
</linearGradient>
<linearGradient id="C" x1="3.369" y1="6.189" x2="6.073" y2="3.484" xlink:href="#B" />
<linearGradient xlink:href="#B" id="D" x1="6.184" y1="13.878" x2="10.04" y2="10.022" />
</defs>
<g transform="matrix(1.016327 0 0 1.016327 -3.52726 -3.909123)">
<path d="M7.74 3.843L3.47 8.33v4.013l4.262-4.27 4.24-4.232z" fill="url(#A)" />
<path d="M3.47 12.344l4.262-4.27 4.24 4.27z" fill="url(#D)" />
<path d="M3.47 3.843H7.74L3.47 8.33z" fill="url(#C)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,134 @@
import UriInputField from '@/mdx-components-v2/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components-v2/Steps';
import Step from '@/mdx-components-v2/Step';
<Steps>
<Step
title="Integrate Logto Swift SDK"
subtitle="Add Logto SDK as a dependency"
>
Use the following URL to add Logto SDK as a dependency in Swift Package Manager.
```bash
https://github.com/logto-io/swift.git
```
Since Xcode 11, you can [directly import a swift package](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app) w/o any additional tool.
We do not support **Carthage** and **CocoaPods** at the time due to some technical issues.
<details>
<summary>Carthage</summary>
Carthage [needs a `xcodeproj` file to build](https://github.com/Carthage/Carthage/issues/1226#issuecomment-290931385), but `swift package generate-xcodeproj` will report a failure since we are using binary targets
for native social plugins. We will try to find a workaround later.
</details>
<details>
<summary>CocoaPods</summary>
CocoaPods [does not support local dependency](https://github.com/CocoaPods/CocoaPods/issues/3276) and monorepo, thus it's hard to create a `.podspec` for this repo.
</details>
</Step>
<Step
title="Init LogtoClient"
subtitle="1 step"
>
<pre>
<code className="language-swift">
{`import Logto
import LogtoClient
let config = try? LogtoConfig(
endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
appId: "${props.app.id}"
)
let logtoClient = LogtoClient(useConfig: config)`}
</code>
</pre>
By default, we store credentials like ID Token and Refresh Token in Keychain. Thus the user doesn't need to sign in again when he returns.
To turn off this behavior, set `usingPersistStorage` to `false`:
```swift
let config = try? LogtoConfig(
// ...
usingPersistStorage: false
)
```
</Step>
<Step
title="Sign in"
subtitle="2 steps"
>
### Configure Redirect URI
First, lets configure your redirect URI scheme. E.g. `io.logto://callback`
<UriInputField name="redirectUris" />
<InlineNotification>
The Redirect URI in iOS SDK is only for internal use. There's <em>NO NEED</em> to add a{' '}
<a href="https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app">
Custom URL Scheme
</a>{' '}
until a connector asks.
</InlineNotification>
Go back to Xcode, use the following code to implement sign-in:
<pre>
<code className="language-swift">
{`do {
try await client.signInWithBrowser(redirectUri: "${
props.redirectUris[0] ?? 'io.logto://callback'
}")
print(client.isAuthenticated) // true
} catch let error as LogtoClientErrors.SignIn {
// error occured during sign in
}`}
</code>
</pre>
</Step>
<Step
title="Sign out"
subtitle="1 step"
>
Calling `.signOut()` will clean all the Logto data in Keychain, if they exist.
```swift
await client.signOut()
```
</Step>
<Step
title="Further readings"
subtitle="4 articles"
>
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
</Step>
</Steps>

View file

@ -0,0 +1,15 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'iOS (Swift)',
description: 'iOS (Swift) application integration guide.',
target: ApplicationType.Native,
sample: {
repo: 'swift',
path: 'Demos/SwiftUI%20Demo',
},
});
export default metadata;

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1845.5007" y1="1255.6392"
x2="-1797.1339" y2="981.3379" gradientTransform="matrix(-1 0 0 -1 -1693.2107 1246.5044)">
<stop offset="0" style="stop-color:#FAAE42" />
<stop offset="1" style="stop-color:#EF3E31" />
</linearGradient>
<path fill="url(#SVGID_1_)"
d="M56.9,0c1.5,0,139.3,0,141.8,0c6.9,0,13.6,1.1,20.1,3.4c9.4,3.4,17.9,9.4,24.3,17.2c6.5,7.8,10.8,17.4,12.3,27.4c0.6,3.7,0.7,7.4,0.7,11.1c0,3.4,0,123.2,0,128.6c0,3.2,0,6.5,0,9.7c0,4.4-0.2,8.9-1.1,13.2c-2,9.9-6.7,19.2-13.5,26.7c-6.7,7.5-15.5,13.1-25,16.1c-5.8,1.8-11.8,2.6-17.9,2.6c-2.7,0-142.1,0-144.2-0.1c-10.2-0.5-20.3-3.8-28.8-9.5c-8.3-5.6-15.1-13.4-19.5-22.4c-3.8-7.7-5.7-16.3-5.7-24.9c0-2,0-140.2,0-142.2C0.2,48.4,2,40,5.7,32.4c4.3-9,11-16.9,19.3-22.5c8.5-5.8,18.5-9.2,28.7-9.7C54.7,0,55.8,0,56.9,0z" />
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="130.6117" y1="4.1363"
x2="95.213" y2="204.8927">
<stop offset="0" style="stop-color:#E39F3A" />
<stop offset="1" style="stop-color:#D33929" />
</linearGradient>
<path fill="url(#SVGID_2_)"
d="M216,209.4c-0.9-1.4-1.9-2.8-3-4.1c-2.5-3-5.4-5.6-8.6-7.8c-4-2.7-8.7-4.4-13.5-4.6c-3.4-0.2-6.8,0.4-10,1.6c-3.2,1.1-6.3,2.7-9.3,4.3c-3.5,1.8-7,3.6-10.7,5.1c-4.4,1.8-9,3.2-13.7,4.2c-5.9,1.1-11.9,1.5-17.8,1.4c-10.7-0.2-21.4-1.8-31.6-4.8c-9-2.7-17.6-6.4-25.7-11.1c-7.1-4.1-13.7-8.8-19.9-14.1c-5.1-4.4-9.8-9.1-14.2-14.1c-3-3.5-5.9-7.2-8.6-11c-1.1-1.5-2.1-3.1-3-4.7c0,0,0,0,0,0c0,0,0,0,0,0L0,121.2V56.7C0,25.4,25.3,0,56.6,0h50.5l37.4,38c0,0,0,0,0,0c84.4,57.4,57.1,120.7,57.1,120.7S225.6,185.7,216,209.4z" />
<path fill="#FFFFFF"
d="M144.7,38c84.4,57.4,57.1,120.7,57.1,120.7s24,27.1,14.3,50.8c0,0-9.9-16.6-26.5-16.6c-16,0-25.4,16.6-57.6,16.6c-71.7,0-105.6-59.9-105.6-59.9C91,192.1,135.1,162,135.1,162c-29.1-16.9-91-97.7-91-97.7c53.9,45.9,77.2,58,77.2,58c-13.9-11.5-52.9-67.7-52.9-67.7c31.2,31.6,93.2,75.7,93.2,75.7C179.2,81.5,144.7,38,144.7,38z" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,4 +1,4 @@
import UriInputField from '@mdx/components/UriInputField';
import UriInputField from '@/mdx-components-v2/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
@ -75,12 +75,7 @@ const App = () => (
First, lets enter your redirect URI. E.g. `http://localhost:3000/callback`.
<UriInputField
appId={props.app.id}
isSingle={!props.isCompact}
name="redirectUris"
title="application_details.redirect_uri"
/>
<UriInputField name="redirectUris" />
### Implement a sign-in button
@ -147,12 +142,7 @@ Calling `.signOut()` will clear all the Logto data in memory and localStorage if
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`.
<UriInputField
appId={props.app.id}
isSingle={!props.isCompact}
name="postLogoutRedirectUris"
title="application_details.post_sign_out_redirect_uri"
/>
<UriInputField name="postLogoutRedirectUris" />
### Implement a sign-out button

View file

@ -0,0 +1,149 @@
import UriInputField from '@/mdx-components-v2/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components-v2/Steps';
import Step from '@/mdx-components-v2/Step';
<Steps>
<Step
title="Add Logto SDK as a dependency"
subtitle="Please select your favorite package manager"
>
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm i @logto/browser
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @logto/browser
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @logto/browser
```
</TabItem>
</Tabs>
</Step>
<Step
title="Init LogtoClient"
subtitle="1 step"
>
Import and init `LogtoClient` by passing config:
<pre>
<code className="language-ts">
{`import LogtoClient from '@logto/browser';
const logtoClient = new LogtoClient({
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
appId: '${props.app.id}',
});`}
</code>
</pre>
</Step>
<Step
title="Sign in"
subtitle="3 steps"
>
<InlineNotification>
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
</InlineNotification>
### Configure Redirect URI
First, lets enter your redirect URI. E.g. `http://localhost:3000/callback`.
<UriInputField name="redirectUris" />
### Implement a sign-in button
Go back to your IDE/editor, use the following code to implement the sign-in button:
<pre>
<code className="language-html">
{`<button onclick="logtoClient.signIn('${
props.redirectUris[0] ?? 'http://localhost:3000/callback'
}')">
Sign In
</button>`}
</code>
</pre>
### Handle redirect
We're almost there! In the last step, we use `http://localhost:3000/callback` as the Redirect URI, and now we need to handle it properly.
Insert the code below in your `/callback` route:
```ts
try {
await logtoClient.handleSignInCallback(window.location.href);
console.log(await logtoClient.isAuthenticated()); // true
} catch {
// Handle error
}
```
Now you can test the sign-in flow.
</Step>
<Step
title="Sign out"
subtitle="1 step"
>
Calling `.signOut()` will clear all the Logto data in memory and localStorage if they exist.
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`.
<UriInputField
appId={props.app.id}
isSingle={!props.isCompact}
name="postLogoutRedirectUris"
title="application_details.post_sign_out_redirect_uri"
/>
### Implement a sign-out button
<pre>
<code className="language-html">
{`<button onclick="logtoClient.signOut('${
props.postLogoutRedirectUris[0] ?? 'http://localhost:3000'
}')">
Sign Out
</button>`}
</code>
</pre>
</Step>
<Step
title="Further readings"
subtitle="4 articles"
>
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
</Step>
</Steps>

View file

@ -0,0 +1,15 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Vanilla JS',
description: 'The framework-agnostic JavaScript integration.',
target: ApplicationType.SPA,
sample: {
repo: 'js',
path: 'packages/browser-sample',
},
});
export default metadata;

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo
Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="JavaScript" role="img"
viewBox="0 0 512 512">
<rect
width="512" height="512"
rx="15%"
fill="#f7df1e" />
<path
d="M324 370c10 17 24 29 47 29c20 0 33-10 33 -24c0-16 -13 -22 -35 -32l-12-5c-35-15 -58 -33 -58 -72c0-36 27 -64 70 -64c31 0 53 11 68 39l-37 24c-8-15 -17 -21 -31 -21c-14 0-23 9 -23 21c0 14 9 20 30 29l12 5c41 18 64 35 64 76c0 43-34 67 -80 67c-45 0-74 -21 -88 -49zm-170 4c8 13 14 25 31 25c16 0 26-6 26 -30V203h48v164c0 50-29 72 -72 72c-39 0-61 -20 -72 -44z" />
</svg>

After

Width:  |  Height:  |  Size: 668 B

View file

@ -0,0 +1,198 @@
import UriInputField from '@/mdx-components-v2/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import Steps from '@/mdx-components-v2/Steps';
import Step from '@/mdx-components-v2/Step';
<Steps>
<Step
title="Add Logto SDK as a dependency"
subtitle="Please select your favorite package manager"
>
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm i @logto/vue
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @logto/vue
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @logto/vue
```
</TabItem>
</Tabs>
</Step>
<Step
title="Init LogtoClient"
subtitle="1 step"
>
<InlineNotification>
We only support Vue 3 Composition API at this point. Will add support to Vue Options API and
possibly Vue 2 in future releases.
</InlineNotification>
Import and use `createLogto` to install Logto plugin:
<pre>
<code className="language-ts">
{`import { createLogto, LogtoConfig } from '@logto/vue';
const config: LogtoConfig = {
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
appId: '${props.app.id}',
};
const app = createApp(App);
app.use(createLogto, config);
app.mount("#app");`}
</code>
</pre>
</Step>
<Step
title="Sign in"
subtitle="3 steps"
>
<InlineNotification>
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
</InlineNotification>
### Configure Redirect URI
First, lets enter your redirect URI. E.g. `http://localhost:3000/callback`.
<UriInputField name="redirectUris" />
### Implement a sign-in button
We provide two composables `useHandleSignInCallback()` and `useLogto()`, which can help you easily manage the authentication flow.
Go back to your IDE/editor, use the following code to implement the sign-in button:
<pre>
<code className="language-html">
{`<script setup lang="ts">
import { useLogto } from "@logto/vue";
const { signIn, isAuthenticated } = useLogto();
const onClickSignIn = () => signIn('${props.redirectUris[0] ?? 'http://localhost:3000/callback'}');
</script>`}
</code>
</pre>
```html
<template>
<div v-if="isAuthenticated">
<div>Signed in</div>
</div>
<div v-else>
<button @click="onClickSignIn">Sign In</button>
</div>
</template>
```
### Handle redirect
We're almost there! In the last step, we use `http://localhost:3000/callback` as the Redirect URI, and now we need to handle it properly.
First let's create a callback component:
```html
<!-- CallbackView.vue -->
<script setup lang="ts">
import { useHandleSignInCallback } from '@logto/vue';
const { isLoading } = useHandleSignInCallback(() => {
// Navigate to root path when finished
});
</script>
```
```html
<template>
<!-- When it's working in progress -->
<p v-if="isLoading">Redirecting...</p>
</template>
```
Finally insert the code below to create a `/callback` route which does NOT require authentication:
```ts
// Assuming vue-router
const router = createRouter({
routes: [
{
path: '/callback',
name: 'callback',
component: CallbackView,
},
],
});
```
</Step>
<Step
title="Sign out"
subtitle="1 step"
>
Calling `.signOut()` will clear all the Logto data in memory and localStorage if they exist.
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`.
<UriInputField name="postLogoutRedirectUris" />
### Implement a sign-out button
<pre>
<code className="language-html">
{`<script setup lang="ts">
import { useLogto } from "@logto/vue";
const { signOut } = useLogto();
const onClickSignOut = () => signOut('${props.postLogoutRedirectUris[0] ?? 'http://localhost:3000'}');
</script>`}
</code>
</pre>
```html
<template>
<button @click="onClickSignOut">Sign Out</button>
</template>
```
</Step>
<Step
title="Further readings"
subtitle="4 articles"
>
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
</Step>
</Steps>

View file

@ -0,0 +1,16 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Vue',
description:
'Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.',
target: ApplicationType.SPA,
sample: {
repo: 'js',
path: 'packages/vue-sample',
},
});
export default metadata;

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -17.5 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><path d="M204.8 0H256L128 220.8 0 0h97.92L128 51.2 157.44 0h47.36z" fill="#41B883"/><path d="M0 0l128 220.8L256 0h-51.2L128 132.48 50.56 0H0z" fill="#41B883"/><path d="M50.56 0L128 133.12 204.8 0h-47.36L128 51.2 97.92 0H50.56z" fill="#35495E"/></svg>

After

Width:  |  Height:  |  Size: 500 B

View file

@ -1,4 +1,4 @@
import UriInputField from '@mdx/components/UriInputField';
import UriInputField from '@/mdx-components-v2/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
@ -92,12 +92,7 @@ app.use(session({ secret: '${buildIdGenerator(32)()}', cookie: { maxAge: 14 * 24
First, lets enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`.
<UriInputField
appId={props.app.id}
isSingle={!props.isCompact}
name="redirectUris"
title="application_details.redirect_uri"
/>
<UriInputField name="redirectUris" />
### Prepare Logto routes

View file

@ -0,0 +1,366 @@
import UriInputField from '@/mdx-components-v2/UriInputField';
import Steps from '@/mdx-components-v2/Steps';
import Step from '@/mdx-components-v2/Step';
import InlineNotification from '@/ds-components/InlineNotification';
<Steps>
<Step
title="Add Logto SDK as a dependency"
>
<InlineNotification>
The following demonstration is built upon the <a href="https://gin-gonic.com">Gin Web Framework</a>.
You may also integrate Logto into other frameworks by taking the same steps.
In the following code snippets, we assume your app is running on <code>http://localhost:8080</code>.
</InlineNotification>
Run in the project root directory:
```bash
go get github.com/logto-io/go
```
Add the `github.com/logto-io/go/client` package to your application code:
```go
// main.go
package main
import (
"github.com/gin-gonic/gin"
// Add dependency
"github.com/logto-io/go/client"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(200, "Hello Logto!")
})
router.Run(":8080")
}
```
</Step>
<Step
title="Use sessions to store user authentication information"
subtitle="2 steps"
>
In traditional web applications, the user authentication information will be stored in the user session.
Logto SDK provides a `Storage` interface, you can implement a `Storage` adapter based on your web framework so that the Logto SDK can store user authentication information in the session.
<InlineNotification>
We do NOT recommend using cookie-based sessions, as user authentication information stored by
Logto may exceed the cookie size limit. In this example, we use memory-based sessions. You can use
Redis, MongoDB, and other technologies in production to store sessions as needed.
</InlineNotification>
The `Storage` type in the Logto SDK is as follows:
```go
// github.com/logto-io/client/storage.go
package client
type Storage interface {
GetItem(key string) string
SetItem(key, value string)
}
```
We will use [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) as an example to demonstrate this process.
### Apply session middleware
Apply the [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) middleware to the application, so that we can get the user session by the user request context in the route handler:
```go
package main
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/logto-io/go/client"
)
func main() {
router := gin.Default()
// We use memory-based session in this example
store := memstore.NewStore([]byte("your session secret"))
router.Use(sessions.Sessions("logto-session", store))
router.GET("/", func(ctx *gin.Context) {
// Get user session
session := sessions.Default(ctx)
// ...
ctx.String(200, "Hello Logto!")
})
router.Run(":8080")
}
```
### Create session storage for Logto to store user authentication information
Create a `session_storage.go` file, define a `SessionStorage` and implement the Logto SDK's `Storage` interfaces:
```go
// session_storage.go
package main
import (
"github.com/gin-contrib/sessions"
)
type SessionStorage struct {
session sessions.Session
}
func (storage *SessionStorage) GetItem(key string) string {
value := storage.session.Get(key)
if value == nil {
return ""
}
return value.(string)
}
func (storage *SessionStorage) SetItem(key, value string) {
storage.session.Set(key, value)
storage.session.Save()
}
```
Now, in the route handler, you can create a session storage for Logto as follows:
```go
session := sessions.Default(ctx)
sessionStorage := &SessionStorage{session: session}
```
</Step>
<Step
title="Init LogtoClient"
subtitle="2 steps"
>
### Create LogtConfig
<pre>
<code className="language-go">
{`// main.go
func main() {
// ...
logtoConfig := &client.LogtoConfig{
Endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
AppId: "${props.app.id}",
AppSecret: "${props.app.secret}",
}
// ...
}`}
</code>
</pre>
### Init LogtoClient for each user request
```go
// main.go
func main() {
// ...
router.GET("/", func(ctx *gin.Context) {
// Init LogtoClient
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)
// Use Logto to control the content of the home page
authState := "You are not logged in to this website. :("
if logtoClient.IsAuthenticated() {
authState = "You are logged in to this website! :)"
}
homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>"
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})
// ...
}
```
</Step>
<Step
title="Sign in"
subtitle="3 steps"
>
### Configure Redirect URI
Add `http://localhost:8080/sign-in-callback` to the Redirect URI field.
This allows Logto to redirect the user to the `/sign-in-callback` route of your application after signing in.
<UriInputField name="redirectUris" />
### Add a route for handling sign-in requests
```go
//main.go
func main() {
// ...
// Add a link to perform a sign-in request on the home page
router.GET("/", func(ctx *gin.Context) {
// ...
homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>" +
// Add link
`<div><a href="/sign-in">Sign In</a></div>`
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})
// Add a route for handling sign-in requests
router.GET("/sign-in", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)
// The sign-in request is handled by Logto.
// The user will be redirected to the Redirect URI on signed in.
signInUri, err := logtoClient.SignIn("http://localhost:8080/sign-in-callback")
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}
// Redirect the user to the Logto sign-in page.
ctx.Redirect(http.StatusTemporaryRedirect, signInUri)
})
// ...
}
```
### Add a route for handling sign-in callback requests
When the user signs in successfully on the Logto sign-in page, Logto will redirect the user to the Redirect URI.
Since the Redirect URI is `http://localhost:8080/sign-in-callback`, we add the `/sign-in-callback` route to handle the callback after signing in.
```go
// main.go
func main() {
// ...
// Add a route for handling sign-in callback requests
router.GET("/sign-in-callback", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)
// The sign-in callback request is handled by Logto
err := logtoClient.HandleSignInCallback(ctx.Request)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}
// Jump to the page specified by the developer.
// This example takes the user back to the home page.
ctx.Redirect(http.StatusTemporaryRedirect, "/")
})
// ...
}
```
</Step>
<Step
title="Sign out"
subtitle="2 steps"
>
### Configure Post Sign-out Redirect URI
Add `http://localhost:8080` to the Post Sign-out Redirect URI filed:
<UriInputField name="postLogoutUris" />
This configuration enables the user to return to the home page after signing out.
### Add a route for handling signing out requests
```go
//main.go
func main() {
// ...
// Add a link to perform a sign-out request on the home page
router.GET("/", func(ctx *gin.Context) {
// ...
homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>" +
`<div><a href="/sign-in">Sign In</a></div>` +
// Add link
`<div><a href="/sign-out">Sign Out</a></div>`
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})
// Add a route for handling signing out requests
router.GET("/sign-out", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)
// The sign-out request is handled by Logto.
// The user will be redirected to the Post Sign-out Redirect URI on signed out.
signOutUri, signOutErr := logtoClient.SignOut("http://localhost:8080")
if signOutErr != nil {
ctx.String(http.StatusOK, signOutErr.Error())
return
}
ctx.Redirect(http.StatusTemporaryRedirect, signOutUri)
})
// ...
}
```
After the user makes a signing-out request, Logto will clear all user authentication information in the session.
</Step>
<Step
title="Further readings"
subtitle="4 articles"
>
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
</Step>
</Steps>

View file

@ -0,0 +1,16 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Go',
description:
'Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.',
target: ApplicationType.Traditional,
sample: {
repo: 'go',
path: 'gin-sample',
},
});
export default metadata;

View file

@ -0,0 +1 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" width="120" height="60" xml:space="preserve"><style>.st0{fill:#00acd7}</style><switch><g><path class="st0" d="M22.3 24.7c-.1 0-.2-.1-.1-.2l.7-1c.1-.1.2-.2.4-.2h12.6c.1 0 .2.1.1.2l-.6.9c-.1.1-.2.2-.4.2l-12.7.1zM17 27.9c-.1 0-.2-.1-.1-.2l.7-1c.1-.1.2-.2.4-.2h16.1c.1 0 .2.1.2.2l-.3 1c0 .1-.2.2-.3.2H17zm8.5 3.3c-.1 0-.2-.1-.1-.2l.5-.9c.1-.1.2-.2.4-.2h7c.1 0 .2.1.2.2l-.1.8c0 .1-.1.2-.2.2l-7.7.1zM62.1 24l-5.9 1.5c-.5.1-.6.2-1-.4-.5-.6-.9-1-1.7-1.3-2.2-1.1-4.4-.8-6.4.5-2.4 1.5-3.6 3.8-3.6 6.7 0 2.8 2 5.1 4.8 5.5 2.4.3 4.4-.5 6-2.3.3-.4.6-.8 1-1.3h-6.8c-.7 0-.9-.5-.7-1.1.5-1.1 1.3-2.9 1.8-3.8.1-.2.4-.6.9-.6h12.8c-.1 1-.1 1.9-.2 2.9-.4 2.5-1.3 4.9-2.9 6.9-2.5 3.3-5.8 5.4-10 6-3.5.5-6.7-.2-9.5-2.3-2.6-2-4.1-4.6-4.5-7.8-.5-3.8.7-7.3 3-10.3 2.5-3.3 5.8-5.4 9.9-6.1 3.3-.6 6.5-.2 9.3 1.7 1.9 1.2 3.2 2.9 4.1 5 .1.4 0 .5-.4.6z"/><path class="st0" d="M73.7 43.5c-3.2-.1-6.1-1-8.6-3.1-2.1-1.8-3.4-4.1-3.8-6.8-.6-4 .5-7.5 2.9-10.6 2.6-3.4 5.7-5.1 9.9-5.9 3.6-.6 7-.3 10 1.8 2.8 1.9 4.5 4.5 5 7.9.6 4.8-.8 8.6-4 11.9-2.3 2.4-5.2 3.8-8.4 4.5-1.1.2-2.1.2-3 .3zm8.4-14.2c0-.5 0-.8-.1-1.2-.6-3.5-3.8-5.5-7.2-4.7-3.3.7-5.4 2.8-6.2 6.1-.6 2.7.7 5.5 3.2 6.7 1.9.8 3.9.7 5.7-.2 2.9-1.4 4.4-3.7 4.6-6.7z"/></g></switch></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,248 @@
import UriInputField from '@/mdx-components-v2/UriInputField';
import Tabs from '@mdx/components/Tabs';
import TabItem from '@mdx/components/TabItem';
import InlineNotification from '@/ds-components/InlineNotification';
import { buildIdGenerator } from '@logto/shared/universal';
import Steps from '@/mdx-components-v2/Steps';
import Step from '@/mdx-components-v2/Step';
<Steps>
<Step
title="Add Logto SDK as a dependency"
subtitle="Please select your favorite package manager"
>
<Tabs>
<TabItem value="npm" label="npm">
```bash
npm i @logto/next
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn add @logto/next
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @logto/next
```
</TabItem>
</Tabs>
</Step>
<Step
title="Init LogtoClient"
subtitle="1 step"
>
<InlineNotification>
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
</InlineNotification>
Import and initialize LogtoClient:
<pre>
<code className="language-ts">
{`// libraries/logto.js
import LogtoClient from '@logto/next';
export const logtoClient = new LogtoClient({
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
appId: '${props.app.id}',
appSecret: '${props.app.secret}',
baseUrl: 'http://localhost:3000', // Change to your own base URL
cookieSecret: '${buildIdGenerator(32)()}', // Auto-generated 32 digit secret
cookieSecure: process.env.NODE_ENV === 'production',
});`}
</code>
</pre>
</Step>
<Step
title="Sign in"
subtitle="3 steps"
>
### Configure Redirect URI
First, lets enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`.
<UriInputField name="redirectUris" />
### Prepare API routes
Prepare [API routes](https://nextjs.org/docs/api-routes/introduction) to connect with Logto.
Go back to your IDE/editor, use the following code to implement the API routes first:
```ts
// pages/api/logto/[action].ts
import { logtoClient } from '../../../libraries/logto';
export default logtoClient.handleAuthRoutes();
```
This will create 4 routes automatically:
1. `/api/logto/sign-in`: Sign in with Logto.
2. `/api/logto/sign-in-callback`: Handle sign-in callback.
3. `/api/logto/sign-out`: Sign out with Logto.
4. `/api/logto/user`: Check if user is authenticated with Logto, if yes, return user info.
### Implement sign-in button
We're almost there! In the last step, we will create a sign-in button:
```tsx
import { useRouter } from 'next/router';
const { push } = useRouter();
<button onClick={() => push('/api/logto/sign-in')}>Sign In</button>;
```
Now you will be navigated to Logto sign-in page when you click the button.
</Step>
<Step
title="Get user profile"
subtitle="2 ways"
>
### Through API request in the frontend
You can fetch user info by calling `/api/logto/user`.
```tsx
import { LogtoUser } from '@logto/next';
import useSWR from 'swr';
const Home = () => {
const { data } = useSWR<LogtoUser>('/api/logto/user');
return <div>User ID: {data?.claims?.sub}</div>;
};
export default Profile;
```
Check [this guide](https://swr.vercel.app/docs/getting-started) to learn more about `useSWR`.
### Through `getServerSideProps` in the backend
```tsx
import { LogtoUser } from '@logto/next';
import { logtoClient } from '../libraries/logto';
type Props = {
user: LogtoUser;
};
const Profile = ({ user }: Props) => {
return <div>User ID: {user.claims?.sub}</div>;
};
export default Profile;
export const getServerSideProps = logtoClient.withLogtoSsr(({ request }) => {
const { user } = request;
return {
props: { user },
};
});
```
Check [Next.js documentation](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) for more details on `getServerSideProps`.
</Step>
<Step
title="Protect API and pages"
subtitle="2 steps"
>
### Protect API routes
Wrap your handler with `logtoClient.withLogtoApiRoute`.
```ts
// pages/api/protected-resource.ts
import { logtoClient } from '../../libraries/logto';
export default logtoClient.withLogtoApiRoute((request, response) => {
if (!request.user.isAuthenticated) {
response.status(401).json({ message: 'Unauthorized' });
return;
}
response.json({
data: 'this_is_protected_resource',
});
});
```
### Protect pages
If you don't want anonymous users to access a page, use `logtoClient.withLogtoSsr` to get auth state, and redirect to sign-in route if not authenticated.
```ts
export const getServerSideProps = logtoClient.withLogtoSsr(async function ({ req, res }) {
const { user } = req;
if (!user.isAuthenticated) {
res.setHeader('location', '/api/logto/sign-in');
res.statusCode = 302;
res.end();
}
return {
props: { user },
};
});
```
</Step>
<Step
title="Sign out"
subtitle="1 step"
>
Calling `/api/logto/sign-out` will clear all the Logto data in memory and cookies if they exist.
After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below before calling `/api/logto/sign-out`.
<UriInputField name="postLogoutRedirectUris" />
### Implement a sign-out button
```tsx
<button onClick={() => push('/api/logto/sign-out')}>Sign Out</button>
```
</Step>
<Step
title="Further readings"
subtitle="4 articles"
>
- [Customize sign-in experience](https://docs.logto.io/docs/tutorials/get-started/customize-sign-in-experience)
- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-sms-or-email-passwordless-sign-in)
- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/passwordless-sign-in-by-adding-connectors#enable-social-sign-in)
- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api)
</Step>
</Steps>

View file

@ -0,0 +1,16 @@
import { ApplicationType } from '@logto/schemas';
import { type GuideMetadata } from '../types';
const metadata: Readonly<GuideMetadata> = Object.freeze({
name: 'Next.js',
description:
'Next.js is a React framework for production - it makes building fullstack React apps and sites a breeze and ships with built-in SSR.',
target: ApplicationType.Traditional,
sample: {
repo: 'js',
path: 'packages/next-sample',
},
});
export default metadata;

View file

@ -0,0 +1,11 @@
<svg viewBox="0 0 256 256" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
preserveAspectRatio="xMidYMid">
<g>
<path
d="M119.616813,0.0688905149 C119.066276,0.118932037 117.314565,0.294077364 115.738025,0.419181169 C79.3775171,3.69690087 45.3192571,23.3131775 23.7481916,53.4631946 C11.7364614,70.2271045 4.05395894,89.2428829 1.15112414,109.384595 C0.12512219,116.415429 0,118.492153 0,128.025062 C0,137.557972 0.12512219,139.634696 1.15112414,146.665529 C8.10791789,194.730411 42.3163245,235.11392 88.7116325,250.076335 C97.0197458,252.753556 105.778299,254.580072 115.738025,255.680985 C119.616813,256.106338 136.383187,256.106338 140.261975,255.680985 C157.453763,253.779407 172.017986,249.525878 186.382014,242.194795 C188.584164,241.068861 189.00958,240.768612 188.709286,240.518404 C188.509091,240.36828 179.124927,227.782837 167.86393,212.570214 L147.393939,184.922273 L121.743891,146.965779 C107.630108,126.098464 96.0187683,109.034305 95.9186706,109.034305 C95.8185728,109.009284 95.7184751,125.873277 95.6684262,146.465363 C95.5933529,182.52028 95.5683284,183.971484 95.1178886,184.82219 C94.4672532,186.048207 93.9667644,186.548623 92.915738,187.099079 C92.114956,187.499411 91.4142717,187.574474 87.6355816,187.574474 L83.3063539,187.574474 L82.1552297,186.848872 C81.4044966,186.373477 80.8539589,185.747958 80.4785924,185.022356 L79.9530792,183.896422 L80.0031281,133.729796 L80.0782014,83.5381493 L80.8539589,82.5623397 C81.25435,82.0369037 82.1051808,81.3613431 82.7057674,81.0360732 C83.7317693,80.535658 84.1321603,80.4856165 88.4613881,80.4856165 C93.5663734,80.4856165 94.4172043,80.6857826 95.7434995,82.1369867 C96.1188661,82.5373189 110.007429,103.454675 126.623656,128.650581 C143.239883,153.846488 165.962072,188.250034 177.122972,205.139048 L197.392766,235.839522 L198.418768,235.163961 C207.502639,229.259062 217.112023,220.852086 224.719453,212.09482 C240.910264,193.504394 251.345455,170.835585 254.848876,146.665529 C255.874878,139.634696 256,137.557972 256,128.025062 C256,118.492153 255.874878,116.415429 254.848876,109.384595 C247.892082,61.3197135 213.683675,20.9362052 167.288368,5.97379012 C159.105376,3.32158945 150.396872,1.49507389 140.637341,0.394160408 C138.234995,0.143952798 121.693842,-0.131275573 119.616813,0.0688905149 L119.616813,0.0688905149 Z M172.017986,77.4831252 C173.219159,78.0836234 174.195112,79.2345784 174.545455,80.435575 C174.74565,81.0861148 174.795699,94.9976579 174.74565,126.348671 L174.670577,171.336 L166.73783,159.17591 L158.780059,147.01582 L158.780059,114.313685 C158.780059,93.1711423 158.880156,81.2862808 159.030303,80.7108033 C159.430694,79.3096407 160.306549,78.2087272 161.507722,77.5581875 C162.533724,77.0327515 162.909091,76.98271 166.837928,76.98271 C170.541544,76.98271 171.19218,77.0327515 172.017986,77.4831252 Z"
fill="#000000">
</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,20 @@
@use '@/scss/underscore' as _;
.wrapper {
display: flex;
align-items: flex-start;
position: relative;
.field {
flex: 1;
.multiTextInput {
flex: 1;
}
}
.saveButton {
flex-shrink: 0;
margin: _.unit(6) 0 0 _.unit(2);
}
}

View file

@ -0,0 +1,153 @@
import type { AdminConsoleKey } from '@logto/phrases';
import type { Application } from '@logto/schemas';
import type { KeyboardEvent } from 'react';
import { useContext, useRef } from 'react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import MultiTextInputField from '@/components/MultiTextInputField';
import Button from '@/ds-components/Button';
import FormField from '@/ds-components/FormField';
import {
convertRhfErrorMessage,
createValidatorForRhf,
} from '@/ds-components/MultiTextInput/utils';
import TextInput from '@/ds-components/TextInput';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import { GuideContext } from '@/pages/Applications/components/GuideV2';
import type { GuideForm } from '@/types/guide';
import { trySubmitSafe } from '@/utils/form';
import { uriValidator } from '@/utils/validator';
import * as styles from './index.module.scss';
type Props = {
name: 'redirectUris' | 'postLogoutRedirectUris';
};
function UriInputField({ name }: Props) {
const methods = useForm<Partial<GuideForm>>();
const {
control,
getValues,
handleSubmit,
reset,
formState: { isSubmitting },
} = methods;
const {
app: { id: appId },
isCompact,
} = useContext(GuideContext);
const isSingle = !isCompact;
const { data, mutate } = useSWR<Application, RequestError>(`api/applications/${appId}`);
const ref = useRef<HTMLDivElement>(null);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useApi();
const title: AdminConsoleKey =
name === 'redirectUris'
? 'application_details.redirect_uri'
: 'application_details.post_sign_out_redirect_uri';
const onSubmit = trySubmitSafe(async (value: string[]) => {
const updatedApp = await api
.patch(`api/applications/${appId}`, {
json: {
oidcClientMetadata: {
[name]: value.filter(Boolean),
},
},
})
.json<Application>();
void mutate(updatedApp);
toast.success(t('general.saved'));
// Reset form to set 'isDirty' to false
reset(getValues());
});
const onKeyPress = (event: KeyboardEvent<HTMLInputElement>, value: string[]) => {
if (event.key === 'Enter') {
event.preventDefault();
void handleSubmit(async () => onSubmit(value))();
}
};
return (
<FormProvider {...methods}>
<form>
<Controller
name={name}
control={control}
defaultValue={data?.oidcClientMetadata[name]}
rules={{
validate: createValidatorForRhf({
required: t(
isSingle ? 'errors.required_field_missing' : 'errors.required_field_missing_plural',
{ field: title }
),
pattern: {
verify: (value) => !value || uriValidator(value),
message: t('errors.invalid_uri_format'),
},
}),
}}
render={({ field: { onChange, value = [] }, fieldState: { error, isDirty } }) => {
const errorObject = convertRhfErrorMessage(error?.message);
return (
<div ref={ref} className={styles.wrapper}>
{isSingle && (
<FormField
isRequired={name === 'redirectUris'}
className={styles.field}
title={title}
>
<TextInput
className={styles.field}
value={value[0]}
error={errorObject?.required ?? errorObject?.inputs?.[0]}
onChange={({ currentTarget: { value } }) => {
onChange([value]);
}}
onKeyPress={(event) => {
onKeyPress(event, value);
}}
/>
</FormField>
)}
{!isSingle && (
<MultiTextInputField
isRequired={name === 'redirectUris'}
formFieldClassName={styles.field}
title={title}
value={value}
error={errorObject}
className={styles.multiTextInput}
onChange={onChange}
onKeyPress={(event) => {
onKeyPress(event, value);
}}
/>
)}
<Button
className={styles.saveButton}
disabled={!isDirty}
isLoading={isSubmitting}
title="general.save"
type="primary"
onClick={handleSubmit(async () => onSubmit(value))}
/>
</div>
);
}}
/>
</form>
</FormProvider>
);
}
export default UriInputField;

View file

@ -14,10 +14,14 @@
overflow-y: auto;
padding: _.unit(6) _.unit(6) max(10vh, 120px);
.banner {
display: flex;
align-items: center;
margin-bottom: _.unit(6);
section p {
font: var(--font-body-2);
margin: _.unit(4) 0;
}
section ul > li {
margin-block: _.unit(2);
padding-inline-start: _.unit(1);
}
}
}