diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/go-web.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/go-web.mdx new file mode 100644 index 000000000..fb956804e --- /dev/null +++ b/packages/console/src/assets/docs/tutorial/integrate-sdk/go-web.mdx @@ -0,0 +1,384 @@ +import UriInputField from '@mdx/components/UriInputField'; +import Step from '@mdx/components/Step'; +import Alert from '@/components/Alert'; + + props.onNext(1)} +> + + 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") +} +``` + + + + 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. + + + 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} +``` + + + + props.onNext(3)} +> + +### Create LogtConfig + +
+
+{`// main.go
+func main() {
+	// ...
+
+	logtoConfig := &client.LogtoConfig{
+		Endpoint:           "${props.endpoint}",
+		AppId:              "${props.appId}",
+		AppSecret:          "${props.appSecret}",
+		PersistAccessToken: true,
+	}
+
+	// ...
+}`}
+
+
+ +The `PersistAccessToken` config indicates whether the access token needs to be stored in the session. + +### 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)) + }) + + // ... +} +``` + +
+ + 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. + + + +### 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 + `
Sign In
` + + 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, "/") + }) + + // ... +} +``` + +
+ + props.onNext(5)} +> + +### 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 + "
" + + `
Sign In
` + + // Add link + `
Sign Out
` + + 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/recipes/customize-sie) +- [Enable SMS or email passcode sign-in](https://docs.logto.io/docs/tutorials/get-started/enable-passcode-sign-in) +- [Enable social sign-in](https://docs.logto.io/docs/tutorials/get-started/enable-social-sign-in) +- [Protect your API](https://docs.logto.io/docs/recipes/protect-your-api) + + diff --git a/packages/console/src/assets/docs/tutorial/integrate-sdk/go-web_zh-cn.mdx b/packages/console/src/assets/docs/tutorial/integrate-sdk/go-web_zh-cn.mdx new file mode 100644 index 000000000..cabeb2319 --- /dev/null +++ b/packages/console/src/assets/docs/tutorial/integrate-sdk/go-web_zh-cn.mdx @@ -0,0 +1,374 @@ +import UriInputField from '@mdx/components/UriInputField'; +import Step from '@mdx/components/Step'; +import Alert from '@/components/Alert'; + + props.onNext(1)} +> + + 在本指南中,我们基于 Gin Web 框架 示范 SDK 的集成过程。你也可以采取同样的步骤轻松地将 Logto 集成到其他的 Web 框架中。 + 在示例代码中,我们假定你的应用运行在 http://localhost:8080 上。 + + +在项目目录下执行: + +```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") +} +``` + + + props.onNext(2)} +> + +在传统网页应用中,用户的认证信息将会被存储在用户的 session 中。 + +Logto SDK 提供了一个 `Storage` 接口,你可以结合自己所使用的网络框架实现一个 `Storage` 的适配器,使 Logto SDK 能将用户认证信息存储到 session 中。 + + + 我们推荐使用非 cookie 的 session,因为 Logto 所存储的信息可能会超过 cookie 的大小限制。在示例中我们使用基于内存 session,在实际项目中,你可以根据需要使用 Redis、 MongoDB 等技术来存储 session。 + + +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} +``` + + + + props.onNext(3)} +> + +### 创建 Logto 配置 + +
+
+{`// main.go
+func main() {
+	// ...
+
+	logtoConfig := &client.LogtoConfig{
+		Endpoint:           "${props.endpoint}",
+		AppId:              "${props.appId}",
+		AppSecret:          "${props.appSecret}",
+		PersistAccessToken: true,
+	}
+
+	// ...
+}`}
+
+
+ +其中 PersistAccessToken 表示是否需要把 access token 存储到 session 中。 + +### 为每个请求初始化 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 := `

Hello Logto

` + + "
" + authState + "
" + + ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage)) + }) + + // ... +} +``` + +
+ + props.onNext(4)} +> + +### 配置 Redirect URI + +将 `http://localhost:8080/sign-in-callback` 添加到 Redirect URI,使用户登录 Logto 后能重定向到应用处理登录回调的 `/sign-in-callback` 路由: + + + +### 添加处理登录请求路由 + +```go +//main.go +func main() { + // ... + + // 在 Home 页面添加登录请求的入口 + router.GET("/", func(ctx *gin.Context) { + // ... + homePage := `

Hello Logto

` + + "
" + authState + "
" + + // 添加登录请求的入口 + `
Sign In
` + + 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, "/") + }) + + // ... +} +``` + +
+ + props.onNext(5)} +> + +### 配置 Post Sign-out Redirect URI + +为应用添加 Post Sign-out Redirect URI,使用户退出登录 Logto 之后将用户重定向回我们的应用。 +将 `http://localhost:8080` 添加到 Post Sign-out Redirect URI,使用户退出登录后回到应用首页: + + + +### 添加退出登录请求路由 + +```go +//main.go +func main() { + // ... + + // 在 Home 页面添加退出登录请求的入口 + router.GET("/", func(ctx *gin.Context) { + // ... + homePage := `

Hello Logto

` + + "
" + authState + "
" + + `
Sign In
` + + // 添加退出登录请求的入口 + `
Sign Out
` + + 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 中所有用户相关的认证信息。 + +
+ + + +- [自定义登录体验](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) + + diff --git a/packages/console/src/pages/Applications/components/Guide/index.tsx b/packages/console/src/pages/Applications/components/Guide/index.tsx index 41ec8cdd1..77be6a32e 100644 --- a/packages/console/src/pages/Applications/components/Guide/index.tsx +++ b/packages/console/src/pages/Applications/components/Guide/index.tsx @@ -27,6 +27,7 @@ const Guides: Record JSX.Elemen vanilla: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/vanilla.mdx')), express: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/express.mdx')), next: lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/next.mdx')), + 'go web': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/go-web.mdx')), 'ios_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/ios_zh-cn.mdx')), 'android_zh-cn': lazy( async () => import('@/assets/docs/tutorial/integrate-sdk/android_zh-cn.mdx') @@ -40,6 +41,7 @@ const Guides: Record JSX.Elemen async () => import('@/assets/docs/tutorial/integrate-sdk/express_zh-cn.mdx') ), 'next_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/next_zh-cn.mdx')), + 'go web_zh-cn': lazy(async () => import('@/assets/docs/tutorial/integrate-sdk/go-web_zh-cn.mdx')), }; const Guide = ({ app, isCompact, onClose }: Props) => { diff --git a/packages/console/src/pages/Applications/components/GuideHeader/index.tsx b/packages/console/src/pages/Applications/components/GuideHeader/index.tsx index 70d41c789..2a8ac7ea8 100644 --- a/packages/console/src/pages/Applications/components/GuideHeader/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideHeader/index.tsx @@ -38,6 +38,8 @@ const getSampleProjectUrl = (sdk: SupportedSdk) => { return `${githubUrlPrefix}/js/tree/master/packages/next-sample`; case SupportedSdk.Express: return `${githubUrlPrefix}/js/tree/master/packages/express-sample`; + case SupportedSdk.GoWeb: + return `${githubUrlPrefix}/go/tree/master/gin-sample`; default: return ''; } diff --git a/packages/console/src/types/applications.ts b/packages/console/src/types/applications.ts index 966f32ba5..c26562ae9 100644 --- a/packages/console/src/types/applications.ts +++ b/packages/console/src/types/applications.ts @@ -14,10 +14,11 @@ export enum SupportedSdk { Vanilla = 'Vanilla', Express = 'Express', Next = 'Next', + GoWeb = 'Go Web', } export const applicationTypeAndSdkTypeMappings = Object.freeze({ [ApplicationType.Native]: [SupportedSdk.iOS, SupportedSdk.Android], [ApplicationType.SPA]: [SupportedSdk.React, SupportedSdk.Vue, SupportedSdk.Vanilla], - [ApplicationType.Traditional]: [SupportedSdk.Next, SupportedSdk.Express], + [ApplicationType.Traditional]: [SupportedSdk.Next, SupportedSdk.Express, SupportedSdk.GoWeb], } as const);