diff --git a/.env.example b/.env.example index 5fe0f48..7001bee 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ APP_DB_HOST=localhost APP_DB_PORT=5432 APP_DB_USERNAME=test_user APP_DB_PASSWORD=test_password -APP_DB_DATABASE=testdb +APP_DB_DATABASE=postgres APP_REDIS_HOST=localhost APP_REDIS_PORT=6379 @@ -16,6 +16,9 @@ APP_JWT_REFRESH_SECRET=secret APP_JWT_ACCESS_EXPIRY=3600 APP_JWT_REFRESH_EXPIRY=86400 +APP_LINE_CHANNEL_ID=secret +APP_LINE_CHANNEL_SECRET=secret + APP_DOMAIN=:80 APP_PGADMIN_EMAIL=test@gmail.com diff --git a/README.md b/README.md index 1171166..811e516 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,16 @@ ### Official Document +- [line messaging github](https://github.com/line/line-bot-sdk-go) - [integrate line login](https://developers.line.biz/en/docs/line-login/integrate-line-login/) ### Tutorial -#### 1. [[Golang][LINE][教學] 導入 LINE Login 到你的商業網站之中,並且加入官方帳號為好友](https://www.evanlin.com/line-login/) +#### 1. Line Login Integration Tutorial -- [github](https://github.com/line/line-bot-sdk-go) +- [[Golang][LINE][教學] 將你的 chatbot 透過 account link 連接你的服務](https://www.evanlin.com/line-accountlink/) +- [[Golang][LINE][教學] 導入 LINE Login 到你的商業網站之中,並且加入官方帳號為好友](https://www.evanlin.com/line-login/) +- [github](https://github.com/kkdai/line-login-go) ## Branch/Commit Type diff --git a/go.mod b/go.mod index 713713a..7ffbb7e 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kkdai/line-login-sdk-go v0.6.1 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/magiconair/properties v1.8.7 // indirect diff --git a/go.sum b/go.sum index 47f02e4..8a176e6 100644 --- a/go.sum +++ b/go.sum @@ -118,6 +118,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kkdai/line-login-sdk-go v0.6.1 h1:YL4iPisaz6iI16BPhNNeW/MdIfLPDVa9JPmT9lDom8U= +github.com/kkdai/line-login-sdk-go v0.6.1/go.mod h1:0D2lfcUZ5YHDdPraslN8RL3Gb3sZHbJlDbmWCL74xFU= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= diff --git a/pkg/bootstrap/app.go b/pkg/bootstrap/app.go index a230051..4a35748 100644 --- a/pkg/bootstrap/app.go +++ b/pkg/bootstrap/app.go @@ -3,6 +3,7 @@ package bootstrap import ( "context" "fmt" + social "github.com/kkdai/line-login-sdk-go" "log" "net/http" "os" @@ -19,10 +20,11 @@ import ( type AppOpts func(app *Application) type Application struct { - Env *Env - Conn *gorm.DB - Cache *redis.Client - Engine *gin.Engine + Env *Env + Conn *gorm.DB + Cache *redis.Client + Engine *gin.Engine + LineSocialClient *social.Client } func App(opts ...AppOpts) *Application { @@ -30,6 +32,7 @@ func App(opts ...AppOpts) *Application { db := NewDB(env) cache := NewCache(env) engine := gin.Default() + lineSocialClient := NewLineSocialClient(env) // Set timezone tz, err := time.LoadLocation(env.Server.TimeZone) @@ -39,10 +42,11 @@ func App(opts ...AppOpts) *Application { time.Local = tz app := &Application{ - Env: env, - Conn: db, - Cache: cache, - Engine: engine, + Env: env, + Conn: db, + Cache: cache, + Engine: engine, + LineSocialClient: lineSocialClient, } for _, opt := range opts { diff --git a/pkg/bootstrap/env.go b/pkg/bootstrap/env.go index f628662..8637fd4 100644 --- a/pkg/bootstrap/env.go +++ b/pkg/bootstrap/env.go @@ -11,6 +11,7 @@ type Env struct { Redis RedisEnv `envPrefix:"REDIS_"` Server Server `envPrefix:"SERVER_"` JWT JWTEnv `envPrefix:"JWT_"` + Line LineEnv `envPrefix:"LINE_"` Domain string `env:"DOMAIN"` } diff --git a/pkg/bootstrap/line.go b/pkg/bootstrap/line.go new file mode 100644 index 0000000..5059f84 --- /dev/null +++ b/pkg/bootstrap/line.go @@ -0,0 +1,13 @@ +package bootstrap + +import social "github.com/kkdai/line-login-sdk-go" + +type LineEnv struct { + ChannelID string `env:"CHANNEL_ID"` + ChannelSecret string `env:"CHANNEL_SECRET"` +} + +func NewLineSocialClient(env *Env) *social.Client { + client, _ := social.New(env.Line.ChannelID, env.Line.ChannelSecret) + return client +} diff --git a/pkg/controller/oauth_controller.go b/pkg/controller/oauth_controller.go new file mode 100644 index 0000000..f14b56e --- /dev/null +++ b/pkg/controller/oauth_controller.go @@ -0,0 +1,117 @@ +package controller + +import ( + "bikefest/pkg/bootstrap" + "bikefest/pkg/model" + "fmt" + "github.com/gin-gonic/gin" + social "github.com/kkdai/line-login-sdk-go" + "log" + "net/http" + "strconv" +) + +func NewOAuthController(lineSocialClient *social.Client, env *bootstrap.Env, userSvc model.UserService) *OAuthController { + return &OAuthController{ + lineSocialClient: lineSocialClient, + userSvc: userSvc, + env: env, + } +} + +type OAuthController struct { + lineSocialClient *social.Client + userSvc model.UserService + env *bootstrap.Env +} + +// http://localhost:8000/line-login/auth +func (ctrl *OAuthController) LineLogin(c *gin.Context) { + //TODO: place `serverURL` into env + serverURL := "http://localhost:8000" + scope := "profile openid" //profile | openid | email + state := social.GenerateNonce() + nonce := social.GenerateNonce() + redirectURL := fmt.Sprintf("%s/line-login/callback", serverURL) + targetURL := ctrl.lineSocialClient.GetWebLoinURL(redirectURL, state, scope, social.AuthRequestOptions{Nonce: nonce, Prompt: "consent"}) + c.Redirect(http.StatusMovedPermanently, targetURL) +} + +func (ctrl *OAuthController) LineLoginCallback(c *gin.Context) { + //TODO: place `serverURL` and `frontendURL` into env + serverURL := "http://localhost:8000" + frontendURL := "http://localhost:3000" + code := c.Query("code") + _ = c.Query("state") + token, err := ctrl.lineSocialClient.GetAccessToken(fmt.Sprintf("%s/line-login/callback", serverURL), code).Do() + if err != nil { + log.Println("RequestLoginToken err:", err) + return + } + log.Println("access_token:", token.AccessToken, " refresh_token:", token.RefreshToken) + + var payload *social.Payload + //if len(token.IDToken) == 0 { + // // User don't request openID, use access token to get user profile + // log.Println(" token:", token, " AccessToken:", token.AccessToken) + // res, err := ctrl.lineSocialClient.GetUserProfile(token.AccessToken).Do() + // if err != nil { + // log.Println("GetUserProfile err:", err) + // return + // } + // payload = &social.Payload{ + // Name: res.DisplayName, + // Picture: res.PictureURL, + // } + //} else { + //Decode token.IDToken to payload + payload, err = token.DecodePayload(ctrl.env.Line.ChannelID) + if err != nil { + log.Println("DecodeIDToken err:", err) + return + } + //} + log.Printf("payload: %#v", payload) + + //c.JSON(http.StatusOK, gin.H{ + // "status": "Success", + // "data": payload, + //}) + + user := &model.User{ + ID: payload.Sub, + Name: payload.Name, + } + + err = ctrl.userSvc.CreateFakeUser(c, user) + + if err != nil { + log.Printf("user with id %s already exists", user.ID) + } + + accessToken, err := ctrl.userSvc.CreateAccessToken(c, user, ctrl.env.JWT.AccessTokenSecret, ctrl.env.JWT.AccessTokenExpiry) + if err != nil { + log.Printf("failed to create access token: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "status": "Failed", + "message": "failed to create access token", + }) + return + } + + refreshToken, err := ctrl.userSvc.CreateRefreshToken(c, user, ctrl.env.JWT.RefreshTokenSecret, ctrl.env.JWT.RefreshTokenExpiry) + if err != nil { + log.Printf("failed to create refresh token: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{ + "status": "Failed", + "message": "failed to create refresh token", + }) + return + } + + // set to cookie + c.SetCookie("access_token", strconv.FormatInt(ctrl.env.JWT.AccessTokenExpiry, 10), 3600, "/", "", false, true) + c.SetCookie("refresh_token", strconv.FormatInt(ctrl.env.JWT.AccessTokenExpiry, 10), 3600, "/", "", false, true) + // redirect to frontend + c.Redirect(http.StatusMovedPermanently, fmt.Sprintf("%s/oauth?access_token=%s&refresh_token=%s", frontendURL, accessToken, refreshToken)) +} diff --git a/pkg/router/oauth_route.go b/pkg/router/oauth_route.go new file mode 100644 index 0000000..e5c37c8 --- /dev/null +++ b/pkg/router/oauth_route.go @@ -0,0 +1,12 @@ +package router + +import ( + "bikefest/pkg/bootstrap" + "bikefest/pkg/controller" +) + +func RegisterOAuthRouter(app *bootstrap.Application, controller *controller.OAuthController) { + lineRouter := app.Engine.Group("/line-login") + lineRouter.GET("/auth", controller.LineLogin) + lineRouter.GET("/callback", controller.LineLoginCallback) +} diff --git a/pkg/router/route.go b/pkg/router/route.go index 572ffab..8e6d2d5 100644 --- a/pkg/router/route.go +++ b/pkg/router/route.go @@ -28,4 +28,8 @@ func RegisterRoutes(app *bootstrap.Application, services *Services) { // Register PsychoTest Routes psychoTestController := controller.NewPsychoTestController(app.Conn) RegisterPsychoTestRouter(app, psychoTestController) + + // Register OAuth Routes + oauthController := controller.NewOAuthController(app.LineSocialClient, app.Env, services.UserService) + RegisterOAuthRouter(app, oauthController) }