diff --git a/packages/console/src/assets/docs/guides/web-gpt-plugin/components/ClientBasics/index.tsx b/packages/console/src/assets/docs/guides/web-gpt-plugin/components/ClientBasics/index.tsx
index 45b337219..ee0c93bce 100644
--- a/packages/console/src/assets/docs/guides/web-gpt-plugin/components/ClientBasics/index.tsx
+++ b/packages/console/src/assets/docs/guides/web-gpt-plugin/components/ClientBasics/index.tsx
@@ -1,5 +1,4 @@
import { useContext } from 'react';
-import { useTranslation } from 'react-i18next';
import CopyToClipboard from '@/ds-components/CopyToClipboard';
import FormField from '@/ds-components/FormField';
@@ -11,7 +10,6 @@ export default function ClientBasics() {
const {
app: { id, secret },
} = useContext(GuideContext);
- const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
diff --git a/packages/console/src/assets/docs/guides/web-python/README.mdx b/packages/console/src/assets/docs/guides/web-python/README.mdx
index ae759f749..46dbb2dd0 100644
--- a/packages/console/src/assets/docs/guides/web-python/README.mdx
+++ b/packages/console/src/assets/docs/guides/web-python/README.mdx
@@ -1 +1,222 @@
-## Replace this with actual guide
\ No newline at end of file
+import UriInputField from '@/mdx-components-v2/UriInputField';
+import Tabs from '@mdx/components/Tabs';
+import TabItem from '@mdx/components/TabItem';
+import InlineNotification from '@/ds-components/InlineNotification';
+import { buildIdGenerator } from '@logto/shared/universal';
+import Steps from '@/mdx-components-v2/Steps';
+import Step from '@/mdx-components-v2/Step';
+
+
+
+
+
+This tutorial will show you how to integrate Logto into your Python web application.
+
+
+ - The example uses Flask, but the concepts are the same for other frameworks.
+ - This tutorial assumes your website is hosted on
{props.sampleUrls.origin}
.
+ - Logto SDK leverages coroutines, remember to use
await
when calling async functions.
+
+
+```bash
+pip install logto # or `poetry add logto` or whatever you use
+```
+
+
+
+
+
+Insert the following code into your python file:
+
+
+
+{`from logto import LogtoClient, LogtoConfig
+
+client = LogtoClient(
+ LogtoConfig(
+ endpoint= "${props.endpoint}",${props.alternativeEndpoint ? ' // or "${props.alternativeEndpoint}"' : ''}
+ appId="${props.app.id}",
+ appSecret="${props.app.secret}",
+ )
+)`}
+
+
+
+Also replace the default memory storage with a persistent storage, for example:
+
+```python
+from logto import LogtoClient, LogtoConfig, Storage
+from flask import session
+
+class SessionStorage(Storage):
+ def get(self, key: str) -> str | None:
+ return session.get(key, None)
+
+ def set(self, key: str, value: str | None) -> None:
+ session[key] = value
+
+ def delete(self, key: str) -> None:
+ session.pop(key, None)
+
+client = LogtoClient(
+ LogtoConfig(...),
+ storage=SessionStorage(),
+)
+```
+
+
+
+
+
+
+First, let’s enter your redirect URI. E.g. {props.sampleUrls.callback}
. This is where Logto will redirect users after they sign in.
+
+
+
+
+
+
+{`@app.route("/sign-in")
+async def sign_in():
+ # Get the sign-in URL and redirect the user to it
+ return redirect(await client.signIn(
+ redirectUri="${props.redirectUris[0] || props.sampleUrls.callback}",
+ ))`}
+
+
+
+If you want to show the sign-up page on the first screen, you can set `interactionMode` to `signUp`:
+
+
+
+{`@app.route("/sign-in")
+async def sign_in():
+ # Get the sign-in URL and redirect the user to it
+ return redirect(await client.signIn(
+ redirectUri="${props.redirectUris[0] || props.sampleUrls.callback}",
+ interactionMode="signUp", # Show the sign-up page on the first screen
+ ))`}
+
+
+
+Now, whenever your users visit `/sign-in`, it will start a new sign-in attempt and redirect the user to the Logto sign-in page.
+
+
+
+
+
+After the user signs in, Logto will redirect the user to the callback URL you set in the Logto Console. In this example, we use `/callback` as the callback URL:
+
+
+
+{`@app.route("/callback")
+async def callback():
+ try:
+ await client.handleSignInCallback(request.url) # Handle a lot of stuff
+ return redirect("/") # Redirect the user to the home page after a successful sign-in
+ except Exception as e:
+ # Change this to your error handling logic
+ return "Error: " + str(e)
+`}
+
+
+
+
+
+
+
+Here we implement a simple home page for demonstration:
+
+- If the user is not signed in, show a sign-in button;
+- If the user is signed in, show some basic information about the user.
+
+```python
+@app.route("/")
+async def home():
+ if client.isAuthenticated() is False:
+ return "Not authenticated Sign in"
+
+ return (
+ # Get local ID token claims
+ client.getIdTokenClaims().model_dump_json(exclude_unset=True)
+ + "
"
+ # Fetch user info from Logto userinfo endpoint
+ (await client.fetchUserInfo()).model_dump_json(exclude_unset=True)
+ + "
Sign out"
+ )
+```
+
+Our data models are based on [pydantic](https://docs.pydantic.dev/), so you can use `model_dump_json` to dump the data model to JSON.
+
+Adding `exclude_unset=True` will exclude unset fields from the JSON output, which makes the output more precise.
+
+For example, if we didn't request the `email` scope when signing in, and the `email` field will be excluded from the JSON output. However, if we requested the `email` scope, but the user doesn't have an email address, the `email` field will be included in the JSON output with a `null` value.
+
+To learn more about scopes and claims, see [Scopes and claims](https://github.com/logto-io/python/blob/master/docs/tutorial.md#scopes-and-claims).
+
+
+
+
+
+To clean up the Python session and Logto session, a sign-out route can be implemented as follows:
+
+
+
+{`@app.route("/sign-out")
+async def sign_out():
+ return redirect(
+ # Redirect the user to the home page after a successful sign-out
+ await client.signOut(postLogoutRedirectUri="${props.sampleUrls.origin}")
+ )
+`}
+
+
+
+`postLogoutRedirectUri` is optional, and if not provided, the user will be redirected to a Logto default page after a successful sign-out (without redirecting back to your application).
+
+> The name `postLogoutRedirectUri` is from the [OpenID Connect RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) specification. Although Logto uses "sign-out" instead of "logout", the concept is the same.
+
+
+
+
+
+Now, you can test your application:
+
+
+ - Visit
{props.sampleUrls.origin}
, and you should see a "Not authenticated" message with a "Sign in" button.
+ - Click the "Sign in" button, and you should be redirected to the Logto sign-in page.
+ - After you sign in, you should be redirected back to
{props.sampleUrls.origin}
, and you should see your user info and a "Sign out" button.
+ - Click the "Sign out" button, and you should be redirected back to
{props.sampleUrls.origin}
, and you should see a "Not authenticated" message with a "Sign in" button.
+
+
+
+
+
+
+Now, you have a working sign-in flow, but your routes are still unprotected. Per the framework you use, you can create a decorator such as `@authenticated` to protect your routes. For example:
+
+```python
+def authenticated(func):
+ @wraps(func)
+ async def wrapper(*args, **kwargs):
+ if client.isAuthenticated() is False:
+ return redirect("/sign-in") # Or directly call `client.signIn`
+ return await func(*args, **kwargs)
+
+ return wrapper
+```
+
+Then, you can use the decorator to protect your routes:
+
+```python
+@app.route("/protected")
+@authenticated
+async def protected():
+ return "Protected page"
+```
+
+You can also create a middleware to achieve the same goal.
+
+
+
+
diff --git a/packages/console/src/assets/docs/guides/web-python/index.ts b/packages/console/src/assets/docs/guides/web-python/index.ts
index 619b9bb94..750c665a2 100644
--- a/packages/console/src/assets/docs/guides/web-python/index.ts
+++ b/packages/console/src/assets/docs/guides/web-python/index.ts
@@ -4,9 +4,12 @@ import { type GuideMetadata } from '../types';
const metadata: Readonly
= Object.freeze({
name: 'Python',
- description:
- 'Python is a programming language that lets you work quickly and integrate systems more effectively.',
+ description: 'Integrate Logto into your Python web app, such as Django and Flask.',
target: ApplicationType.Traditional,
+ sample: {
+ repo: 'python',
+ path: 'samples',
+ },
});
export default metadata;
diff --git a/packages/console/src/pages/Applications/components/GuideV2/index.module.scss b/packages/console/src/pages/Applications/components/GuideV2/index.module.scss
index 394c3c879..a5461f8e3 100644
--- a/packages/console/src/pages/Applications/components/GuideV2/index.module.scss
+++ b/packages/console/src/pages/Applications/components/GuideV2/index.module.scss
@@ -19,7 +19,9 @@
margin: _.unit(4) 0;
}
- section ul > li {
+ section ul > li,
+ section ol > li {
+ font: var(--font-body-2);
margin-block: _.unit(2);
padding-inline-start: _.unit(1);
}
diff --git a/packages/console/src/pages/Applications/components/GuideV2/index.tsx b/packages/console/src/pages/Applications/components/GuideV2/index.tsx
index 8a7c4d7cf..d6e7b6075 100644
--- a/packages/console/src/pages/Applications/components/GuideV2/index.tsx
+++ b/packages/console/src/pages/Applications/components/GuideV2/index.tsx
@@ -33,6 +33,10 @@ type GuideContextType = {
redirectUris: string[];
postLogoutRedirectUris: string[];
isCompact: boolean;
+ sampleUrls: {
+ origin: string;
+ callback: string;
+ };
};
// eslint-disable-next-line no-restricted-syntax
@@ -68,6 +72,10 @@ function GuideV2({ guideId, app, isCompact, onClose }: Props) {
redirectUris: app.oidcClientMetadata.redirectUris,
postLogoutRedirectUris: app.oidcClientMetadata.postLogoutRedirectUris,
isCompact: Boolean(isCompact),
+ sampleUrls: {
+ origin: 'http://localhost:3001/',
+ callback: 'http://localhost:3001/callback',
+ },
}) satisfies GuideContextType,
[guide, app, tenantEndpoint, isCustomDomainActive, isCompact]
);