mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
Merge branch 'master' into gao-refactor-sync-keys-cli
This commit is contained in:
commit
602c2d5ab0
224 changed files with 1742 additions and 5626 deletions
2
.github/workflows/changesets.yml
vendored
2
.github/workflows/changesets.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
|
||||
|
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
|
@ -41,7 +41,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
|
2
.github/workflows/commitlint.yml
vendored
2
.github/workflows/commitlint.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
6
.github/workflows/integration-test.yml
vendored
6
.github/workflows/integration-test.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v3
|
||||
|
@ -45,7 +45,7 @@ jobs:
|
|||
DB_URL: postgres://postgres:postgres@localhost:5432/postgres
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: tests
|
||||
|
||||
|
@ -74,7 +74,7 @@ jobs:
|
|||
uses: ikalnytskyi/action-setup-postgres@v4
|
||||
|
||||
- name: Setup Redis
|
||||
uses: supercharge/redis-github-action@1.6.0
|
||||
uses: supercharge/redis-github-action@6dc7a5eeaf9a8f860b6464e05195a15f8b9f3bbb # 1.7.0
|
||||
with:
|
||||
redis-version: 6
|
||||
|
||||
|
|
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v3
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v3
|
||||
|
@ -46,7 +46,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v3
|
||||
|
@ -73,7 +73,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -92,7 +92,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
# ** Checkout fresh and alteration ref **
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
path: ./fresh
|
||||
|
@ -102,7 +102,7 @@ jobs:
|
|||
working-directory: ./fresh
|
||||
run: echo "current=$(git describe --match "@logto/schemas@*" --abbrev=0)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.version.outputs.current }}
|
||||
path: ./alteration
|
||||
|
|
2
.github/workflows/master-codecov-report.yml
vendored
2
.github/workflows/master-codecov-report.yml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v3
|
||||
|
|
2
.github/workflows/pen-tests.yml
vendored
2
.github/workflows/pen-tests.yml
vendored
|
@ -21,7 +21,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Compose up
|
||||
run: |
|
||||
|
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
|||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
@ -72,7 +72,7 @@ jobs:
|
|||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Set Git operations with the bot PAT since we have tag protection rule
|
||||
token: ${{ secrets.BOT_PAT }}
|
||||
|
@ -104,7 +104,7 @@ jobs:
|
|||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
2
.github/workflows/update-pr-metadata.yml
vendored
2
.github/workflows/update-pr-metadata.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# https://github.com/actions/checkout/issues/518#issuecomment-890401887
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
|
|
2
.github/workflows/upload-annotations.yml
vendored
2
.github/workflows/upload-annotations.yml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
|||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node and pnpm
|
||||
uses: silverhand-io/actions-node-pnpm-run-steps@v3
|
||||
|
|
|
@ -5,26 +5,36 @@ The Microsoft Azure AD connector provides a succinct way for your application to
|
|||
**Table of contents**
|
||||
- [Microsoft Azure AD connector](#microsoft-azure-ad-connector)
|
||||
- [Set up Microsoft Azure AD in the Azure Portal](#set-up-microsoft-azure-ad-in-the-azure-portal)
|
||||
- [Fill in the configuration](#fill-in-the-configuration)
|
||||
- [Configure your client secret](#configure-your-client-secret)
|
||||
- [Config types](#config-types)
|
||||
- [Config types](#config-types)
|
||||
- [References](#references)
|
||||
|
||||
## Set up Microsoft Azure AD in the Azure Portal
|
||||
|
||||
- Visit the [Azure Portal](https://portal.azure.com/#home) and sign in with your Azure account. You need to have an active subscription to access Microsoft Azure AD.
|
||||
- Click the **Azure Active Directory** from the services they offer, and click the **App Registrations** from the left menu.
|
||||
- Click **New Registration** at the top and enter a description, select your **access type** and add your **Redirect URI**, which redirect the user to the application after logging in. In our case, this will be `${your_logto_origin}/callback/${connector_id}`. e.g. `https://logto.dev/callback/${connector_id}`. You need to select Web as Platform. The `connector_id` can be found on the top bar of the Logto Admin Console connector details page.
|
||||
- Click **New Registration** at the top and enter a description, select your **access type** and add your **Redirect URI**, which redirect the user to the application after logging in. In our case, this will be `${your_logto_endpoint}/callback/${connector_id}`. e.g. `https://foo.logto.app/callback/${connector_id}`. (The `connector_id` can be also found on the top bar of the Logto Admin Console connector details page)
|
||||
- You need to select Web as Platform.
|
||||
- If you select **Sign in users of a specific organization only** for access type then you need to enter **TenantID**.
|
||||
- If you select **Sign in users with work and school accounts or personal Microsoft accounts** for access type then you need to enter **common**.
|
||||
- If you select **Sign in users with work and school accounts** for access type then you need to enter **organizations**.
|
||||
- If you select **Sign in users with personal Microsoft accounts (MSA) only** for access type then you need to enter **consumers**.
|
||||
|
||||
## Configure your client secret
|
||||
- In your newly created project, click the **Certificates & Secrets** to get a client secret, and click the **New client secret** from the top.
|
||||
- Enter a description and an expiration.
|
||||
- This will only show your client secret once. Save the **value** to a secure location.
|
||||
> You can copy the `Callback URI` in the configuration section.
|
||||
|
||||
### Config types
|
||||
## Fill in the configuration
|
||||
|
||||
In details page of the newly registered app, you can find the **Application (client) ID** and **Directory (tenant) ID**.
|
||||
|
||||
For **Cloud Instance**, usually it is `https://login.microsoftonline.com/`. See [Azure AD authentication endpoints](https://learn.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints) for more information.
|
||||
|
||||
## Configure your client secret
|
||||
- In your newly created application, click the **Certificates & Secrets** to get a client secret, and click the **New client secret** from the top.
|
||||
- Enter a description and an expiration.
|
||||
- This will only show your client secret once. Fill the **value** to the Logto connector configuration and save it to a secure location.
|
||||
|
||||
## Config types
|
||||
|
||||
| Name | Type |
|
||||
| ------------- | ------ |
|
||||
|
@ -34,4 +44,4 @@ The Microsoft Azure AD connector provides a succinct way for your application to
|
|||
| cloudInstance | string |
|
||||
|
||||
## References
|
||||
* [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=nodejs)
|
||||
* [Web app that signs in users](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-web-app-sign-user-overview)
|
||||
|
|
|
@ -43,7 +43,8 @@ export const defaultMetadata: ConnectorMetadata = {
|
|||
type: ConnectorConfigFormItemType.Text,
|
||||
required: true,
|
||||
label: 'Cloud Instance',
|
||||
placeholder: '<cloud-instance>',
|
||||
placeholder: 'https://login.microsoftonline.com',
|
||||
defaultValue: 'https://login.microsoftonline.com',
|
||||
},
|
||||
{
|
||||
key: 'tenantId',
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"@fontsource/roboto-mono": "^5.0.0",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@logto/app-insights": "workspace:^1.3.1",
|
||||
"@logto/cloud": "0.2.5-1795c3d",
|
||||
"@logto/cloud": "0.2.5-71b7fea",
|
||||
"@logto/connector-kit": "workspace:^1.1.1",
|
||||
"@logto/core-kit": "workspace:^2.0.1",
|
||||
"@logto/language-kit": "workspace:^1.0.0",
|
||||
|
@ -60,7 +60,7 @@
|
|||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.1",
|
||||
"@withtyped/client": "^0.7.21",
|
||||
"@withtyped/client": "^0.7.22",
|
||||
"buffer": "^5.7.1",
|
||||
"classnames": "^2.3.1",
|
||||
"clean-deep": "^3.4.0",
|
||||
|
|
|
@ -60,8 +60,10 @@ This may be fixed by replacing Parcel with something else.
|
|||
|
||||
The guides are ordered by the following rules in ascending order:
|
||||
|
||||
1. The first segment of the directory name, which should be the target of the guide;
|
||||
2. The `order` property of the guide.
|
||||
1. The `order` property of all guides across all category groups
|
||||
2. The sorting order should exactly follow the order in UX design. E.g. Next.js being the 1st one, React being the 2nd
|
||||
3. The guides in featured group ("Popular and for you") should have `1.x` order value
|
||||
4. The guides that are not listed in featured group can have order value equal to or greater than 2
|
||||
|
||||
You can configure the property by creating a `config.json` file in the guide directory. The file should be an object with the following structure:
|
||||
|
||||
|
|
|
@ -34,13 +34,7 @@ const data = await Promise.all(
|
|||
};
|
||||
})
|
||||
);
|
||||
const metadata = data.filter(Boolean).slice().sort((a, b) => {
|
||||
if (a.name.split('-')[0] !== b.name.split('-')[0]) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
return a.order - b.order;
|
||||
});
|
||||
const metadata = data.filter(Boolean).slice().sort((a, b) => a.order - b.order);
|
||||
|
||||
const camelCase = (value) => value.replaceAll(/-./g, (x) => x[1].toUpperCase());
|
||||
const filename = 'index.ts';
|
||||
|
|
|
@ -24,69 +24,6 @@ import webPython from './web-python/index';
|
|||
import webRemix from './web-remix/index';
|
||||
|
||||
const guides: Readonly<Guide[]> = Object.freeze([
|
||||
{
|
||||
order: Number.POSITIVE_INFINITY,
|
||||
id: 'm2m-general',
|
||||
Logo: lazy(async () => import('./m2m-general/logo.svg')),
|
||||
Component: lazy(async () => import('./m2m-general/README.mdx')),
|
||||
metadata: m2mGeneral,
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
id: 'native-ios-swift',
|
||||
Logo: lazy(async () => import('./native-ios-swift/logo.svg')),
|
||||
Component: lazy(async () => import('./native-ios-swift/README.mdx')),
|
||||
metadata: nativeIosSwift,
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
id: 'native-android-java',
|
||||
Logo: lazy(async () => import('./native-android-java/logo.svg')),
|
||||
Component: lazy(async () => import('./native-android-java/README.mdx')),
|
||||
metadata: nativeAndroidJava,
|
||||
},
|
||||
{
|
||||
order: 2.1,
|
||||
id: 'native-android-kt',
|
||||
Logo: lazy(async () => import('./native-android-kt/logo.svg')),
|
||||
Component: lazy(async () => import('./native-android-kt/README.mdx')),
|
||||
metadata: nativeAndroidKt,
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
id: 'native-flutter',
|
||||
Logo: lazy(async () => import('./native-flutter/logo.svg')),
|
||||
Component: lazy(async () => import('./native-flutter/README.mdx')),
|
||||
metadata: nativeFlutter,
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
id: 'native-capacitor',
|
||||
Logo: lazy(async () => import('./native-capacitor/logo.svg')),
|
||||
Component: lazy(async () => import('./native-capacitor/README.mdx')),
|
||||
metadata: nativeCapacitor,
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
id: 'spa-react',
|
||||
Logo: lazy(async () => import('./spa-react/logo.svg')),
|
||||
Component: lazy(async () => import('./spa-react/README.mdx')),
|
||||
metadata: spaReact,
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
id: 'spa-vue',
|
||||
Logo: lazy(async () => import('./spa-vue/logo.svg')),
|
||||
Component: lazy(async () => import('./spa-vue/README.mdx')),
|
||||
metadata: spaVue,
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
id: 'spa-vanilla',
|
||||
Logo: lazy(async () => import('./spa-vanilla/logo.svg')),
|
||||
Component: lazy(async () => import('./spa-vanilla/README.mdx')),
|
||||
metadata: spaVanilla,
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
id: 'web-next',
|
||||
|
@ -94,6 +31,13 @@ const guides: Readonly<Guide[]> = Object.freeze([
|
|||
Component: lazy(async () => import('./web-next/README.mdx')),
|
||||
metadata: webNext,
|
||||
},
|
||||
{
|
||||
order: 1.1,
|
||||
id: 'spa-react',
|
||||
Logo: lazy(async () => import('./spa-react/logo.svg')),
|
||||
Component: lazy(async () => import('./spa-react/README.mdx')),
|
||||
metadata: spaReact,
|
||||
},
|
||||
{
|
||||
order: 1.1,
|
||||
id: 'web-next-app-router',
|
||||
|
@ -102,61 +46,117 @@ const guides: Readonly<Guide[]> = Object.freeze([
|
|||
metadata: webNextAppRouter,
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
order: 1.2,
|
||||
id: 'm2m-general',
|
||||
Logo: lazy(async () => import('./m2m-general/logo.svg')),
|
||||
Component: lazy(async () => import('./m2m-general/README.mdx')),
|
||||
metadata: m2mGeneral,
|
||||
},
|
||||
{
|
||||
order: 1.2,
|
||||
id: 'web-express',
|
||||
Logo: lazy(async () => import('./web-express/logo.svg')),
|
||||
Component: lazy(async () => import('./web-express/README.mdx')),
|
||||
metadata: webExpress,
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
order: 1.3,
|
||||
id: 'web-go',
|
||||
Logo: lazy(async () => import('./web-go/logo.svg')),
|
||||
Component: lazy(async () => import('./web-go/README.mdx')),
|
||||
metadata: webGo,
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
id: 'web-python',
|
||||
Logo: lazy(async () => import('./web-python/logo.svg')),
|
||||
Component: lazy(async () => import('./web-python/README.mdx')),
|
||||
metadata: webPython,
|
||||
order: 1.5,
|
||||
id: 'web-gpt-plugin',
|
||||
Logo: lazy(async () => import('./web-gpt-plugin/logo.svg')),
|
||||
Component: lazy(async () => import('./web-gpt-plugin/README.mdx')),
|
||||
metadata: webGptPlugin,
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
order: 1.6,
|
||||
id: 'spa-vue',
|
||||
Logo: lazy(async () => import('./spa-vue/logo.svg')),
|
||||
Component: lazy(async () => import('./spa-vue/README.mdx')),
|
||||
metadata: spaVue,
|
||||
},
|
||||
{
|
||||
order: 1.7,
|
||||
id: 'native-ios-swift',
|
||||
Logo: lazy(async () => import('./native-ios-swift/logo.svg')),
|
||||
Component: lazy(async () => import('./native-ios-swift/README.mdx')),
|
||||
metadata: nativeIosSwift,
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
id: 'native-android-kt',
|
||||
Logo: lazy(async () => import('./native-android-kt/logo.svg')),
|
||||
Component: lazy(async () => import('./native-android-kt/README.mdx')),
|
||||
metadata: nativeAndroidKt,
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
id: 'spa-vanilla',
|
||||
Logo: lazy(async () => import('./spa-vanilla/logo.svg')),
|
||||
Component: lazy(async () => import('./spa-vanilla/README.mdx')),
|
||||
metadata: spaVanilla,
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
id: 'web-php',
|
||||
Logo: lazy(async () => import('./web-php/logo.svg')),
|
||||
Component: lazy(async () => import('./web-php/README.mdx')),
|
||||
metadata: webPhp,
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
order: 3,
|
||||
id: 'native-android-java',
|
||||
Logo: lazy(async () => import('./native-android-java/logo.svg')),
|
||||
Component: lazy(async () => import('./native-android-java/README.mdx')),
|
||||
metadata: nativeAndroidJava,
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
id: 'web-python',
|
||||
Logo: lazy(async () => import('./web-python/logo.svg')),
|
||||
Component: lazy(async () => import('./web-python/README.mdx')),
|
||||
metadata: webPython,
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
id: 'native-capacitor',
|
||||
Logo: lazy(async () => import('./native-capacitor/logo.svg')),
|
||||
Component: lazy(async () => import('./native-capacitor/README.mdx')),
|
||||
metadata: nativeCapacitor,
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
id: 'web-remix',
|
||||
Logo: lazy(async () => import('./web-remix/logo.svg')),
|
||||
Component: lazy(async () => import('./web-remix/README.mdx')),
|
||||
metadata: webRemix,
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
order: 5,
|
||||
id: 'native-flutter',
|
||||
Logo: lazy(async () => import('./native-flutter/logo.svg')),
|
||||
Component: lazy(async () => import('./native-flutter/README.mdx')),
|
||||
metadata: nativeFlutter,
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
id: 'web-asp-net-core',
|
||||
Logo: lazy(async () => import('./web-asp-net-core/logo.svg')),
|
||||
Component: lazy(async () => import('./web-asp-net-core/README.mdx')),
|
||||
metadata: webAspNetCore,
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
order: 6,
|
||||
id: 'web-outline',
|
||||
Logo: lazy(async () => import('./web-outline/logo.svg')),
|
||||
Component: lazy(async () => import('./web-outline/README.mdx')),
|
||||
metadata: webOutline,
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
id: 'web-gpt-plugin',
|
||||
Logo: lazy(async () => import('./web-gpt-plugin/logo.svg')),
|
||||
Component: lazy(async () => import('./web-gpt-plugin/README.mdx')),
|
||||
metadata: webGptPlugin,
|
||||
},
|
||||
]);
|
||||
|
||||
export default guides;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import ApplicationCredentials from '@/mdx-components-v2/ApplicationCredentials';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
import ApplicationCredentials from '@/mdx-components/ApplicationCredentials';
|
||||
import EnableAdminAccess from './components/EnableAdminAccess';
|
||||
import EnableAdminAccessSrc from './assets/enable-admin-access.png';
|
||||
import AppIdentifierSrc from './assets/api-identifier.png';
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"order": 1.2
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/UriInputField';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 2
|
||||
"order": 3
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/UriInputField';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 2.1
|
||||
"order": 2
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import UriInputField from '@/mdx-components/UriInputField';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
import capaticorIos from './assets/capacitor-ios.webp';
|
||||
import logtoSignInPage from './assets/logto-sign-in-page.webp';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import UriInputField from '@/mdx-components/UriInputField';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 3
|
||||
"order": 5
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 1
|
||||
"order": 1.7
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 1
|
||||
"order": 1.1
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
@ -115,7 +115,6 @@ After signing out, it'll be great to redirect user back to your website. Let's a
|
|||
|
||||
<UriInputField
|
||||
appId={props.app.id}
|
||||
isSingle={!props.isCompact}
|
||||
name="postLogoutRedirectUris"
|
||||
title="application_details.post_sign_out_redirect_uri"
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 3
|
||||
"order": 2
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 2
|
||||
"order": 1.6
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 7
|
||||
"order": 5
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 2
|
||||
"order": 1.2
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import UriInputField from '@/mdx-components/UriInputField';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Steps>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 3
|
||||
"order": 1.3
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
import UriInputField from '@/mdx-components/UriInputField';
|
||||
|
||||
import AlwaysIssueRefreshToken from './components/AlwaysIssueRefreshToken';
|
||||
import AiPluginJson from './components/AiPluginJson';
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 9
|
||||
"order": 1.5
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"order": 1.4
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import Steps from '@/mdx-components-v2/Steps';
|
||||
import Step from '@/mdx-components-v2/Step';
|
||||
import UriInputField from '@/mdx-components/UriInputField';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
import logtoSignInExperience from './assets/logto-sign-in-experience.png';
|
||||
import outlineHome from './assets/outline-home.png';
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 8
|
||||
"order": 6
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 5
|
||||
"order": 2
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 4
|
||||
"order": 3
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import UriInputField from '@/mdx-components-v2/UriInputField';
|
||||
import UriInputField from '@/mdx-components/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';
|
||||
import Steps from '@/mdx-components/Steps';
|
||||
import Step from '@/mdx-components/Step';
|
||||
|
||||
<Steps>
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"order": 6
|
||||
"order": 4
|
||||
}
|
||||
|
|
|
@ -1,229 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="Integrate Logto Android SDK"
|
||||
subtitle="Add Logto SDK as a dependency"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
<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:
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="kotlin" label="Kotlin">
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("io.logto.sdk:android:1.0.0")
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="groovy" label="Groovy">
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'io.logto.sdk:android:1.0.0'
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Init LogtoClient"
|
||||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="kotlin" label="Kotlin">
|
||||
|
||||
<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.appId}",
|
||||
scopes = null,
|
||||
resources = null,
|
||||
usingPersistStorage = true,
|
||||
)
|
||||
|
||||
val logtoClient = LogtoClient(logtoConfig, application)
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="java" label="Java">
|
||||
|
||||
<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.appId}",
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
logtoClient = new LogtoClient(logtoConfig, getApplication());
|
||||
}
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Sign in"
|
||||
subtitle="2 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Configure Redirect URI
|
||||
|
||||
First, let’s configure your redirect URI. E.g. `io.logto.android://io.logto.sample/callback`
|
||||
|
||||
<UriInputField
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="redirectUris"
|
||||
title="application_details.redirect_uri"
|
||||
/>
|
||||
|
||||
Go back to your IDE/editor, use the following code to implement sign-in:
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="kotlin" label="Kotlin">
|
||||
|
||||
<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>
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="java" label="Java">
|
||||
|
||||
<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>
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
After signing in successfully, `logtoClient.isAuthenticated` will be `true`.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Sign out"
|
||||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
Calling `.signOut(completion)` will always clear local credentials even if errors occurred.
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="kotlin" label="Kotlin">
|
||||
|
||||
```kotlin
|
||||
logtoClient.signOut { logtoException: LogtoException? ->
|
||||
// Local credentials are cleared regardless of whether `logtoException` is null.
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="java" label="Java">
|
||||
|
||||
```java
|
||||
logtoClient.signOut(logtoException -> {
|
||||
// Local credentials are cleared regardless of whether `logtoException` is null.
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Further readings"
|
||||
subtitle="4 articles"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [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>
|
|
@ -1,222 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="集成 Logto Android SDK"
|
||||
subtitle="将 Logto SDK 添加至依赖"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
Logto Andorid SDK 支持的最小 Android API 级别为 24
|
||||
</InlineNotification>
|
||||
|
||||
将 `mavenCentral()` 添加到构建脚本中:
|
||||
|
||||
```kotlin
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
```
|
||||
|
||||
添加 Logto Android SDK 依赖:
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="kotlin" label="Kotlin">
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("io.logto.sdk:android:1.0.0")
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="groovy" label="Groovy">
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'io.logto.sdk:android:1.0.0'
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="初始化 LogtoClient"
|
||||
subtitle="共 1 步"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="kotlin" label="Kotlin">
|
||||
|
||||
<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 ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
appId = "${props.appId}",
|
||||
scopes = null,
|
||||
resources = null,
|
||||
usingPersistStorage = true,
|
||||
)
|
||||
|
||||
val logtoClient = LogtoClient(logtoConfig, application)
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="java" label="Java">
|
||||
|
||||
<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 ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
"${props.appId}",
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
logtoClient = new LogtoClient(logtoConfig, getApplication());
|
||||
}
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="登录"
|
||||
subtitle="共 2 步"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
||||
首先,我们来添加 Redirect URI。例如 `io.logto.android://io.logto.sample/callback`
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="application_details.redirect_uri" />
|
||||
|
||||
返回你的 IDE 或编辑器,使用如下代码实现登录:
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="kotlin" label="Kotlin">
|
||||
|
||||
<pre>
|
||||
<code className="language-kotlin">
|
||||
{`logtoClient.signIn(this, "${props.redirectUris[0] ?? '<your-redirect-uri>'}") { logtoException: LogtoException? ->
|
||||
// 当 \`logtoException\` 为 null 时,则登录成功。
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="java" label="Java">
|
||||
|
||||
<pre>
|
||||
<code className="language-java">
|
||||
{`logtoClient.signIn(this, "${props.redirectUris[0] ?? '<your-redirect-uri>'}", logtoException -> {
|
||||
// 当 \`logtoException\` 为 null 时,则登录成功。
|
||||
});`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
当成功登录后,`logtoClient.isAuthenticated` 的值将为 `true`。
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="退出登录"
|
||||
subtitle="共 1 步"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
调用 `.signOut(completion)` 操作会清除本地存储的用户相关凭据,即使在退出登录过程中发生了异常。
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="kotlin" label="Kotlin">
|
||||
|
||||
```kotlin
|
||||
logtoClient.signOut { logtoException: LogtoException? ->
|
||||
// 无论是否存在 `logtoException`,本地存储的用户相关凭据都已清除。
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="java" label="Java">
|
||||
|
||||
```java
|
||||
logtoClient.signOut(logtoException -> {
|
||||
// 无论是否存在 `logtoException`,本地存储的用户相关凭据都已清除。
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="延展阅读"
|
||||
subtitle="共 4 篇"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie)
|
||||
- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in)
|
||||
- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)
|
||||
- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api)
|
||||
|
||||
</Step>
|
|
@ -1,227 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import { buildIdGenerator } from '@logto/shared/universal';
|
||||
|
||||
<Step
|
||||
title="Add dependencies"
|
||||
subtitle="Please select your favorite package manager"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
||||
```bash
|
||||
npm i @logto/express cookie-parser express-session
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
```bash
|
||||
yarn add @logto/express cookie-parser express-session
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm add @logto/express cookie-parser express-session
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Init LogtoClient"
|
||||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<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">
|
||||
{`import LogtoClient from '@logto/express';
|
||||
|
||||
export const logtoClient = new LogtoClient({
|
||||
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||
appId: '${props.appId}',
|
||||
appSecret: '${props.appSecret}',
|
||||
baseUrl: 'http://localhost:3000', // Change to your own base URL
|
||||
});`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Prepare required middlewares"
|
||||
subtitle="1 step"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
The SDK requires [express-session](https://www.npmjs.com/package/express-session) to be configured in prior.
|
||||
|
||||
<pre>
|
||||
<code className="language-ts">
|
||||
{`import cookieParser from 'cookie-parser';
|
||||
import session from 'express-session';
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(session({ secret: '${buildIdGenerator(32)()}', cookie: { maxAge: 14 * 24 * 60 * 60 } }));`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Sign in"
|
||||
subtitle="3 steps"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
### Configure Redirect URI
|
||||
|
||||
First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`.
|
||||
|
||||
<UriInputField
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="redirectUris"
|
||||
title="application_details.redirect_uri"
|
||||
/>
|
||||
|
||||
### Prepare Logto routes
|
||||
|
||||
Prepare routes to connect with Logto.
|
||||
|
||||
Go back to your IDE/editor, use the following code to implement the API routes first:
|
||||
|
||||
```ts
|
||||
import { handleAuthRoutes } from '@logto/express';
|
||||
|
||||
app.use(handleAuthRoutes(config));
|
||||
```
|
||||
|
||||
This will create 3 routes automatically:
|
||||
|
||||
1. `/logto/sign-in`: Sign in with Logto.
|
||||
2. `/logto/sign-in-callback`: Handle sign-in callback.
|
||||
3. `/logto/sign-out`: Sign out with Logto.
|
||||
|
||||
### Implement sign-in
|
||||
|
||||
We're almost there! Now, create a sign-in button to redirect to the sign-in route on user click.
|
||||
|
||||
```ts
|
||||
app.get('/', (req, res) => {
|
||||
res.setHeader('content-type', 'text/html');
|
||||
res.end(`<div><a href="/logto/sign-in">Sign In</a></div>`);
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Get user profile"
|
||||
subtitle="2 steps"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(5)}
|
||||
>
|
||||
|
||||
In order to get user profile, we need to use the `withLogto` middleware:
|
||||
|
||||
```ts
|
||||
import { withLogto } from '@logto/express';
|
||||
|
||||
app.use(withLogto(config));
|
||||
```
|
||||
|
||||
Then the user profile will be attached to `req`, example usage:
|
||||
|
||||
```ts
|
||||
app.get('/user', (req, res) => {
|
||||
res.json(req.user);
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Protect routes"
|
||||
subtitle="2 steps"
|
||||
index={5}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(6)}
|
||||
>
|
||||
|
||||
After setting up `withLogto` in the previous step, we can protect routes by creating a simple middleware:
|
||||
|
||||
```ts
|
||||
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user.isAuthenticated) {
|
||||
res.redirect('/logto/sign-in');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
And then:
|
||||
|
||||
```ts
|
||||
app.get('/protected', requireAuth, (req, res) => {
|
||||
res.end('protected resource');
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Sign out"
|
||||
subtitle="1 step"
|
||||
index={6}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(7)}
|
||||
>
|
||||
|
||||
Calling `/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 your user back to your website. Let's add `http://localhost:3000` as one of the Post Sign-out URIs in Admin Console (shows under Redirect URIs).
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Further readings"
|
||||
subtitle="4 articles"
|
||||
index={7}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [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>
|
|
@ -1,222 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import { buildIdGenerator } from '@logto/shared/universal';
|
||||
|
||||
<Step
|
||||
title="添加依赖"
|
||||
subtitle="选择你熟悉的包管理工具"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
||||
```bash
|
||||
npm i @logto/express cookie-parser express-session
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
```bash
|
||||
yarn add @logto/express cookie-parser express-session
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm add @logto/express cookie-parser express-session
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="初始化 LogtoClient"
|
||||
subtitle="共 1 步"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
在如下代码示例中, 我们均先假设你的 React 应用运行在 <code>http://localhost:3000</code> 上。
|
||||
</InlineNotification>
|
||||
|
||||
引入并实例化 LogtoClient:
|
||||
|
||||
<pre>
|
||||
<code className="language-ts">
|
||||
{`import LogtoClient from '@logto/express';
|
||||
|
||||
export const logtoClient = new LogtoClient({
|
||||
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
appId: '${props.appId}',
|
||||
appSecret: '${props.appSecret}',
|
||||
baseUrl: 'http://localhost:3000', // 你可以修改为自己真实的 URL
|
||||
});`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="准备前置中间件"
|
||||
subtitle="共 1 步"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
本 SDK 要求预先安装并配置好 [express-session](https://www.npmjs.com/package/express-session)。
|
||||
|
||||
<pre>
|
||||
<code className="language-ts">
|
||||
{`import cookieParser from 'cookie-parser';
|
||||
import session from 'express-session';
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(session({ secret: '${buildIdGenerator(32)()}', cookie: { maxAge: 14 * 24 * 60 * 60 } }));`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="登录"
|
||||
subtitle="共 3 步"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
||||
首先,我们来添加 Redirect URI,如:`http://localhost:3000/api/logto/sign-in-callback`.
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="application_details.redirect_uri" />
|
||||
|
||||
### 准备 Logto 路由
|
||||
|
||||
准备与 Logto 后台交互的路由。
|
||||
|
||||
返回你的 IDE 或编辑器,首先让我们使用如下代码来实现一组 API 路由:
|
||||
|
||||
```ts
|
||||
import { handleAuthRoutes } from '@logto/express';
|
||||
|
||||
app.use(handleAuthRoutes(config));
|
||||
```
|
||||
|
||||
这将为你自动创建好 3 个路由,分别是:
|
||||
|
||||
1. `/logto/sign-in`: 登录
|
||||
2. `/logto/sign-in-callback`: 处理登录重定向
|
||||
3. `/logto/sign-out`: 登出
|
||||
|
||||
### 实现登录
|
||||
|
||||
马上就要大功告成!创建一个登录按钮,点击后将会跳转到登录路由。
|
||||
|
||||
```ts
|
||||
app.get('/', (req, res) => {
|
||||
res.setHeader('content-type', 'text/html');
|
||||
res.end(`<div><a href="/logto/sign-in">Sign In</a></div>`);
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="获取用户信息"
|
||||
subtitle="共 2 步"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(5)}
|
||||
>
|
||||
|
||||
需要集成 `withLogto` 中间件来获取用户信息:
|
||||
|
||||
```ts
|
||||
import { withLogto } from '@logto/express';
|
||||
|
||||
app.use(withLogto(config));
|
||||
```
|
||||
|
||||
之后用户信息将会被注入到 `req`, 用法举例:
|
||||
|
||||
```ts
|
||||
app.get('/user', (req, res) => {
|
||||
res.json(req.user);
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="保护路由"
|
||||
subtitle="共 2 步"
|
||||
index={5}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(6)}
|
||||
>
|
||||
|
||||
根据前面的步骤配置好 `withLogto` 后, 我们可以创建一个简单的中间件来保护路由:
|
||||
|
||||
```ts
|
||||
const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user.isAuthenticated) {
|
||||
res.redirect('/logto/sign-in');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
```
|
||||
|
||||
然后:
|
||||
|
||||
```ts
|
||||
app.get('/protected', requireAuth, (req, res) => {
|
||||
res.end('protected resource');
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="退出登录"
|
||||
subtitle="共 1 步"
|
||||
index={6}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(7)}
|
||||
>
|
||||
|
||||
调用 `/logto/sign-out` 将清理内存与 cookies 中的所有 Logto 数据(如果有)。
|
||||
|
||||
在退出登录后,让你的用户重新回到你的网站是个不错的选择。让我们将 `http://localhost:3000` 添加至「管理控制台」里的 Post Sign-out URIs 中(位于 Redirect URIs 下方)。
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="延展阅读"
|
||||
subtitle="共 4 篇"
|
||||
index={6}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie)
|
||||
- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in)
|
||||
- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)
|
||||
- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api)
|
||||
|
||||
</Step>
|
|
@ -1,391 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="Add Logto SDK as a dependency"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<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"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
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"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Create LogtConfig
|
||||
|
||||
<pre>
|
||||
<code className="language-go">
|
||||
{`// main.go
|
||||
func main() {
|
||||
// ...
|
||||
|
||||
logtoConfig := &client.LogtoConfig{
|
||||
Endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||
AppId: "${props.appId}",
|
||||
AppSecret: "${props.appSecret}",
|
||||
}
|
||||
|
||||
// ...
|
||||
}`}
|
||||
</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"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
### 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
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="redirectUris"
|
||||
title="application_details.redirect_uri"
|
||||
/>
|
||||
|
||||
### 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"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(5)}
|
||||
>
|
||||
|
||||
### Configure Post Sign-out Redirect URI
|
||||
|
||||
Add `http://localhost:8080` to the Post Sign-out Redirect URI filed:
|
||||
|
||||
<UriInputField
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="postLogoutUris"
|
||||
title="application_details.post_sign_out_redirect_uri"
|
||||
/>
|
||||
|
||||
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"
|
||||
index={5}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [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>
|
|
@ -1,371 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="为项目安装 Logto SDK 依赖"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<InlineNotification>
|
||||
在本指南中,我们基于 <a href="https://gin-gonic.com">Gin Web 框架</a> 示范 SDK 的集成过程。你也可以采取同样的步骤轻松地将 Logto 集成到其他的 Web 框架中。
|
||||
在示例代码中,我们假定你的应用运行在 <code>http://localhost:8080</code> 上。
|
||||
</InlineNotification>
|
||||
|
||||
在项目目录下执行:
|
||||
|
||||
```bash
|
||||
go get github.com/logto-io/go
|
||||
```
|
||||
|
||||
将 `github.com/logto-io/go/client` 包依赖添加到到代码中:
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
// 添加依赖
|
||||
"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="使用 session 存储用户认证信息"
|
||||
subtitle="共 2 步"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
在传统网页应用中,用户的认证信息将会被存储在用户的 session 中。
|
||||
|
||||
Logto SDK 提供了一个 `Storage` 接口,你可以结合自己所使用的网络框架实现一个 `Storage` 的适配器,使 Logto SDK 能将用户认证信息存储到 session 中。
|
||||
|
||||
<InlineNotification>
|
||||
我们推荐使用非 cookie 的 session,因为 Logto 所存储的信息可能会超过 cookie 的大小限制。在示例中我们使用基于内存 session,在实际项目中,你可以根据需要使用 Redis、 MongoDB 等技术来存储 session。
|
||||
</InlineNotification>
|
||||
|
||||
Logto SDK 中的 `Storage` 类型如下:
|
||||
|
||||
```go
|
||||
// github.com/logto-io/client/storage.go
|
||||
pacakge client
|
||||
|
||||
type Storage interface {
|
||||
GetItem(key string) string
|
||||
SetItem(key, value string)
|
||||
}
|
||||
```
|
||||
|
||||
我们将以 [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) 为例,示范这个过程。
|
||||
|
||||
### 在应用中使用 Session 中间件
|
||||
|
||||
在应用中使用 [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) 中间件,这样在请求的路由处理方法中就可以根据用户请求的上下文获取用户的 session:
|
||||
|
||||
```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()
|
||||
|
||||
// 示例中使用基于内存的 session
|
||||
store := memstore.NewStore([]byte("your session secret"))
|
||||
router.Use(sessions.Sessions("logto-session", store))
|
||||
|
||||
router.GET("/", func(ctx *gin.Context) {
|
||||
// 获取用户的 session
|
||||
session := sessions.Default(ctx)
|
||||
// ...
|
||||
ctx.String(200, "Hello Logto!")
|
||||
})
|
||||
router.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 创建 session storage 供 Logto 存储用户认证信息
|
||||
|
||||
创建一个 `session_storage.go` 文件,定义一个 `SessionStorage` 并实现 Logto SDK 定义的 `Storage` 的接口:
|
||||
|
||||
```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()
|
||||
}
|
||||
```
|
||||
|
||||
至此,你就可以在请求的路由处理方法中通过以下方式创建一个 session storage 给 Logto 使用了:
|
||||
|
||||
```go
|
||||
session := sessions.Default(ctx)
|
||||
sessionStorage := &SessionStorage{session: session}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="初始化 LogtoClient"
|
||||
subtitle="共 2 步"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### 创建 Logto 配置
|
||||
|
||||
<pre>
|
||||
<code className="language-go">
|
||||
{`// main.go
|
||||
func main() {
|
||||
// ...
|
||||
|
||||
logtoConfig := &client.LogtoConfig{
|
||||
Endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
AppId: "${props.appId}",
|
||||
AppSecret: "${props.appSecret}",
|
||||
}
|
||||
|
||||
// ...
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
### 为每个请求初始化 LogtoClient
|
||||
|
||||
```go
|
||||
// main.go
|
||||
func main() {
|
||||
// ...
|
||||
|
||||
router.GET("/", func(ctx *gin.Context) {
|
||||
// 初始化 LogtoClient
|
||||
session := sessions.Default(ctx)
|
||||
logtoClient := client.NewLogtoClient(
|
||||
logtoConfig,
|
||||
&SessionStorage{session: session},
|
||||
)
|
||||
|
||||
// 使用 Logto 来控制首页的显示内容
|
||||
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="登录"
|
||||
subtitle="共 3 步"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
||||
将 `http://localhost:8080/sign-in-callback` 添加到 Redirect URI,使用户登录 Logto 后能重定向到应用处理登录回调的 `/sign-in-callback` 路由:
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="application_details.redirect_uri" />
|
||||
|
||||
### 添加处理登录请求路由
|
||||
|
||||
```go
|
||||
//main.go
|
||||
func main() {
|
||||
// ...
|
||||
|
||||
// 在 Home 页面添加登录请求的入口
|
||||
router.GET("/", func(ctx *gin.Context) {
|
||||
// ...
|
||||
homePage := `<h1>Hello Logto</h1>` +
|
||||
"<div>" + authState + "</div>" +
|
||||
// 添加登录请求的入口
|
||||
`<div><a href="/sign-in">Sign In</a></div>`
|
||||
|
||||
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
|
||||
})
|
||||
|
||||
// 添加处理登录请求的路由
|
||||
router.GET("/sign-in", func(ctx *gin.Context) {
|
||||
session := sessions.Default(ctx)
|
||||
logtoClient := client.NewLogtoClient(
|
||||
logtoConfig,
|
||||
&SessionStorage{session: session},
|
||||
)
|
||||
|
||||
// 由 Logto 处理登录请求,指定登录成功后重定向到 Redirect URI
|
||||
signInUri, err := logtoClient.SignIn("http://localhost:8080/sign-in-callback")
|
||||
if err != nil {
|
||||
ctx.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 将页面重定向到 Logto 登录页
|
||||
ctx.Redirect(http.StatusTemporaryRedirect, signInUri)
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 添加处理登录回调路由
|
||||
|
||||
当我们在 Logto 登录页登录成功后,Logto 会将用户重定向 Redirect URI。
|
||||
因 Redirect URI 是 `http://localhost:8080/sign-in-callback`,所以我们添加 `/sign-in-callback` 路由来处理登录后的回调。
|
||||
|
||||
```go
|
||||
// main.go
|
||||
func main() {
|
||||
// ...
|
||||
|
||||
// 添加处理登录回调的路由
|
||||
router.GET("/sign-in-callback", func(ctx *gin.Context) {
|
||||
session := sessions.Default(ctx)
|
||||
logtoClient := client.NewLogtoClient(
|
||||
logtoConfig,
|
||||
&SessionStorage{session: session},
|
||||
)
|
||||
|
||||
// 由 Logto 处理登录回调
|
||||
err := logtoClient.HandleSignInCallback(ctx.Request)
|
||||
if err != nil {
|
||||
ctx.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 根据需求在登录成功后跳转到某个页面。(该示例中使用户回到首页)
|
||||
ctx.Redirect(http.StatusTemporaryRedirect, "/")
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="退出登录"
|
||||
subtitle="共 2 步"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(5)}
|
||||
>
|
||||
|
||||
### 配置 Post Sign-out Redirect URI
|
||||
|
||||
为应用添加 Post Sign-out Redirect URI,使用户退出登录 Logto 之后将用户重定向回我们的应用。
|
||||
将 `http://localhost:8080` 添加到 Post Sign-out Redirect URI,使用户退出登录后回到应用首页:
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="postLogoutUris" title="application_details.post_sign_out_redirect_uri" />
|
||||
|
||||
### 添加退出登录请求路由
|
||||
|
||||
```go
|
||||
//main.go
|
||||
func main() {
|
||||
// ...
|
||||
|
||||
// 在 Home 页面添加退出登录请求的入口
|
||||
router.GET("/", func(ctx *gin.Context) {
|
||||
// ...
|
||||
homePage := `<h1>Hello Logto</h1>` +
|
||||
"<div>" + authState + "</div>" +
|
||||
`<div><a href="/sign-in">Sign In</a></div>` +
|
||||
// 添加退出登录请求的入口
|
||||
`<div><a href="/sign-out">Sign Out</a></div>`
|
||||
|
||||
ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
|
||||
})
|
||||
|
||||
// 添加处理退出登录请求的路由
|
||||
router.GET("/sign-out", func(ctx *gin.Context) {
|
||||
session := sessions.Default(ctx)
|
||||
logtoClient := client.NewLogtoClient(
|
||||
logtoConfig,
|
||||
&SessionStorage{session: session},
|
||||
)
|
||||
|
||||
// 由 Logto 处理退出登录请求,指定用户退出登录后重定向回 Post Sign-out Redirect URI
|
||||
signOutUri, signOutErr := logtoClient.SignOut("http://localhost:8080")
|
||||
|
||||
if signOutErr != nil {
|
||||
ctx.String(http.StatusOK, signOutErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(http.StatusTemporaryRedirect, signOutUri)
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
当用户发起退出登录后,Logto 会清除 session 中所有用户相关的认证信息。
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="延展阅读"
|
||||
subtitle="共 4 篇"
|
||||
index={5}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie)
|
||||
- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in)
|
||||
- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)
|
||||
- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api)
|
||||
|
||||
</Step>
|
|
@ -1,151 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="Integrate Logto Swift SDK"
|
||||
subtitle="Add Logto SDK as a dependency"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
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"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<pre>
|
||||
<code className="language-swift">
|
||||
{`import Logto
|
||||
import LogtoClient
|
||||
|
||||
let config = try? LogtoConfig(
|
||||
endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||
appId: "${props.appId}"
|
||||
)
|
||||
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"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Configure Redirect URI
|
||||
|
||||
First, let’s configure your redirect URI scheme. E.g. `io.logto://callback`
|
||||
|
||||
<UriInputField
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="redirectUris"
|
||||
title="application_details.redirect_uri"
|
||||
/>
|
||||
|
||||
<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"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
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"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [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>
|
|
@ -1,139 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="集成 Logto Swift SDK"
|
||||
subtitle="将 Logto SDK 添加至依赖"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
|
||||
使用如下的 URL 将 Logto SDK 添加至 Swift Package Manager 的依赖中。
|
||||
|
||||
```bash
|
||||
https://github.com/logto-io/swift.git
|
||||
```
|
||||
|
||||
从 Xcode 11 开始,无需任何额外工具你就可以 [引入一个 Swift package](https://developer.apple.com/documentation/swift_packages/adding_package_dependencies_to_your_app)。
|
||||
|
||||
因为一些技术原因,我们暂时不支持 **Carthage** 和 **CocoaPods**。
|
||||
|
||||
<details>
|
||||
<summary>Carthage</summary>
|
||||
|
||||
Carthage [需要创建一个 `xcodeproj` 文件才能编译](https://github.com/Carthage/Carthage/issues/1226#issuecomment-290931385),但由于我们内置了一些社交插件所用到的二进制目标文件,导致使用 `swift package generate-xcodeproj` 命令时报错。我们会继续努力寻求解决方案。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>CocoaPods</summary>
|
||||
|
||||
CocoaPods [不支持本地依赖](https://github.com/CocoaPods/CocoaPods/issues/3276) 和 monorepo,所以要在此工程创建 `.podspec` 文件使用 Cocoapods 的话将非常困难。
|
||||
|
||||
</details>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="初始化 LogtoClient"
|
||||
subtitle="共 1 步"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<pre>
|
||||
<code className="language-swift">
|
||||
{`import Logto
|
||||
import LogtoClient
|
||||
|
||||
let config = try? LogtoConfig(
|
||||
endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
appId: "${props.appId}"
|
||||
)
|
||||
let logtoClient = LogtoClient(useConfig: config)`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
我们默认会把例如 ID Token 和 Refresh Token 这样的凭据存储在 Keychain 中,如此一来用户在重新打开应用的时候无需再次登录。
|
||||
|
||||
如果需要禁用这个行为,可将 `usingPersistStorage` 设置成 `false`:
|
||||
|
||||
```swift
|
||||
let config = try? LogtoConfig(
|
||||
// ...
|
||||
usingPersistStorage: false
|
||||
)
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="登录"
|
||||
subtitle="共 2 步"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
||||
首先,我们来配置你的 redirect URI scheme。例如 `io.logto://callback`
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="application_details.redirect_uri" />
|
||||
|
||||
<InlineNotification>
|
||||
iOS SDK 中的 Redirect URI 仅用于内部。除非连接器有要求,否则 <em>无需</em> 在项目中添加 <a href="https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app">Custom URL Scheme</a>。
|
||||
</InlineNotification>
|
||||
|
||||
回到 Xcode,使用如下代码实现登录:
|
||||
|
||||
<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 {
|
||||
// 登录过程中有错误发生
|
||||
}`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="退出登录"
|
||||
subtitle="共 1 步"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
调用 `.signOut()` 将清除 Keychain 中所有 Logto 的数据(如果有)。
|
||||
|
||||
```swift
|
||||
await client.signOut()
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="延展阅读"
|
||||
subtitle="共 4 篇"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie)
|
||||
- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in)
|
||||
- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)
|
||||
- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api)
|
||||
|
||||
</Step>
|
|
@ -1,276 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import { buildIdGenerator } from '@logto/shared/universal';
|
||||
|
||||
<Step
|
||||
title="Add Logto SDK as a dependency"
|
||||
subtitle="Please select your favorite package manager"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<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"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<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.appId}',
|
||||
appSecret: '${props.appSecret}',
|
||||
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"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### Configure Redirect URI
|
||||
|
||||
First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`.
|
||||
|
||||
<UriInputField
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="redirectUris"
|
||||
title="application_details.redirect_uri"
|
||||
/>
|
||||
|
||||
### 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"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
### 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"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(5)}
|
||||
>
|
||||
|
||||
### 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"
|
||||
index={5}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(6)}
|
||||
>
|
||||
|
||||
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
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="postLogoutRedirectUris"
|
||||
title="application_details.post_sign_out_redirect_uri"
|
||||
/>
|
||||
|
||||
### Implement a sign-out button
|
||||
|
||||
```tsx
|
||||
<button onClick={() => push('/api/logto/sign-out')}>Sign Out</button>
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Further readings"
|
||||
subtitle="4 articles"
|
||||
index={6}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [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>
|
|
@ -1,270 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import { buildIdGenerator } from '@logto/shared/universal';
|
||||
|
||||
<Step
|
||||
title="将 Logto SDK 添加至依赖"
|
||||
subtitle="选择你熟悉的包管理工具"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<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="初始化 LogtoClient"
|
||||
subtitle="共 1 步"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
在如下代码示例中, 我们均先假设你的 React 应用运行在 <code>http://localhost:3000</code> 上。
|
||||
</InlineNotification>
|
||||
|
||||
引入并实例化 LogtoClient:
|
||||
|
||||
<pre>
|
||||
<code className="language-ts">
|
||||
{`// libraries/logto.js
|
||||
import LogtoClient from '@logto/next';
|
||||
|
||||
export const logtoClient = new LogtoClient({
|
||||
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
appId: '${props.appId}',
|
||||
appSecret: '${props.appSecret}',
|
||||
baseUrl: 'http://localhost:3000', // 你可以修改为自己真实的 URL
|
||||
cookieSecret: '${buildIdGenerator(32)()}', // Logto 自动帮你生成的 32 位密钥
|
||||
cookieSecure: process.env.NODE_ENV === 'production',
|
||||
});`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="登录"
|
||||
subtitle="共 3 步"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
||||
首先,我们来添加 Redirect URI,如:`http://localhost:3000/api/logto/sign-in-callback`.
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="application_details.redirect_uri" />
|
||||
|
||||
### 准备 API 路由
|
||||
|
||||
实现与 Logto 后台交互的 [API 路由](https://nextjs.org/docs/api-routes/introduction)。
|
||||
|
||||
返回你的 IDE 或编辑器,首先让我们使用如下代码来实现一组 API 路由:
|
||||
|
||||
```ts
|
||||
// pages/api/logto/[action].ts
|
||||
import { logtoClient } from '../../../libraries/logto';
|
||||
|
||||
export default logtoClient.handleAuthRoutes();
|
||||
```
|
||||
|
||||
这将为你自动创建好 4 个路由,分别是:
|
||||
|
||||
1. `/api/logto/sign-in`:登录
|
||||
2. `/api/logto/sign-in-callback`:处理登录重定向
|
||||
3. `/api/logto/sign-out`:登出
|
||||
4. `/api/logto/user`:检查用户是否已通过 Logto 获得授权。如果是,则返回用户信息。
|
||||
|
||||
### 实现登录按钮
|
||||
|
||||
马上就要大功告成!在这最后一步,我们将用如下代码实现一个登录按钮:
|
||||
|
||||
```tsx
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const { push } = useRouter();
|
||||
|
||||
<button onClick={() => push('/api/logto/sign-in')}>
|
||||
登录
|
||||
</button>
|
||||
```
|
||||
|
||||
现在你可以尝试点击登录按钮了,点击之后页面会跳转到 Logto 的登录界面。
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="获取用户信息"
|
||||
subtitle="两种方式"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
### 通过前端发送 API 请求获取
|
||||
|
||||
你可以调用 `/api/logto/user` 接口来获取用户信息,如:
|
||||
|
||||
```tsx
|
||||
import { LogtoUser } from '@logto/next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const Home = () => {
|
||||
const { data } = useSWR<LogtoUser>('/api/logto/user');
|
||||
|
||||
return <div>用户 ID:{data?.claims?.sub}</div>;
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
```
|
||||
|
||||
你可以查看 [这篇教程](https://swr.vercel.app/docs/getting-started) 来了解有关 `useSWR` 的更多信息。
|
||||
|
||||
### 通过后端的 `getServerSideProps` 方法获取
|
||||
|
||||
```tsx
|
||||
import { LogtoUser } from '@logto/next';
|
||||
import { logtoClient } from '../libraries/logto';
|
||||
|
||||
type Props = {
|
||||
user: LogtoUser;
|
||||
};
|
||||
|
||||
const Profile = ({ user }: Props) => {
|
||||
return <div>用户 ID:{user.claims?.sub}</div>;
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
||||
export const getServerSideProps = logtoClient.withLogtoSsr(({ request }) => {
|
||||
const { user } = request;
|
||||
|
||||
return {
|
||||
props: { user },
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
查看 [Next.js 官方文档](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) 来了解有关 `getServerSideProps` 的更多信息。
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="保护 API 和页面资源"
|
||||
subtitle="共 2 步"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(5)}
|
||||
>
|
||||
|
||||
### 保护 API 路由
|
||||
|
||||
使用 `logtoClient.withLogtoApiRoute` 来包裹 API 逻辑:
|
||||
|
||||
```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: '未授权' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
response.json({
|
||||
data: '这是受保护的 API 资源',
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 保护页面
|
||||
|
||||
如果你不想匿名用户访问你的某个页面,你可以使用 `logtoClient.withLogtoSsr` 来获取登录认证状态,如未登录则自动跳转到登录页面。
|
||||
|
||||
```ts
|
||||
export const getServerSideProps = logtoClient.withLogtoSsr(async function ({ request, response }) {
|
||||
const { user } = request;
|
||||
|
||||
if (!user.isAuthenticated) {
|
||||
response.setHeader('location', '/api/logto/sign-in');
|
||||
response.statusCode = 302;
|
||||
response.end();
|
||||
}
|
||||
|
||||
return {
|
||||
props: { user },
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="退出登录"
|
||||
subtitle="共 1 步"
|
||||
index={5}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(6)}
|
||||
>
|
||||
|
||||
调用 `/api/logto/sign-out` 将清理内存与 cookies 中的所有 Logto 数据(如果有)。
|
||||
|
||||
在退出登录后,让你的用户重新回到你的网站是个不错的选择。在调用 `/api/logto/sign-out` 发起退出登录操作之前,让我们先将 `http://localhost:3000` 添加至下面的输入框。
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="postLogoutRedirectUris" title="application_details.post_sign_out_redirect_uri" />
|
||||
|
||||
### 实现退出登录按钮
|
||||
|
||||
```tsx
|
||||
<button onClick={() => push('/api/logto/sign-out')}>
|
||||
退出登录
|
||||
</button>
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="延展阅读"
|
||||
subtitle="共 4 篇"
|
||||
index={6}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie)
|
||||
- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in)
|
||||
- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)
|
||||
- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api)
|
||||
|
||||
</Step>
|
|
@ -1,201 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="Add Logto SDK as a dependency"
|
||||
subtitle="Please select your favorite package manager"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
||||
```bash
|
||||
npm i @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
```bash
|
||||
yarn add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Init LogtoClient"
|
||||
subtitle="1 step"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
Import and use `LogtoProvider` to provide a Logto context:
|
||||
|
||||
<pre>
|
||||
<code className="language-tsx">
|
||||
{`import { LogtoProvider, LogtoConfig } from '@logto/react';
|
||||
|
||||
const config: LogtoConfig = {
|
||||
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
|
||||
appId: '${props.appId}',
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<LogtoProvider config={config}>
|
||||
<YourAppContent />
|
||||
</LogtoProvider>
|
||||
);`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Sign in"
|
||||
subtitle="3 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
|
||||
</InlineNotification>
|
||||
|
||||
### Configure Redirect URI
|
||||
|
||||
First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`.
|
||||
|
||||
<UriInputField
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="redirectUris"
|
||||
title="application_details.redirect_uri"
|
||||
/>
|
||||
|
||||
### Implement a sign-in button
|
||||
|
||||
We provide two hooks `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-tsx">
|
||||
{`import { useLogto } from '@logto/react';
|
||||
|
||||
const SignIn = () => {
|
||||
const { signIn, isAuthenticated } = useLogto();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <div>Signed in</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => 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.
|
||||
|
||||
First let's create a callback component:
|
||||
|
||||
```tsx
|
||||
import { useHandleSignInCallback } from '@logto/react';
|
||||
|
||||
const Callback = () => {
|
||||
const { isLoading } = useHandleSignInCallback(() => {
|
||||
// Navigate to root path when finished
|
||||
});
|
||||
|
||||
// When it's working in progress
|
||||
if (isLoading) {
|
||||
return <div>Redirecting...</div>;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Finally insert the code below to create a `/callback` route which does NOT require authentication:
|
||||
|
||||
```tsx
|
||||
// Assuming react-router
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Sign out"
|
||||
subtitle="1 step"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
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.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="postLogoutRedirectUris"
|
||||
title="application_details.post_sign_out_redirect_uri"
|
||||
/>
|
||||
|
||||
### Implement a sign-out button
|
||||
|
||||
<pre>
|
||||
<code className="language-tsx">
|
||||
{`const SignOut = () => {
|
||||
const { signOut } = useLogto();
|
||||
|
||||
return (
|
||||
<button onClick={() => signOut('${
|
||||
props.postLogoutRedirectUris[0] ?? 'http://localhost:3000'
|
||||
}')}>
|
||||
Sign out
|
||||
</button>
|
||||
);
|
||||
};`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Further readings"
|
||||
subtitle="4 articles"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [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>
|
|
@ -1,189 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="将 Logto SDK 添加至依赖"
|
||||
subtitle="选择你熟悉的包管理工具"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<Tabs>
|
||||
<TabItem value="npm" label="npm">
|
||||
|
||||
```bash
|
||||
npm i @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="yarn" label="Yarn">
|
||||
|
||||
```bash
|
||||
yarn add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="pnpm" label="pnpm">
|
||||
|
||||
```bash
|
||||
pnpm add @logto/react
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="初始化 LogtoClient"
|
||||
subtitle="共 1 步"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
Import 并使用 `LogtoProvider` 来提供 Logto context:
|
||||
|
||||
<pre>
|
||||
<code className="language-tsx">
|
||||
{`import { LogtoProvider, LogtoConfig } from '@logto/react';
|
||||
|
||||
const config: LogtoConfig = {
|
||||
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
appId: '${props.appId}',
|
||||
};
|
||||
|
||||
const App = () => (
|
||||
<LogtoProvider config={config}>
|
||||
<YourAppContent />
|
||||
</LogtoProvider>
|
||||
);`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="登录"
|
||||
subtitle="共 3 步"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
在如下代码示例中, 我们均先假设你的 React 应用运行在 <code>http://localhost:3000</code> 上。
|
||||
</InlineNotification>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
||||
首先,我们来添加 Redirect URI,如:`http://localhost:3000/callback`。
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="application_details.redirect_uri" />
|
||||
|
||||
### 实现登录按钮
|
||||
|
||||
我们提供了两个 hook 方法 `useHandleSignInCallback()` 和 `useLogto()`,它们可以帮助你轻松完成登录认证流程。
|
||||
|
||||
返回你的 IDE 或编辑器,使用如下代码来实现一个登录按钮:
|
||||
|
||||
<pre>
|
||||
<code className="language-tsx">
|
||||
{`import { useLogto } from '@logto/react';
|
||||
|
||||
const SignIn = () => {
|
||||
const { signIn, isAuthenticated } = useLogto();
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <div>已登录</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => signIn('${props.redirectUris[0] ?? 'http://localhost:3000/callback'}')}>
|
||||
登录
|
||||
</button>
|
||||
);
|
||||
};`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
### 处理重定向
|
||||
|
||||
马上就要大功告成!在上一步,我们将 `http://localhost:3000/callback` 用作 Redirect URI,现在我们需要对其妥善处理。
|
||||
|
||||
首先,让我们来创建一个 callback 组件:
|
||||
|
||||
```tsx
|
||||
import { useHandleSignInCallback } from '@logto/react';
|
||||
|
||||
const Callback = () => {
|
||||
const { isLoading } = useHandleSignInCallback(() => {
|
||||
// 完成时跳转至根路由
|
||||
});
|
||||
|
||||
// 当登录认证尚未完成时
|
||||
if (isLoading) {
|
||||
return <div>正在重定向...</div>;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
最后我们插入如下代码来实现一个 _无需_ 登录的 `/callback` 路由:
|
||||
|
||||
```tsx
|
||||
// 假设用 react-router
|
||||
<Route path="/callback" element={<Callback />} />
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="退出登录"
|
||||
subtitle="共 1 步"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
调用 `.signOut()` 将清理内存与 localStorage 中的所有 Logto 数据(如果有)。
|
||||
|
||||
在退出登录后,让你的用户重新回到你的网站是个不错的选择。让我们将 `http://localhost:3000` 添加至下面的输入框,并将其作为调用 `.signOut()` 的参数。
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="postLogoutRedirectUris" title="application_details.post_sign_out_redirect_uri" />
|
||||
|
||||
### 实现退出登录按钮
|
||||
|
||||
<pre>
|
||||
<code className="language-tsx">
|
||||
{`const SignOut = () => {
|
||||
const { signOut } = useLogto();
|
||||
|
||||
return (
|
||||
<button onClick={() => signOut('${props.postLogoutRedirectUris[0] ?? 'http://localhost:3000'}')}>
|
||||
退出登录
|
||||
</button>
|
||||
);
|
||||
};`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="延展阅读"
|
||||
subtitle="共 4 篇"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie)
|
||||
- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in)
|
||||
- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)
|
||||
- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api)
|
||||
|
||||
</Step>
|
|
@ -1,166 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="Add Logto SDK as a dependency"
|
||||
subtitle="Please select your favorite package manager"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<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"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
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.appId}',
|
||||
});`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Sign in"
|
||||
subtitle="3 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
|
||||
</InlineNotification>
|
||||
|
||||
### Configure Redirect URI
|
||||
|
||||
First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`.
|
||||
|
||||
<UriInputField
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="redirectUris"
|
||||
title="application_details.redirect_uri"
|
||||
/>
|
||||
|
||||
### 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"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
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.appId}
|
||||
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"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [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>
|
|
@ -1,152 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="将 Logto SDK 添加至依赖"
|
||||
subtitle="选择你熟悉的包管理工具"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<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="初始化 LogtoClient"
|
||||
subtitle="共 1 步"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
Import 并传入 config 以初始化 `LogtoClient`:
|
||||
|
||||
<pre>
|
||||
<code className="language-ts">
|
||||
{`import LogtoClient from '@logto/browser';
|
||||
|
||||
const logtoClient = new LogtoClient({
|
||||
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
appId: '${props.appId}',
|
||||
});`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="登录"
|
||||
subtitle="共 3 步"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
在如下代码示例中, 我们均先假设你的应用运行在 <code>http://localhost:3000</code> 上。
|
||||
</InlineNotification>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
||||
首先,我们来添加 redirect URI,如: `http://localhost:3000/callback`。
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="application_details.redirect_uri" />
|
||||
|
||||
### 实现登录按钮
|
||||
|
||||
返回你的 IDE 或编辑器,使用如下代码来实现一个登录按钮:
|
||||
|
||||
<pre>
|
||||
<code className="language-html">
|
||||
{`<button onclick="logtoClient.signIn('${props.redirectUris[0] ?? 'http://localhost:3000/callback'}')">
|
||||
登录
|
||||
</button>`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
### 处理重定向
|
||||
|
||||
马上就要大功告成!在上一步,我们将 `http://localhost:3000/callback` 用作 Redirect URI,现在我们需要对其妥善处理。
|
||||
|
||||
在你的 `/callback` 路由下插入如下代码:
|
||||
|
||||
```ts
|
||||
try {
|
||||
await logtoClient.handleSignInCallback(window.location.href);
|
||||
console.log(await logtoClient.isAuthenticated()); // true
|
||||
} catch {
|
||||
// 处理错误
|
||||
}
|
||||
```
|
||||
|
||||
现在可以测试登录流程了。
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="退出登录"
|
||||
subtitle="共 1 步"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
调用 `.signOut()` 将清理内存与 localStorage 中的所有 Logto 数据(如果有)。
|
||||
|
||||
在退出登录后,让你的用户重新回到你的网站是个不错的选择。让我们将 `http://localhost:3000` 添加至下面的输入框,并将其作为调用 `.signOut()` 的参数。
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="postLogoutRedirectUris" title="application_details.post_sign_out_redirect_uri" />
|
||||
|
||||
### 实现退出登录按钮
|
||||
|
||||
<pre>
|
||||
<code className="language-html">
|
||||
{`<button onclick="logtoClient.signOut('${props.postLogoutRedirectUris[0] ?? 'http://localhost:3000'}')">
|
||||
退出登录
|
||||
</button>`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="延展阅读"
|
||||
subtitle="共 4 篇"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie)
|
||||
- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in)
|
||||
- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)
|
||||
- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api)
|
||||
|
||||
</Step>
|
|
@ -1,220 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="Add Logto SDK as a dependency"
|
||||
subtitle="Please select your favorite package manager"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<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"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<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.appId}',
|
||||
};
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createLogto, config);
|
||||
app.mount("#app");`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="Sign in"
|
||||
subtitle="3 steps"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
In the following steps, we assume your app is running on <code>http://localhost:3000</code>.
|
||||
</InlineNotification>
|
||||
|
||||
### Configure Redirect URI
|
||||
|
||||
First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`.
|
||||
|
||||
<UriInputField
|
||||
appId={props.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="redirectUris"
|
||||
title="application_details.redirect_uri"
|
||||
/>
|
||||
|
||||
### 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"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
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.appId}
|
||||
isSingle={!props.isCompact}
|
||||
name="postLogoutRedirectUris"
|
||||
title="application_details.post_sign_out_redirect_uri"
|
||||
/>
|
||||
|
||||
### 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"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [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>
|
|
@ -1,207 +0,0 @@
|
|||
import UriInputField from '@mdx/components/UriInputField';
|
||||
import Step from '@mdx/components/Step';
|
||||
import Tabs from '@mdx/components/Tabs';
|
||||
import TabItem from '@mdx/components/TabItem';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
<Step
|
||||
title="将 Logto SDK 添加至依赖"
|
||||
subtitle="选择你熟悉的包管理工具"
|
||||
index={0}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(1)}
|
||||
>
|
||||
<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="初始化 LogtoClient"
|
||||
subtitle="共 1 步"
|
||||
index={1}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(2)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
目前仅支持 Vue 3 的 组合式(Composition)API,我们会在后续版本中陆续添加对选项式(Options)API 和 Vue 2 的支持。
|
||||
</InlineNotification>
|
||||
|
||||
Import 并使用 `createLogto` 以插件的形式安装 Logto:
|
||||
|
||||
<pre>
|
||||
<code className="language-ts">
|
||||
{`import { createLogto, LogtoConfig } from '@logto/vue';
|
||||
|
||||
const config: LogtoConfig = {
|
||||
endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // 或 "${props.alternativeEndpoint}"` : ''}
|
||||
appId: '${props.appId}',
|
||||
};
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createLogto, config);
|
||||
app.mount("#app");`}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="登录"
|
||||
subtitle="共 3 步"
|
||||
index={2}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(3)}
|
||||
>
|
||||
|
||||
<InlineNotification>
|
||||
在如下代码示例中, 我们均先假设你的 Vue 应用运行在 <code>http://localhost:3000</code> 上。
|
||||
</InlineNotification>
|
||||
|
||||
### 配置 Redirect URI
|
||||
|
||||
首先,我们来添加 Redirect URI,如:`http://localhost:3000/callback`。
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="redirectUris" title="application_details.redirect_uri" />
|
||||
|
||||
### 实现登录按钮
|
||||
|
||||
我们提供了两个组合式 API `useHandleSignInCallback()` 和 `useLogto()`,它们可以帮助你轻松完成登录认证流程。
|
||||
|
||||
返回你的 IDE 或编辑器,使用如下代码来实现一个登录按钮:
|
||||
|
||||
<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>已登录</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button @click="onClickSignIn">登录</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 处理重定向
|
||||
|
||||
马上就要大功告成!在上一步,我们将 `http://localhost:3000/callback` 用作 Redirect URI,现在我们需要对其妥善处理。
|
||||
|
||||
首先,让我们来创建一个 Callback 组件:
|
||||
|
||||
```html
|
||||
<!-- CallbackView.vue -->
|
||||
<script setup lang="ts">
|
||||
import { useHandleSignInCallback } from '@logto/vue';
|
||||
const { isLoading } = useHandleSignInCallback(() => {
|
||||
// 完成时跳转至根路由
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```html
|
||||
<template>
|
||||
<!-- 当登录认证尚未完成时 -->
|
||||
<p v-if="isLoading">正在重定向...</p>
|
||||
</template>
|
||||
```
|
||||
|
||||
最后我们插入如下代码来实现一个 _无需_ 登录的 `/callback` 路由:
|
||||
|
||||
```ts
|
||||
// 假设用 vue-router
|
||||
const router = createRouter({
|
||||
routes: [
|
||||
{
|
||||
path: '/callback',
|
||||
name: 'callback',
|
||||
component: CallbackView,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="退出登录"
|
||||
subtitle="共 1 步"
|
||||
index={3}
|
||||
activeIndex={props.activeStepIndex}
|
||||
onButtonClick={() => props.onNext(4)}
|
||||
>
|
||||
|
||||
调用 `.signOut()` 将清理内存与 localStorage 中的所有 Logto 数据(如果有)。
|
||||
|
||||
在退出登录后,让你的用户重新回到你的网站是个不错的选择。让我们将 `http://localhost:3000` 添加至下面的输入框,并将其作为调用 `.signOut()` 的参数。
|
||||
|
||||
<UriInputField appId={props.appId} isSingle={!props.isCompact} name="postLogoutRedirectUris" title="application_details.post_sign_out_redirect_uri" />
|
||||
|
||||
### 实现退出登录按钮
|
||||
|
||||
<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">退出登录</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step
|
||||
title="延展阅读"
|
||||
subtitle="共 4 篇"
|
||||
index={4}
|
||||
activeIndex={props.activeStepIndex}
|
||||
buttonText="general.done"
|
||||
buttonType="primary"
|
||||
onButtonClick={props.onComplete}
|
||||
>
|
||||
|
||||
- [自定义登录体验](https://docs.logto.io/zh-cn/docs/recipes/customize-sie)
|
||||
- [启用短信或邮件验证码登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-passcode-sign-in)
|
||||
- [启用社交登录](https://docs.logto.io/zh-cn/docs/tutorials/get-started/enable-social-sign-in)
|
||||
- [保护你的 API](https://docs.logto.io/zh-cn/docs/recipes/protect-your-api)
|
||||
|
||||
</Step>
|
|
@ -1,4 +1,4 @@
|
|||
import { HookEvent } from '@logto/schemas';
|
||||
import { HookEvent, type Hook, type HookConfig } from '@logto/schemas';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -8,8 +8,6 @@ import FormField from '@/ds-components/FormField';
|
|||
import TextInput from '@/ds-components/TextInput';
|
||||
import { uriValidator } from '@/utils/validator';
|
||||
|
||||
import { type BasicWebhookFormType } from '../../types';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const hookEventOptions = Object.values(HookEvent).map((event) => ({
|
||||
|
@ -17,6 +15,12 @@ const hookEventOptions = Object.values(HookEvent).map((event) => ({
|
|||
value: event,
|
||||
}));
|
||||
|
||||
export type BasicWebhookFormType = {
|
||||
name: Hook['name'];
|
||||
events: HookEvent[];
|
||||
url: HookConfig['url'];
|
||||
};
|
||||
|
||||
function BasicWebhookForm() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
|
@ -1,10 +1,17 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.platforms {
|
||||
margin-top: _.unit(6);
|
||||
padding: _.unit(4) _.unit(6);
|
||||
background: var(--color-bg-layer-2);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: _.unit(3);
|
||||
margin-top: _.unit(4);
|
||||
|
||||
.title {
|
||||
font: var(--font-label-2);
|
||||
margin-bottom: _.unit(3);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,6 +144,13 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) {
|
|||
size={radioGroupSize}
|
||||
onChange={handleGroupChange}
|
||||
/>
|
||||
{activeGroup && (
|
||||
<PlatformSelector
|
||||
connectorGroup={activeGroup}
|
||||
connectorId={activeFactoryId}
|
||||
onConnectorIdChange={setActiveFactoryId}
|
||||
/>
|
||||
)}
|
||||
{standardGroups.length > 0 && (
|
||||
<>
|
||||
<div className={styles.standardLabel}>
|
||||
|
@ -159,13 +166,6 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) {
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{activeGroup && (
|
||||
<PlatformSelector
|
||||
connectorGroup={activeGroup}
|
||||
connectorId={activeFactoryId}
|
||||
onConnectorIdChange={setActiveFactoryId}
|
||||
/>
|
||||
)}
|
||||
</ModalLayout>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { useContext, useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import PlanUsage from '@/components/PlanUsage';
|
||||
import { contactEmailLink } from '@/consts';
|
||||
import { subscriptionPage } from '@/consts/pages';
|
||||
|
@ -21,35 +19,22 @@ import PlanName from '../PlanName';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
function MauExceededModal() {
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const cloudApi = useCloudApi();
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
const { usage, subscription } = currentTenant ?? {};
|
||||
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { navigate } = useTenantPathname();
|
||||
const [hasClosed, setHasClosed] = useState(false);
|
||||
|
||||
const [hasClosed, setHasClosed] = useState(false);
|
||||
const handleCloseModal = () => {
|
||||
setHasClosed(true);
|
||||
};
|
||||
|
||||
const { data: subscriptionPlans } = useSubscriptionPlans();
|
||||
|
||||
const { data: currentSubscription } = useSWRImmutable(
|
||||
`/api/tenants/${currentTenantId}/subscription`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/subscription', { params: { tenantId: currentTenantId } })
|
||||
);
|
||||
const currentPlan = subscriptionPlans?.find((plan) => plan.id === subscription?.planId);
|
||||
|
||||
const { data: currentUsage } = useSWRImmutable(
|
||||
`/api/tenants/${currentTenantId}/usage`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/usage', { params: { tenantId: currentTenantId } })
|
||||
);
|
||||
|
||||
const currentPlan =
|
||||
currentSubscription &&
|
||||
subscriptionPlans?.find((plan) => plan.id === currentSubscription.planId);
|
||||
|
||||
if (!currentPlan || !currentUsage || hasClosed) {
|
||||
if (!subscription || !usage || !currentPlan || hasClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -58,7 +43,7 @@ function MauExceededModal() {
|
|||
name: planName,
|
||||
} = currentPlan;
|
||||
|
||||
const isMauExceeded = mauLimit !== null && currentUsage.activeUsers >= mauLimit;
|
||||
const isMauExceeded = mauLimit !== null && usage.activeUsers >= mauLimit;
|
||||
|
||||
if (!isMauExceeded) {
|
||||
return null;
|
||||
|
@ -102,8 +87,8 @@ function MauExceededModal() {
|
|||
</InlineNotification>
|
||||
<FormField title="subscription.plan_usage">
|
||||
<PlanUsage
|
||||
subscriptionUsage={currentUsage}
|
||||
currentSubscription={currentSubscription}
|
||||
subscriptionUsage={usage}
|
||||
currentSubscription={subscription}
|
||||
currentPlan={currentPlan}
|
||||
/>
|
||||
</FormField>
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import { contactEmailLink } from '@/consts';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
@ -13,7 +11,6 @@ import InlineNotification from '@/ds-components/InlineNotification';
|
|||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
import useSubscribe from '@/hooks/use-subscribe';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { getLatestUnpaidInvoice } from '@/utils/subscription';
|
||||
|
||||
import BillInfo from '../BillInfo';
|
||||
|
||||
|
@ -22,26 +19,17 @@ import * as styles from './index.module.scss';
|
|||
function PaymentOverdueModal() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentTenant, currentTenantId } = useContext(TenantsContext);
|
||||
const cloudApi = useCloudApi();
|
||||
const { data: invoicesResponse } = useSWRImmutable(
|
||||
`/api/tenants/${currentTenantId}/invoices`,
|
||||
async () =>
|
||||
cloudApi.get('/api/tenants/:tenantId/invoices', { params: { tenantId: currentTenantId } })
|
||||
);
|
||||
const { openInvoices = [] } = currentTenant ?? {};
|
||||
|
||||
const { visitManagePaymentPage } = useSubscribe();
|
||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||
|
||||
const latestUnpaidInvoice = useMemo(
|
||||
() => conditional(invoicesResponse && getLatestUnpaidInvoice(invoicesResponse.invoices)),
|
||||
[invoicesResponse]
|
||||
);
|
||||
|
||||
const [hasClosed, setHasClosed] = useState(false);
|
||||
const handleCloseModal = () => {
|
||||
setHasClosed(true);
|
||||
};
|
||||
|
||||
if (!invoicesResponse || !latestUnpaidInvoice || hasClosed) {
|
||||
if (!isCloud || openInvoices.length === 0 || hasClosed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -82,7 +70,12 @@ function PaymentOverdueModal() {
|
|||
</InlineNotification>
|
||||
)}
|
||||
<FormField title="upsell.payment_overdue_modal.unpaid_bills">
|
||||
<BillInfo cost={latestUnpaidInvoice.amountDue} />
|
||||
<BillInfo
|
||||
cost={openInvoices.reduce(
|
||||
(total, currentInvoice) => total + currentInvoice.amountDue,
|
||||
0
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</ModalLayout>
|
||||
</ReactModal>
|
||||
|
|
|
@ -1,35 +1,16 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { type TenantResponse } from '@/cloud/types/router';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
import useInvoices from '@/hooks/use-invoices';
|
||||
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
|
||||
import useSubscriptionUsage from '@/hooks/use-subscription-usage';
|
||||
import { getLatestUnpaidInvoice } from '@/utils/subscription';
|
||||
import { type SubscriptionPlan } from '@/types/subscriptions';
|
||||
|
||||
type Props = {
|
||||
tenantId: string;
|
||||
tenantData: TenantResponse;
|
||||
tenantPlan: SubscriptionPlan;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function TenantStatusTag({ tenantId, className }: Props) {
|
||||
const { data: usage, error: fetchUsageError } = useSubscriptionUsage(tenantId);
|
||||
const { data: invoices, error: fetchInvoiceError } = useInvoices(tenantId);
|
||||
const { data: subscriptionPlan, error: fetchSubscriptionError } = useSubscriptionPlan(tenantId);
|
||||
|
||||
const isLoadingUsage = !usage && !fetchUsageError;
|
||||
const isLoadingInvoice = !invoices && !fetchInvoiceError;
|
||||
const isLoadingSubscription = !subscriptionPlan && !fetchSubscriptionError;
|
||||
|
||||
const latestUnpaidInvoice = useMemo(
|
||||
() => conditional(invoices && getLatestUnpaidInvoice(invoices)),
|
||||
[invoices]
|
||||
);
|
||||
|
||||
if (isLoadingUsage || isLoadingInvoice || isLoadingSubscription) {
|
||||
return null;
|
||||
}
|
||||
function TenantStatusTag({ tenantData, tenantPlan, className }: Props) {
|
||||
const { usage, openInvoices } = tenantData;
|
||||
|
||||
/**
|
||||
* Tenant status priority:
|
||||
|
@ -38,7 +19,7 @@ function TenantStatusTag({ tenantId, className }: Props) {
|
|||
* 3. mau exceeded
|
||||
*/
|
||||
|
||||
if (invoices && latestUnpaidInvoice) {
|
||||
if (openInvoices.length > 0) {
|
||||
return (
|
||||
<Tag className={className}>
|
||||
<DynamicT forKey="tenants.status.overdue" />
|
||||
|
@ -46,22 +27,20 @@ function TenantStatusTag({ tenantId, className }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (subscriptionPlan && usage) {
|
||||
const { activeUsers } = usage;
|
||||
const { activeUsers } = usage;
|
||||
|
||||
const {
|
||||
quota: { mauLimit },
|
||||
} = subscriptionPlan;
|
||||
const {
|
||||
quota: { mauLimit },
|
||||
} = tenantPlan;
|
||||
|
||||
const isMauExceeded = mauLimit !== null && activeUsers >= mauLimit;
|
||||
const isMauExceeded = mauLimit !== null && activeUsers >= mauLimit;
|
||||
|
||||
if (isMauExceeded) {
|
||||
return (
|
||||
<Tag className={className}>
|
||||
<DynamicT forKey="tenants.status.mau_exceeded" />
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
if (isMauExceeded) {
|
||||
return (
|
||||
<Tag className={className}>
|
||||
<DynamicT forKey="tenants.status.mau_exceeded" />
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import Tick from '@/assets/icons/tick.svg';
|
||||
import { type TenantResponse } from '@/cloud/types/router';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import { DropdownItem } from '@/ds-components/Dropdown';
|
||||
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
|
||||
import useSubscriptionPlans from '@/hooks/use-subscription-plans';
|
||||
|
||||
import TenantEnvTag from '../TenantEnvTag';
|
||||
|
||||
|
@ -18,8 +19,18 @@ type Props = {
|
|||
};
|
||||
|
||||
function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
|
||||
const { id, name, tag } = tenantData;
|
||||
const { data: tenantPlan } = useSubscriptionPlan(id);
|
||||
const {
|
||||
name,
|
||||
tag,
|
||||
subscription: { planId },
|
||||
} = tenantData;
|
||||
|
||||
const { data: plans } = useSubscriptionPlans();
|
||||
const tenantPlan = useMemo(() => plans?.find((plan) => plan.id === planId), [plans, planId]);
|
||||
|
||||
if (!tenantPlan) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownItem className={styles.item} onClick={onClick}>
|
||||
|
@ -27,9 +38,15 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
|
|||
<div className={styles.meta}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<TenantEnvTag tag={tag} />
|
||||
<TenantStatusTag tenantId={id} className={styles.statusTag} />
|
||||
<TenantStatusTag
|
||||
tenantData={tenantData}
|
||||
tenantPlan={tenantPlan}
|
||||
className={styles.statusTag}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.planName}>
|
||||
<PlanName name={tenantPlan.name} />
|
||||
</div>
|
||||
<div className={styles.planName}>{tenantPlan && <PlanName name={tenantPlan.name} />}</div>
|
||||
</div>
|
||||
<Tick className={classNames(styles.checkIcon, isSelected && styles.visible)} />
|
||||
</DropdownItem>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { TenantsContext } from '@/contexts/TenantsProvider';
|
|||
import Divider from '@/ds-components/Divider';
|
||||
import Dropdown from '@/ds-components/Dropdown';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import useUserDefaultTenantId from '@/hooks/use-user-default-tenant-id';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import TenantDropdownItem from './TenantDropdownItem';
|
||||
|
@ -28,6 +29,7 @@ export default function TenantSelector() {
|
|||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [showCreateTenantModal, setShowCreateTenantModal] = useState(false);
|
||||
const { updateDefaultTenantId } = useUserDefaultTenantId();
|
||||
|
||||
if (tenants.length === 0 || !currentTenantInfo) {
|
||||
return null;
|
||||
|
@ -69,6 +71,7 @@ export default function TenantSelector() {
|
|||
isSelected={tenantData.id === currentTenantId}
|
||||
onClick={() => {
|
||||
navigateTenant(tenantData.id);
|
||||
void updateDefaultTenantId(tenantData.id);
|
||||
setShowDropdown(false);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -10,7 +10,6 @@ import AppLoading from '@/components/AppLoading';
|
|||
// eslint-disable-next-line unused-imports/no-unused-imports
|
||||
import type ProtectedRoutes from '@/containers/ProtectedRoutes';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import useUserDefaultTenantId from '@/hooks/use-user-default-tenant-id';
|
||||
|
||||
/**
|
||||
* The container that ensures the user has access to the current tenant. When the user is
|
||||
|
@ -47,7 +46,6 @@ export default function TenantAccess() {
|
|||
const { getAccessToken, signIn, isAuthenticated } = useLogto();
|
||||
const { currentTenant, currentTenantId, currentTenantStatus, setCurrentTenantStatus } =
|
||||
useContext(TenantsContext);
|
||||
const { updateIfNeeded } = useUserDefaultTenantId();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
// Clean the cache when the current tenant ID changes. This is required because the
|
||||
|
@ -104,12 +102,5 @@ export default function TenantAccess() {
|
|||
signIn,
|
||||
]);
|
||||
|
||||
// Update the user's default tenant ID if the current tenant is validated.
|
||||
useEffect(() => {
|
||||
if (currentTenantStatus === 'validated') {
|
||||
void updateIfNeeded();
|
||||
}
|
||||
}, [currentTenantStatus, updateIfNeeded]);
|
||||
|
||||
return currentTenantStatus === 'validated' ? <Outlet /> : <AppLoading />;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { defaultManagementApi, defaultTenantId } from '@logto/schemas';
|
||||
import { TenantTag } from '@logto/schemas/models';
|
||||
import { conditionalArray, noop } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, createContext, useState } from 'react';
|
||||
import { useMatch, useNavigate } from 'react-router-dom';
|
||||
|
@ -65,17 +66,27 @@ const { tenantId, indicator } = defaultManagementApi.resource;
|
|||
* - For cloud, the initial tenants data is empty, and it will be fetched from the cloud API.
|
||||
* - OSS has a fixed tenant with ID `default` and no cloud API to dynamically fetch tenants.
|
||||
*/
|
||||
const initialTenants = Object.freeze(
|
||||
conditionalArray(
|
||||
!isCloud && {
|
||||
id: tenantId,
|
||||
name: `tenant_${tenantId}`,
|
||||
tag: TenantTag.Development,
|
||||
indicator,
|
||||
planId: `${ReservedPlanId.free}`, // `planId` is string type.
|
||||
}
|
||||
)
|
||||
);
|
||||
const defaultTenantResponse: TenantResponse = {
|
||||
id: tenantId,
|
||||
name: `tenant_${tenantId}`,
|
||||
tag: TenantTag.Development,
|
||||
indicator,
|
||||
subscription: {
|
||||
status: 'active',
|
||||
planId: ReservedPlanId.free,
|
||||
currentPeriodStart: dayjs().toDate(),
|
||||
currentPeriodEnd: dayjs().add(1, 'month').toDate(),
|
||||
},
|
||||
usage: {
|
||||
activeUsers: 0,
|
||||
cost: 0,
|
||||
},
|
||||
openInvoices: [],
|
||||
isSuspended: false,
|
||||
planId: ReservedPlanId.free, // Reserved for compatibility with cloud
|
||||
};
|
||||
|
||||
const initialTenants = Object.freeze(conditionalArray(!isCloud && defaultTenantResponse));
|
||||
|
||||
export const TenantsContext = createContext<Tenants>({
|
||||
tenants: initialTenants,
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
align-items: center;
|
||||
margin-bottom: _.unit(1);
|
||||
|
||||
&.withLargeSpacing {
|
||||
margin-bottom: _.unit(2);
|
||||
}
|
||||
|
||||
.title {
|
||||
font: var(--font-label-2);
|
||||
color: var(--color-text);
|
||||
|
|
|
@ -20,6 +20,7 @@ export type Props = {
|
|||
isRequired?: boolean;
|
||||
isMultiple?: boolean;
|
||||
className?: string;
|
||||
headlineSpacing?: 'default' | 'large';
|
||||
headlineClassName?: string;
|
||||
tip?: ToggleTipProps['content'];
|
||||
};
|
||||
|
@ -30,6 +31,7 @@ function FormField({
|
|||
isRequired,
|
||||
isMultiple,
|
||||
className,
|
||||
headlineSpacing = 'default',
|
||||
tip,
|
||||
headlineClassName,
|
||||
}: Props) {
|
||||
|
@ -37,7 +39,13 @@ function FormField({
|
|||
|
||||
return (
|
||||
<div className={classNames(styles.field, className)}>
|
||||
<div className={classNames(styles.headline, headlineClassName)}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.headline,
|
||||
headlineSpacing === 'large' && styles.withLargeSpacing,
|
||||
headlineClassName
|
||||
)}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
{typeof title === 'string' ? <DynamicT forKey={title} /> : title}
|
||||
{isMultiple && (
|
||||
|
|
|
@ -67,7 +67,7 @@ function useTenantPathname(): TenantPathname {
|
|||
const match = useCallback(
|
||||
(pathname: string, exact = false) => {
|
||||
// Match relative pathnames directly
|
||||
if (pathname.startsWith('.')) {
|
||||
if (!pathname.startsWith('/')) {
|
||||
return (
|
||||
matchPath(joinPath(location.pathname, pathname, exact ? '' : '*'), location.pathname) !==
|
||||
null
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defaultTenantId as ossDefaultTenantId } from '@logto/schemas';
|
||||
import { trySafe } from '@silverhand/essentials';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isCloud } from '@/consts/env';
|
||||
|
@ -24,8 +24,6 @@ const useUserDefaultTenantId = () => {
|
|||
() => trySafe(() => z.object({ [key]: z.string() }).parse(data)[key]),
|
||||
[data]
|
||||
);
|
||||
/** The last tenant ID that has been updated in the user's `customData`. */
|
||||
const [updatedTenantId, setUpdatedTenantId] = useState(storedId);
|
||||
|
||||
const defaultTenantId = useMemo(() => {
|
||||
// Directly return the default tenant ID for OSS because it's single tenant.
|
||||
|
@ -42,31 +40,26 @@ const useUserDefaultTenantId = () => {
|
|||
return tenants[0]?.id;
|
||||
}, [storedId, tenants]);
|
||||
|
||||
const updateIfNeeded = useCallback(async () => {
|
||||
// No need for updating for OSS because it's single tenant.
|
||||
if (!isCloud) {
|
||||
return;
|
||||
}
|
||||
const updateDefaultTenantId = useCallback(
|
||||
async (tenantId: string) => {
|
||||
// No need for updating for OSS because it's single tenant.
|
||||
if (!isCloud) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note storedId is not checked here because it's by design that the default tenant ID
|
||||
// should be updated only when the user manually changes the current tenant. That is,
|
||||
// if the user opens a new tab and go back to the original tab, the default tenant ID
|
||||
// should still be the ID of the new tab.
|
||||
if (currentTenantId !== updatedTenantId) {
|
||||
setUpdatedTenantId(currentTenantId);
|
||||
await updateMeCustomData({
|
||||
[key]: currentTenantId,
|
||||
[key]: tenantId,
|
||||
});
|
||||
}
|
||||
}, [currentTenantId, updateMeCustomData, updatedTenantId]);
|
||||
},
|
||||
[updateMeCustomData]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
defaultTenantId,
|
||||
/** Update the default tenant ID to the current tenant ID. */
|
||||
updateIfNeeded,
|
||||
updateDefaultTenantId,
|
||||
}),
|
||||
[defaultTenantId, updateIfNeeded]
|
||||
[defaultTenantId, updateDefaultTenantId]
|
||||
);
|
||||
};
|
||||
|
||||
|
|
27
packages/console/src/hooks/use-window-resize.ts
Normal file
27
packages/console/src/hooks/use-window-resize.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useEffect, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
type Callback = (event?: UIEvent) => void;
|
||||
|
||||
const useWindowResize = (callback: Callback) => {
|
||||
const callbackRef = useRef<Callback>(callback);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
callbackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handler: Callback = (event) => {
|
||||
callbackRef.current(event);
|
||||
};
|
||||
|
||||
handler();
|
||||
window.addEventListener('resize', handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handler);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useWindowResize;
|
|
@ -1,15 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.wrapper {
|
||||
border-radius: 16px;
|
||||
background: var(--color-layer-1);
|
||||
padding: _.unit(5) _.unit(6);
|
||||
width: 100%;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(4);
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import { type ReactNode, forwardRef, type Ref } from 'react';
|
||||
|
||||
import Index from '@/components/Index';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export type Props = {
|
||||
index?: number;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function Step({ title, subtitle, index, children }: Props, ref?: Ref<HTMLDivElement>) {
|
||||
return (
|
||||
<section ref={ref} className={styles.wrapper}>
|
||||
<header>
|
||||
<Index index={(index ?? 0) + 1} />
|
||||
<CardTitle
|
||||
size="medium"
|
||||
title={<DangerousRaw>{title}</DangerousRaw>}
|
||||
subtitle={<DangerousRaw>{subtitle}</DangerousRaw>}
|
||||
/>
|
||||
</header>
|
||||
<div>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef<HTMLDivElement, Props>(Step);
|
|
@ -1,53 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navigationAnchor {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.navigation {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
flex-shrink: 0;
|
||||
margin-right: _.unit(4);
|
||||
width: 220px;
|
||||
|
||||
> :not(:last-child) {
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 858px;
|
||||
|
||||
> :not(:last-child) {
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
}
|
||||
|
||||
.stepper {
|
||||
font: var(--font-title-2);
|
||||
color: var(--color-text);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-surface-5);
|
||||
padding: _.unit(3) _.unit(4);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-5);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-focused-variant);
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Avoid using element selector directly in mdx components, as it will
|
||||
* affect all elements in the entire admin console app.
|
||||
*/
|
||||
.form {
|
||||
margin: _.unit(4) 0;
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import type { Application } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
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/Guide';
|
||||
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';
|
||||
/** The default value of the input field when there's no data. */
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
function UriInputField({ name, defaultValue }: 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))();
|
||||
}
|
||||
};
|
||||
|
||||
const clientMetadata = data?.oidcClientMetadata[name];
|
||||
const defaultValueArray = clientMetadata?.length
|
||||
? clientMetadata
|
||||
: conditional(defaultValue && [defaultValue]);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form className={styles.form}>
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaultValueArray}
|
||||
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;
|
|
@ -1,103 +1,15 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.card {
|
||||
.wrapper {
|
||||
border-radius: 16px;
|
||||
background: var(--color-layer-1);
|
||||
padding: _.unit(5) _.unit(6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
scroll-margin: _.unit(5);
|
||||
width: 100%;
|
||||
|
||||
.header {
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
> svg {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.index {
|
||||
margin-right: _.unit(4);
|
||||
}
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.expanded {
|
||||
height: auto;
|
||||
overflow: unset;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
font: var(--font-body-2);
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-inline-start: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-inline-start: 4ch;
|
||||
|
||||
> li {
|
||||
margin-block-start: _.unit(2);
|
||||
margin-block-end: _.unit(2);
|
||||
padding-inline-start: _.unit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding-inline-start: 2ch;
|
||||
|
||||
> li {
|
||||
margin-block-start: _.unit(3);
|
||||
margin-block-end: _.unit(3);
|
||||
padding-inline-start: _.unit(1);
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font: var(--font-title-2);
|
||||
color: var(--color-text-secondary);
|
||||
margin: _.unit(6) 0 _.unit(3);
|
||||
}
|
||||
|
||||
p {
|
||||
font: var(--font-body-2);
|
||||
margin: _.unit(3) 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font: var(--font-label-2);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: _.unit(3) 0;
|
||||
}
|
||||
|
||||
code:not(pre > code) {
|
||||
background: var(--color-layer-2);
|
||||
font: var(--font-body-2);
|
||||
padding: _.unit(1) _.unit(1);
|
||||
border-radius: 4px;
|
||||
gap: _.unit(4);
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
}
|
||||
|
||||
.card + .card {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
|
|
@ -1,110 +1,32 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import classNames from 'classnames';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { type ReactNode, forwardRef, type Ref } from 'react';
|
||||
|
||||
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
|
||||
import KeyboardArrowUp from '@/assets/icons/keyboard-arrow-up.svg';
|
||||
import Index from '@/components/Index';
|
||||
import Button from '@/ds-components/Button';
|
||||
import Card from '@/ds-components/Card';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||
import IconButton from '@/ds-components/IconButton';
|
||||
import Spacer from '@/ds-components/Spacer';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
export type Props = {
|
||||
index?: number;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
index: number;
|
||||
activeIndex: number;
|
||||
buttonText?: AdminConsoleKey;
|
||||
buttonHtmlType?: 'submit' | 'button';
|
||||
buttonType?: 'primary' | 'outline';
|
||||
isLoading?: boolean;
|
||||
onButtonClick?: () => void;
|
||||
}>;
|
||||
|
||||
function Step({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
index,
|
||||
activeIndex,
|
||||
buttonText = 'general.next',
|
||||
buttonHtmlType = 'button',
|
||||
buttonType = 'outline',
|
||||
isLoading,
|
||||
onButtonClick,
|
||||
}: Props) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const isActive = index === activeIndex;
|
||||
const isComplete = index < activeIndex;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
ref.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
const onToggle = useCallback(() => {
|
||||
setIsExpanded((expand) => !expand);
|
||||
}, [setIsExpanded]);
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function Step({ title, subtitle, index, children }: Props, ref?: Ref<HTMLDivElement>) {
|
||||
return (
|
||||
<Card key={title} ref={ref} className={styles.card}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.header}
|
||||
onKeyDown={onKeyDownHandler({
|
||||
Esc: () => {
|
||||
setIsExpanded(false);
|
||||
},
|
||||
Enter: onToggle,
|
||||
' ': onToggle,
|
||||
})}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Index
|
||||
className={styles.index}
|
||||
index={index + 1}
|
||||
isActive={isActive}
|
||||
isComplete={isComplete}
|
||||
/>
|
||||
<section ref={ref} className={styles.wrapper}>
|
||||
<header>
|
||||
<Index index={(index ?? 0) + 1} />
|
||||
<CardTitle
|
||||
size="medium"
|
||||
title={<DangerousRaw>{title}</DangerousRaw>}
|
||||
subtitle={<DangerousRaw>{subtitle}</DangerousRaw>}
|
||||
/>
|
||||
<Spacer />
|
||||
<IconButton>{isExpanded ? <KeyboardArrowUp /> : <KeyboardArrowDown />}</IconButton>
|
||||
</div>
|
||||
<div className={classNames(styles.content, isExpanded && styles.expanded)}>
|
||||
{children}
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button
|
||||
type={buttonType}
|
||||
size="large"
|
||||
isLoading={isLoading}
|
||||
htmlType={buttonHtmlType}
|
||||
title={buttonText}
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</header>
|
||||
<div>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default Step;
|
||||
export default forwardRef<HTMLDivElement, Props>(Step);
|
||||
|
|
67
packages/console/src/mdx-components/Steps/index.module.scss
Normal file
67
packages/console/src/mdx-components/Steps/index.module.scss
Normal file
|
@ -0,0 +1,67 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
@use '@/scss/dimensions' as dim;
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navigationAnchor {
|
||||
position: absolute;
|
||||
inset: _.unit(6) auto _.unit(6) _.unit(6);
|
||||
}
|
||||
|
||||
.navigation {
|
||||
position: sticky;
|
||||
top: _.unit(6);
|
||||
flex-shrink: 0;
|
||||
margin-right: _.unit(7.5);
|
||||
width: 220px;
|
||||
|
||||
> :not(:last-child) {
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
min-width: dim.$guide-content-min-width;
|
||||
max-width: dim.$guide-content-max-width;
|
||||
padding: dim.$guide-content-padding calc(dim.$guide-sidebar-width + dim.$guide-panel-gap + dim.$guide-content-padding);
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
|
||||
> :not(:last-child) {
|
||||
margin-bottom: _.unit(6);
|
||||
}
|
||||
|
||||
&.compact {
|
||||
min-width: 652px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stepper {
|
||||
font: var(--font-title-2);
|
||||
color: var(--color-text);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-surface-5);
|
||||
padding: _.unit(3) _.unit(4);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-5);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-focused-variant);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: dim.$guide-content-max-width) {
|
||||
.content {
|
||||
margin: 0;
|
||||
padding-right: dim.$guide-content-padding;
|
||||
max-width: calc(dim.$guide-main-content-max-width + dim.$guide-sidebar-width + dim.$guide-panel-gap + 2 * dim.$guide-content-padding);
|
||||
}
|
||||
}
|
|
@ -48,6 +48,7 @@ export default function Steps({ children: reactChildren }: Props) {
|
|||
: [reactChildren, furtherReadings],
|
||||
[furtherReadings, reactChildren]
|
||||
);
|
||||
|
||||
const { isCompact } = useContext(GuideContext);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -85,30 +86,30 @@ export default function Steps({ children: reactChildren }: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.wrapper, isCompact && styles.fullWidth)}>
|
||||
{!isCompact && (
|
||||
<div className={styles.navigationAnchor}>
|
||||
<nav className={styles.navigation}>
|
||||
{children.map((component, index) => (
|
||||
<div
|
||||
key={component.props.title}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={classNames(styles.stepper, index === activeIndex && styles.active)}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
navigateToStep(index);
|
||||
})}
|
||||
onClick={() => {
|
||||
navigateToStep(index);
|
||||
}}
|
||||
>
|
||||
{index + 1}. {component.props.title}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
<div ref={contentRef} className={styles.content}>
|
||||
<div className={styles.wrapper}>
|
||||
<div ref={contentRef} className={classNames(styles.content, isCompact && styles.compact)}>
|
||||
{!isCompact && (
|
||||
<div className={styles.navigationAnchor}>
|
||||
<nav className={styles.navigation}>
|
||||
{children.map((component, index) => (
|
||||
<div
|
||||
key={component.props.title}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={classNames(styles.stepper, index === activeIndex && styles.active)}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
navigateToStep(index);
|
||||
})}
|
||||
onClick={() => {
|
||||
navigateToStep(index);
|
||||
}}
|
||||
>
|
||||
{index + 1}. {component.props.title}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
<Sample />
|
||||
{children.map((component, index) =>
|
||||
React.cloneElement(component, {
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue