diff --git a/packages/console/src/assets/docs/guides/README.md b/packages/console/src/assets/docs/guides/README.md index 26b90c8e4..db8d8d662 100644 --- a/packages/console/src/assets/docs/guides/README.md +++ b/packages/console/src/assets/docs/guides/README.md @@ -19,9 +19,17 @@ The `README.mdx` file contains the actual guide content. The `assets` directory ### Create the guide directory -The guide directory should be named `[target]-name`, where `[target]` is the target of the guide in kebab-case (see `types.ts` for a list of all targets) and `name` is the name of the guide. The name should be kebab-cased and should not contain any special characters. +The guide directory should be named `[target]-name`, where `[target]` is the target of the guide in kebab-case and `name` is the name of the guide. The name should be kebab-cased and should not contain any special characters. -For example, a guide for the `MachineToMachine` target with the name `General` should be placed in the directory `machine-to-machine-general`; a guide for the `SPA` target with the name `React` should be placed in the directory `spa-react`. +Currently we have the following targets: + +- `spa`: Single-page application +- `web`: Web application +- `native`: Native application +- `m2m`: Machine-to-machine +- `api`: API resource + +For example, a guide for the `MachineToMachine` target with the name `General` should be placed in the directory `m2m-general`; a guide for the `SPA` target with the name `React` should be placed in the directory `spa-react`. > **Note** > The directory name will be the unique identifier of the guide. diff --git a/packages/console/src/assets/docs/guides/index.ts b/packages/console/src/assets/docs/guides/index.ts index 37d2f6e82..0a6579c08 100644 --- a/packages/console/src/assets/docs/guides/index.ts +++ b/packages/console/src/assets/docs/guides/index.ts @@ -2,11 +2,39 @@ import { lazy } from 'react'; +import nativeAndroidJava from './native-android-java/index'; +import nativeAndroidKt from './native-android-kt/index'; +import nativeIosSwift from './native-ios-swift/index'; import spaReact from './spa-react/index'; +import spaVanilla from './spa-vanilla/index'; +import spaVue from './spa-vue/index'; import { type Guide } from './types'; import webExpress from './web-express/index'; +import webGo from './web-go/index'; +import webNext from './web-next/index'; const guides: Readonly = Object.freeze([ + { + id: 'native-android-java', + Logo: lazy(async () => import('./native-android-java/logo.svg')), + Component: lazy(async () => import('./native-android-java/README.mdx')), + metadata: nativeAndroidJava, + }, + + { + id: 'native-android-kt', + Logo: lazy(async () => import('./native-android-kt/logo.svg')), + Component: lazy(async () => import('./native-android-kt/README.mdx')), + metadata: nativeAndroidKt, + }, + + { + id: 'native-ios-swift', + Logo: lazy(async () => import('./native-ios-swift/logo.svg')), + Component: lazy(async () => import('./native-ios-swift/README.mdx')), + metadata: nativeIosSwift, + }, + { id: 'spa-react', Logo: lazy(async () => import('./spa-react/logo.svg')), @@ -14,12 +42,40 @@ const guides: Readonly = Object.freeze([ metadata: spaReact, }, + { + id: 'spa-vanilla', + Logo: lazy(async () => import('./spa-vanilla/logo.svg')), + Component: lazy(async () => import('./spa-vanilla/README.mdx')), + metadata: spaVanilla, + }, + + { + id: 'spa-vue', + Logo: lazy(async () => import('./spa-vue/logo.svg')), + Component: lazy(async () => import('./spa-vue/README.mdx')), + metadata: spaVue, + }, + { id: 'web-express', Logo: lazy(async () => import('./web-express/logo.svg')), Component: lazy(async () => import('./web-express/README.mdx')), metadata: webExpress, }, + + { + id: 'web-go', + Logo: lazy(async () => import('./web-go/logo.svg')), + Component: lazy(async () => import('./web-go/README.mdx')), + metadata: webGo, + }, + + { + id: 'web-next', + Logo: lazy(async () => import('./web-next/logo.svg')), + Component: lazy(async () => import('./web-next/README.mdx')), + metadata: webNext, + }, ]); export default guides; diff --git a/packages/console/src/assets/docs/guides/native-android-java/README.mdx b/packages/console/src/assets/docs/guides/native-android-java/README.mdx new file mode 100644 index 000000000..4fb4c8646 --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-android-java/README.mdx @@ -0,0 +1,121 @@ +import UriInputField from '@/mdx-components-v2/UriInputField'; +import InlineNotification from '@/ds-components/InlineNotification'; +import Steps from '@/mdx-components-v2/Steps'; +import Step from '@/mdx-components-v2/Step'; + + + + + +The minimum supported Android API is level 24 + +Add the `mavenCentral()` repository to your Gradle project build file: + +```kotlin +repositories { + mavenCentral() +} +``` + +Add Logto Android SDK to your dependencies: + +```groovy +dependencies { + implementation 'io.logto.sdk:android:1.0.0' +} +``` + + + + + +
+  
+    {`import io.logto.sdk.android.LogtoClient;
+import io.logto.sdk.android.type.LogtoConfig;
+
+public class MainActivity extends AppCompatActivity {
+    private LogtoClient logtoClient;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        LogtoConfig logtoConfig = new LogtoConfig(
+            "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
+            "${props.app.id}",
+            null,
+            null,
+            true
+        );
+
+        logtoClient = new LogtoClient(logtoConfig, getApplication());
+    }
+}`}
+  
+
+ +
+ + + +### Configure Redirect URI + +First, let’s configure your redirect URI. E.g. `io.logto.android://io.logto.sample/callback` + + + +Go back to your IDE/editor, use the following code to implement sign-in: + +
+  
+    {`logtoClient.signIn(this, "${
+      props.redirectUris[0] ?? ''
+    }", logtoException -> {
+    // User signed in successfully if \`logtoException\` is null.
+});`}
+  
+
+ +After signing in successfully, `logtoClient.isAuthenticated` will be `true`. + +
+ + + +Calling `.signOut(completion)` will always clear local credentials even if errors occurred. + +```java +logtoClient.signOut(logtoException -> { + // Local credentials are cleared regardless of whether `logtoException` is null. +}); +``` + + + + + +- [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) + + + +
diff --git a/packages/console/src/assets/docs/guides/native-android-java/index.ts b/packages/console/src/assets/docs/guides/native-android-java/index.ts new file mode 100644 index 000000000..6bdd44039 --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-android-java/index.ts @@ -0,0 +1,15 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'Android (Java)', + description: 'Android integration for Java.', + target: ApplicationType.Native, + sample: { + repo: 'kotlin', + path: 'android-sample-java', + }, +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/native-android-java/logo.svg b/packages/console/src/assets/docs/guides/native-android-java/logo.svg new file mode 100644 index 000000000..01aeddccf --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-android-java/logo.svg @@ -0,0 +1,8 @@ + + + + diff --git a/packages/console/src/assets/docs/guides/native-android-kt/README.mdx b/packages/console/src/assets/docs/guides/native-android-kt/README.mdx new file mode 100644 index 000000000..23d667d36 --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-android-kt/README.mdx @@ -0,0 +1,113 @@ +import UriInputField from '@/mdx-components-v2/UriInputField'; +import InlineNotification from '@/ds-components/InlineNotification'; +import Steps from '@/mdx-components-v2/Steps'; +import Step from '@/mdx-components-v2/Step'; + + + + + +The minimum supported Android API is level 24 + +Add the `mavenCentral()` repository to your Gradle project build file: + +```kotlin +repositories { + mavenCentral() +} +``` + +Add Logto Android SDK to your dependencies: + +```kotlin +dependencies { + implementation("io.logto.sdk:android:1.0.0") +} +``` + + + + + +
+  
+    {`import io.logto.sdk.android.LogtoClient
+import io.logto.sdk.android.type.LogtoConfig
+
+class MainActivity : AppCompatActivity() {
+    val logtoConfig = LogtoConfig(
+        endpoint = "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
+        appId = "${props.app.id}",
+        scopes = null,
+        resources = null,
+        usingPersistStorage = true,
+    )
+
+    val logtoClient = LogtoClient(logtoConfig, application)
+}`}
+  
+
+ +
+ + + +### Configure Redirect URI + +First, let’s configure your redirect URI. E.g. `io.logto.android://io.logto.sample/callback` + + + +Go back to your IDE/editor, use the following code to implement sign-in: + +
+  
+    {`logtoClient.signIn(this, "${
+      props.redirectUris[0] ?? ''
+    }") { logtoException: LogtoException? ->
+    // User signed in successfully if \`logtoException\` is null.
+}`}
+  
+
+ +After signing in successfully, `logtoClient.isAuthenticated` will be `true`. + +
+ + + +Calling `.signOut(completion)` will always clear local credentials even if errors occurred. + +```kotlin +logtoClient.signOut { logtoException: LogtoException? -> + // Local credentials are cleared regardless of whether `logtoException` is null. +} +``` + + + + + +- [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) + + + +
diff --git a/packages/console/src/assets/docs/guides/native-android-kt/index.ts b/packages/console/src/assets/docs/guides/native-android-kt/index.ts new file mode 100644 index 000000000..4335baf38 --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-android-kt/index.ts @@ -0,0 +1,15 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'Android (Kotlin)', + description: 'Android integration for Kotlin.', + target: ApplicationType.Native, + sample: { + repo: 'kotlin', + path: 'android-sample-kotlin', + }, +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/native-android-kt/logo.svg b/packages/console/src/assets/docs/guides/native-android-kt/logo.svg new file mode 100644 index 000000000..230d8365c --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-android-kt/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/console/src/assets/docs/guides/native-ios-swift/README.mdx b/packages/console/src/assets/docs/guides/native-ios-swift/README.mdx new file mode 100644 index 000000000..f304021df --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-ios-swift/README.mdx @@ -0,0 +1,134 @@ +import UriInputField from '@/mdx-components-v2/UriInputField'; +import Tabs from '@mdx/components/Tabs'; +import TabItem from '@mdx/components/TabItem'; +import InlineNotification from '@/ds-components/InlineNotification'; +import Steps from '@/mdx-components-v2/Steps'; +import Step from '@/mdx-components-v2/Step'; + + + + + +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. + +
+ Carthage + +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. + +
+ +
+ CocoaPods + +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. + +
+ +
+ + + +
+  
+    {`import Logto
+import LogtoClient
+
+let config = try? LogtoConfig(
+  endpoint: "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
+  appId: "${props.app.id}"
+)
+let logtoClient = LogtoClient(useConfig: config)`}
+  
+
+ +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 +) +``` + +
+ + + +### Configure Redirect URI + +First, let’s configure your redirect URI scheme. E.g. `io.logto://callback` + + + + + The Redirect URI in iOS SDK is only for internal use. There's NO NEED to add a{' '} + + Custom URL Scheme + {' '} + until a connector asks. + + +Go back to Xcode, use the following code to implement sign-in: + +
+  
+    {`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
+}`}
+  
+
+ +
+ + + +Calling `.signOut()` will clean all the Logto data in Keychain, if they exist. + +```swift +await client.signOut() +``` + + + + + +- [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) + + + +
diff --git a/packages/console/src/assets/docs/guides/native-ios-swift/index.ts b/packages/console/src/assets/docs/guides/native-ios-swift/index.ts new file mode 100644 index 000000000..3ded79500 --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-ios-swift/index.ts @@ -0,0 +1,15 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'iOS (Swift)', + description: 'iOS (Swift) application integration guide.', + target: ApplicationType.Native, + sample: { + repo: 'swift', + path: 'Demos/SwiftUI%20Demo', + }, +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/native-ios-swift/logo.svg b/packages/console/src/assets/docs/guides/native-ios-swift/logo.svg new file mode 100644 index 000000000..b631ed097 --- /dev/null +++ b/packages/console/src/assets/docs/guides/native-ios-swift/logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/packages/console/src/assets/docs/guides/spa-react/README.mdx b/packages/console/src/assets/docs/guides/spa-react/README.mdx index b9c94039a..d25455956 100644 --- a/packages/console/src/assets/docs/guides/spa-react/README.mdx +++ b/packages/console/src/assets/docs/guides/spa-react/README.mdx @@ -1,4 +1,4 @@ -import UriInputField from '@mdx/components/UriInputField'; +import UriInputField from '@/mdx-components-v2/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; @@ -75,12 +75,7 @@ const App = () => ( First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`. - + ### Implement a sign-in button @@ -147,12 +142,7 @@ Calling `.signOut()` will clear all the Logto data in memory and localStorage if After signing out, it'll be great to redirect user back to your website. Let's add `http://localhost:3000` as the Post Sign-out URI below, and use it as the parameter when calling `.signOut()`. - + ### Implement a sign-out button diff --git a/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx b/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx new file mode 100644 index 000000000..d879a2f71 --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-vanilla/README.mdx @@ -0,0 +1,149 @@ +import UriInputField from '@/mdx-components-v2/UriInputField'; +import Tabs from '@mdx/components/Tabs'; +import TabItem from '@mdx/components/TabItem'; +import InlineNotification from '@/ds-components/InlineNotification'; +import Steps from '@/mdx-components-v2/Steps'; +import Step from '@/mdx-components-v2/Step'; + + + + + + + +```bash +npm i @logto/browser +``` + + + + +```bash +yarn add @logto/browser +``` + + + + +```bash +pnpm add @logto/browser +``` + + + + + + + +Import and init `LogtoClient` by passing config: + +
+  
+    {`import LogtoClient from '@logto/browser';
+
+const logtoClient = new LogtoClient({
+  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
+  appId: '${props.app.id}',
+});`}
+  
+
+ +
+ + + + + In the following steps, we assume your app is running on http://localhost:3000. + + +### Configure Redirect URI + +First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`. + + + +### Implement a sign-in button + +Go back to your IDE/editor, use the following code to implement the sign-in button: + +
+  
+    {``}
+  
+
+ +### 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. + +
+ + + +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()`. + + + +### Implement a sign-out button + +
+  
+    {``}
+  
+
+ +
+ + + +- [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) + + + +
diff --git a/packages/console/src/assets/docs/guides/spa-vanilla/index.ts b/packages/console/src/assets/docs/guides/spa-vanilla/index.ts new file mode 100644 index 000000000..3728997f7 --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-vanilla/index.ts @@ -0,0 +1,15 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'Vanilla JS', + description: 'The framework-agnostic JavaScript integration.', + target: ApplicationType.SPA, + sample: { + repo: 'js', + path: 'packages/browser-sample', + }, +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/spa-vanilla/logo.svg b/packages/console/src/assets/docs/guides/spa-vanilla/logo.svg new file mode 100644 index 000000000..670a4627f --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-vanilla/logo.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/packages/console/src/assets/docs/guides/spa-vue/README.mdx b/packages/console/src/assets/docs/guides/spa-vue/README.mdx new file mode 100644 index 000000000..e1e0641b1 --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-vue/README.mdx @@ -0,0 +1,198 @@ +import UriInputField from '@/mdx-components-v2/UriInputField'; +import Tabs from '@mdx/components/Tabs'; +import TabItem from '@mdx/components/TabItem'; +import InlineNotification from '@/ds-components/InlineNotification'; +import Steps from '@/mdx-components-v2/Steps'; +import Step from '@/mdx-components-v2/Step'; + + + + + + + +```bash +npm i @logto/vue +``` + + + + +```bash +yarn add @logto/vue +``` + + + + +```bash +pnpm add @logto/vue +``` + + + + + + + + + We only support Vue 3 Composition API at this point. Will add support to Vue Options API and + possibly Vue 2 in future releases. + + +Import and use `createLogto` to install Logto plugin: + +
+  
+    {`import { createLogto, LogtoConfig } from '@logto/vue';
+
+const config: LogtoConfig = {
+  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
+  appId: '${props.app.id}',
+};
+
+const app = createApp(App);
+
+app.use(createLogto, config);
+app.mount("#app");`}
+  
+
+ +
+ + + + + In the following steps, we assume your app is running on http://localhost:3000. + + +### Configure Redirect URI + +First, let’s enter your redirect URI. E.g. `http://localhost:3000/callback`. + + + +### 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: + +
+
+{``}
+
+
+
+ +```html + +``` + +### 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 + + +``` + +```html + +``` + +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, + }, + ], +}); +``` + +
+ + + +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()`. + + + +### Implement a sign-out button + +
+
+{``}
+
+
+
+ +```html + +``` + +
+ + + +- [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) + + + +
diff --git a/packages/console/src/assets/docs/guides/spa-vue/index.ts b/packages/console/src/assets/docs/guides/spa-vue/index.ts new file mode 100644 index 000000000..df8b83641 --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-vue/index.ts @@ -0,0 +1,16 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'Vue', + description: + 'Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.', + target: ApplicationType.SPA, + sample: { + repo: 'js', + path: 'packages/vue-sample', + }, +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/spa-vue/logo.svg b/packages/console/src/assets/docs/guides/spa-vue/logo.svg new file mode 100644 index 000000000..ad3d675c2 --- /dev/null +++ b/packages/console/src/assets/docs/guides/spa-vue/logo.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/packages/console/src/assets/docs/guides/web-express/README.mdx b/packages/console/src/assets/docs/guides/web-express/README.mdx index 1a557d7d7..7f9efbd04 100644 --- a/packages/console/src/assets/docs/guides/web-express/README.mdx +++ b/packages/console/src/assets/docs/guides/web-express/README.mdx @@ -1,4 +1,4 @@ -import UriInputField from '@mdx/components/UriInputField'; +import UriInputField from '@/mdx-components-v2/UriInputField'; import Tabs from '@mdx/components/Tabs'; import TabItem from '@mdx/components/TabItem'; import InlineNotification from '@/ds-components/InlineNotification'; @@ -92,12 +92,7 @@ app.use(session({ secret: '${buildIdGenerator(32)()}', cookie: { maxAge: 14 * 24 First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`. - + ### Prepare Logto routes diff --git a/packages/console/src/assets/docs/guides/web-go/README.mdx b/packages/console/src/assets/docs/guides/web-go/README.mdx new file mode 100644 index 000000000..a465e2532 --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-go/README.mdx @@ -0,0 +1,366 @@ +import UriInputField from '@/mdx-components-v2/UriInputField'; +import Steps from '@/mdx-components-v2/Steps'; +import Step from '@/mdx-components-v2/Step'; +import InlineNotification from '@/ds-components/InlineNotification'; + + + + + + The following demonstration is built upon the Gin Web Framework. + 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 http://localhost:8080. + + +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") +} +``` + + + + + +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. + + + 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. + + +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} +``` + + + + + +### Create LogtConfig + +
+  
+    {`// main.go
+func main() {
+	// ...
+
+	logtoConfig := &client.LogtoConfig{
+		Endpoint:           "${props.endpoint}",${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
+		AppId:              "${props.app.id}",
+		AppSecret:          "${props.app.secret}",
+	}
+
+	// ...
+}`}
+  
+
+ +### 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 := `

Hello Logto

` + + "
" + authState + "
" + + ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage)) + }) + + // ... +} +``` + +
+ + + +### 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. + + + +### 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 := `

Hello Logto

` + + "
" + authState + "
" + + // Add link + `` + + 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, "/") + }) + + // ... +} +``` + +
+ + + +### Configure Post Sign-out Redirect URI + +Add `http://localhost:8080` to the Post Sign-out Redirect URI filed: + + + +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 := `

Hello Logto

` + + "
" + authState + "
" + + `` + + // Add link + `` + + 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. + +
+ + + +- [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) + + + +
diff --git a/packages/console/src/assets/docs/guides/web-go/index.ts b/packages/console/src/assets/docs/guides/web-go/index.ts new file mode 100644 index 000000000..c51177327 --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-go/index.ts @@ -0,0 +1,16 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'Go', + description: + 'Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.', + target: ApplicationType.Traditional, + sample: { + repo: 'go', + path: 'gin-sample', + }, +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/web-go/logo.svg b/packages/console/src/assets/docs/guides/web-go/logo.svg new file mode 100644 index 000000000..64b1e5df9 --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-go/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/console/src/assets/docs/guides/web-next/README.mdx b/packages/console/src/assets/docs/guides/web-next/README.mdx new file mode 100644 index 000000000..6a8fb25c1 --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-next/README.mdx @@ -0,0 +1,248 @@ +import UriInputField from '@/mdx-components-v2/UriInputField'; +import Tabs from '@mdx/components/Tabs'; +import TabItem from '@mdx/components/TabItem'; +import InlineNotification from '@/ds-components/InlineNotification'; +import { buildIdGenerator } from '@logto/shared/universal'; +import Steps from '@/mdx-components-v2/Steps'; +import Step from '@/mdx-components-v2/Step'; + + + + + + + +```bash +npm i @logto/next +``` + + + + +```bash +yarn add @logto/next +``` + + + + +```bash +pnpm add @logto/next +``` + + + + + + + + + In the following steps, we assume your app is running on http://localhost:3000. + + +Import and initialize LogtoClient: + +
+  
+    {`// libraries/logto.js
+import LogtoClient from '@logto/next';
+
+export const logtoClient = new LogtoClient({
+  endpoint: '${props.endpoint}',${props.alternativeEndpoint ? ` // or "${props.alternativeEndpoint}"` : ''}
+  appId: '${props.app.id}',
+  appSecret: '${props.app.secret}',
+  baseUrl: 'http://localhost:3000', // Change to your own base URL
+  cookieSecret: '${buildIdGenerator(32)()}', // Auto-generated 32 digit secret
+  cookieSecure: process.env.NODE_ENV === 'production',
+});`}
+  
+
+ +
+ + + +### Configure Redirect URI + +First, let’s enter your redirect URI. E.g. `http://localhost:3000/api/logto/sign-in-callback`. + + + +### 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(); + +; +``` + +Now you will be navigated to Logto sign-in page when you click the button. + + + + + +### 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('/api/logto/user'); + + return
User ID: {data?.claims?.sub}
; +}; + +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
User ID: {user.claims?.sub}
; +}; + +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`. + +
+ + + +### 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 }, + }; +}); +``` + + + + + +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`. + + + +### Implement a sign-out button + +```tsx + +``` + + + + + +- [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) + + + +
diff --git a/packages/console/src/assets/docs/guides/web-next/index.ts b/packages/console/src/assets/docs/guides/web-next/index.ts new file mode 100644 index 000000000..6d34e1a8c --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-next/index.ts @@ -0,0 +1,16 @@ +import { ApplicationType } from '@logto/schemas'; + +import { type GuideMetadata } from '../types'; + +const metadata: Readonly = Object.freeze({ + name: 'Next.js', + description: + 'Next.js is a React framework for production - it makes building fullstack React apps and sites a breeze and ships with built-in SSR.', + target: ApplicationType.Traditional, + sample: { + repo: 'js', + path: 'packages/next-sample', + }, +}); + +export default metadata; diff --git a/packages/console/src/assets/docs/guides/web-next/logo.svg b/packages/console/src/assets/docs/guides/web-next/logo.svg new file mode 100644 index 000000000..8e42a15db --- /dev/null +++ b/packages/console/src/assets/docs/guides/web-next/logo.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/console/src/mdx-components-v2/UriInputField/index.module.scss b/packages/console/src/mdx-components-v2/UriInputField/index.module.scss new file mode 100644 index 000000000..1b8cea632 --- /dev/null +++ b/packages/console/src/mdx-components-v2/UriInputField/index.module.scss @@ -0,0 +1,20 @@ +@use '@/scss/underscore' as _; + +.wrapper { + display: flex; + align-items: flex-start; + position: relative; + + .field { + flex: 1; + + .multiTextInput { + flex: 1; + } + } + + .saveButton { + flex-shrink: 0; + margin: _.unit(6) 0 0 _.unit(2); + } +} diff --git a/packages/console/src/mdx-components-v2/UriInputField/index.tsx b/packages/console/src/mdx-components-v2/UriInputField/index.tsx new file mode 100644 index 000000000..0141d3e61 --- /dev/null +++ b/packages/console/src/mdx-components-v2/UriInputField/index.tsx @@ -0,0 +1,153 @@ +import type { AdminConsoleKey } from '@logto/phrases'; +import type { Application } from '@logto/schemas'; +import type { KeyboardEvent } from 'react'; +import { useContext, useRef } from 'react'; +import { Controller, FormProvider, useForm } from 'react-hook-form'; +import { toast } from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; +import useSWR from 'swr'; + +import MultiTextInputField from '@/components/MultiTextInputField'; +import Button from '@/ds-components/Button'; +import FormField from '@/ds-components/FormField'; +import { + convertRhfErrorMessage, + createValidatorForRhf, +} from '@/ds-components/MultiTextInput/utils'; +import TextInput from '@/ds-components/TextInput'; +import type { RequestError } from '@/hooks/use-api'; +import useApi from '@/hooks/use-api'; +import { GuideContext } from '@/pages/Applications/components/GuideV2'; +import type { GuideForm } from '@/types/guide'; +import { trySubmitSafe } from '@/utils/form'; +import { uriValidator } from '@/utils/validator'; + +import * as styles from './index.module.scss'; + +type Props = { + name: 'redirectUris' | 'postLogoutRedirectUris'; +}; + +function UriInputField({ name }: Props) { + const methods = useForm>(); + const { + control, + getValues, + handleSubmit, + reset, + formState: { isSubmitting }, + } = methods; + const { + app: { id: appId }, + isCompact, + } = useContext(GuideContext); + const isSingle = !isCompact; + const { data, mutate } = useSWR(`api/applications/${appId}`); + + const ref = useRef(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(); + void mutate(updatedApp); + toast.success(t('general.saved')); + + // Reset form to set 'isDirty' to false + reset(getValues()); + }); + + const onKeyPress = (event: KeyboardEvent, value: string[]) => { + if (event.key === 'Enter') { + event.preventDefault(); + void handleSubmit(async () => onSubmit(value))(); + } + }; + + return ( + +
+ !value || uriValidator(value), + message: t('errors.invalid_uri_format'), + }, + }), + }} + render={({ field: { onChange, value = [] }, fieldState: { error, isDirty } }) => { + const errorObject = convertRhfErrorMessage(error?.message); + + return ( +
+ {isSingle && ( + + { + onChange([value]); + }} + onKeyPress={(event) => { + onKeyPress(event, value); + }} + /> + + )} + {!isSingle && ( + { + onKeyPress(event, value); + }} + /> + )} +
+ ); + }} + /> + +
+ ); +} + +export default UriInputField; 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 33046c1b9..8890c5886 100644 --- a/packages/console/src/pages/Applications/components/GuideV2/index.module.scss +++ b/packages/console/src/pages/Applications/components/GuideV2/index.module.scss @@ -14,10 +14,14 @@ overflow-y: auto; padding: _.unit(6) _.unit(6) max(10vh, 120px); - .banner { - display: flex; - align-items: center; - margin-bottom: _.unit(6); + section p { + font: var(--font-body-2); + margin: _.unit(4) 0; + } + + section ul > li { + margin-block: _.unit(2); + padding-inline-start: _.unit(1); } } }