update: google oauth2.0 with hd=email@forth.co.th only now functional
This commit is contained in:
parent
984707c7bf
commit
36c71eda38
31 changed files with 580 additions and 317 deletions
3
server/.gitignore
vendored
3
server/.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
/cofffeemachineConfig
|
||||
/cofffeemachineConfig
|
||||
client_secret.json
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@ module recipe-manager
|
|||
|
||||
go 1.21.1
|
||||
|
||||
require github.com/go-chi/chi/v5 v5.0.10
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/cors v1.2.1
|
||||
golang.org/x/oauth2 v0.12.0
|
||||
)
|
||||
|
||||
require github.com/go-chi/cors v1.2.1 // indirect
|
||||
require (
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,3 +2,27 @@ github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
|||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
|
||||
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
s := NewServer(3000)
|
||||
s := NewServer(8080)
|
||||
|
||||
serverCtx, serverStopCtx := context.WithCancel(context.Background())
|
||||
sig := make(chan os.Signal, 1)
|
||||
|
|
|
|||
198
server/routers/auth.go
Normal file
198
server/routers/auth.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
package routers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type AuthRouter struct {
|
||||
gConfig *oauth2.Config
|
||||
nonce map[string]map[string]string
|
||||
}
|
||||
|
||||
func NewAuthRouter() *AuthRouter {
|
||||
|
||||
file, err := os.Open("client_secret.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var clientSecret map[string]interface{}
|
||||
json.NewDecoder(file).Decode(&clientSecret)
|
||||
|
||||
return &AuthRouter{
|
||||
gConfig: &oauth2.Config{
|
||||
ClientID: clientSecret["web"].(map[string]interface{})["client_id"].(string),
|
||||
ClientSecret: clientSecret["web"].(map[string]interface{})["client_secret"].(string),
|
||||
RedirectURL: "http://localhost:8080/auth/google/callback",
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: clientSecret["web"].(map[string]interface{})["auth_uri"].(string),
|
||||
TokenURL: clientSecret["web"].(map[string]interface{})["token_uri"].(string),
|
||||
},
|
||||
},
|
||||
nonce: make(map[string]map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (ar *AuthRouter) Route(r chi.Router) {
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Get("/google", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// generate state and nonce
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
state := base64.URLEncoding.EncodeToString(bytes)
|
||||
|
||||
stateMap := map[string]string{}
|
||||
|
||||
if r.URL.Query().Get("redirect_to") != "" {
|
||||
stateMap["redirect_to"] = r.URL.Query().Get("redirect_to")
|
||||
}
|
||||
|
||||
ar.nonce[state] = stateMap
|
||||
|
||||
url := ar.gConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", "forth.co.th"), oauth2.SetAuthURLParam("include_granted_scopes", "true"), oauth2.AccessTypeOffline)
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
r.Get("/google/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
// check state
|
||||
|
||||
var redirect_to string
|
||||
if r.URL.Query().Get("state") == "" {
|
||||
http.Error(w, "State not found", http.StatusBadRequest)
|
||||
return
|
||||
} else {
|
||||
val, ok := ar.nonce[r.URL.Query().Get("state")]
|
||||
|
||||
if !ok {
|
||||
http.Error(w, "Invalid state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
redirect_to = val["redirect_to"]
|
||||
|
||||
// remove state from nonce
|
||||
delete(ar.nonce, r.URL.Query().Get("state"))
|
||||
}
|
||||
|
||||
// exchange code for token
|
||||
token, err := ar.gConfig.Exchange(r.Context(), r.FormValue("code"))
|
||||
if err != nil {
|
||||
http.Error(w, "Error exchanging code for token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get user info
|
||||
client := ar.gConfig.Client(r.Context(), token)
|
||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
|
||||
if err != nil {
|
||||
http.Error(w, "Error getting user info", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var userInfo map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&userInfo)
|
||||
|
||||
value := url.Values{}
|
||||
|
||||
value.Add("email", userInfo["email"].(string))
|
||||
value.Add("name", userInfo["name"].(string))
|
||||
value.Add("picture", userInfo["picture"].(string))
|
||||
if redirect_to != "" {
|
||||
value.Add("redirect_to", redirect_to)
|
||||
}
|
||||
|
||||
// redirect to frontend with token and refresh token
|
||||
w.Header().Add("set-cookie", "access_token="+token.AccessToken+"; Path=/; HttpOnly; SameSite=None; Secure")
|
||||
w.Header().Add("set-cookie", "refresh_token="+token.RefreshToken+"; Path=/; HttpOnly; SameSite=None; Secure")
|
||||
http.Redirect(w, r, "http://localhost:4200/callback?"+value.Encode(), http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
r.Get("/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||
// get refresh token from query string
|
||||
refreshToken := r.URL.Query().Get("refresh_token")
|
||||
redirectTo := r.URL.Query().Get("redirect_to")
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "Refresh token not found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// exchange refresh token for new token
|
||||
token, err := ar.gConfig.TokenSource(r.Context(), &oauth2.Token{RefreshToken: refreshToken}).Token()
|
||||
if err != nil {
|
||||
http.Error(w, "Error exchanging refresh token for token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to frontend with token and refresh token
|
||||
http.Redirect(w, r, "http://localhost:4200/callback?token="+token.AccessToken+"&redirect_to="+redirectTo, http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
r.Get("/revoke", func(w http.ResponseWriter, r *http.Request) {
|
||||
// get access token and refresh token from cookie
|
||||
if cookie, err := r.Cookie("access_token"); err == nil {
|
||||
// request to revoke token at oauth2.googleapis.com/revoke
|
||||
_, err := http.PostForm("https://oauth2.googleapis.com/revoke", url.Values{"token": {cookie.Value}})
|
||||
if err != nil {
|
||||
http.Error(w, "Error revoking token", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// remove cookie with expire from frontend and response no content
|
||||
w.Header().Add("set-cookie", "access_token=; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=0")
|
||||
w.Header().Add("set-cookie", "refresh_token=; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=0")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
r.Get("/user", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
token := &oauth2.Token{}
|
||||
if cookie, err := r.Cookie("access_token"); err == nil {
|
||||
token.AccessToken = cookie.Value
|
||||
}
|
||||
|
||||
// if have refresh token, set refresh token to token
|
||||
if cookie, err := r.Cookie("refresh_token"); err == nil {
|
||||
token.RefreshToken = cookie.Value
|
||||
}
|
||||
|
||||
// get user info
|
||||
client := ar.gConfig.Client(r.Context(), token)
|
||||
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
|
||||
if err != nil {
|
||||
http.Error(w, "Error getting user info", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var userInfo map[string]interface{}
|
||||
if err = json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
||||
http.Error(w, "Error getting user info", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// return user info
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"user": map[string]string{
|
||||
"email": userInfo["email"].(string),
|
||||
"username": userInfo["name"].(string),
|
||||
"image": userInfo["picture"].(string),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ func NewRecipeRouter(data *data.Data) *RecipeRouter {
|
|||
}
|
||||
}
|
||||
|
||||
func (rr *RecipeRouter) Route(r *chi.Mux) {
|
||||
func (rr *RecipeRouter) Route(r chi.Router) {
|
||||
r.Route("/recipes", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
|
|
|||
131
server/server.go
131
server/server.go
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"recipe-manager/data"
|
||||
|
|
@ -36,54 +37,100 @@ func (s *Server) Run() error {
|
|||
|
||||
func createHandler() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"https://*", "http://*"},
|
||||
AllowedOrigins: []string{"http://localhost:4200"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
ExposedHeaders: []string{"Content-Type"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
// Debug: true,
|
||||
}))
|
||||
r.Post("/merge", func(w http.ResponseWriter, r *http.Request) {
|
||||
var targetMap map[string]interface{}
|
||||
err := json.NewDecoder(r.Body).Decode(&targetMap)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed: ", err)
|
||||
return
|
||||
}
|
||||
log.Println(targetMap)
|
||||
master_version := targetMap["master"].(string)
|
||||
dev_version := targetMap["dev"].(string)
|
||||
|
||||
// find target file in the cofffeemachineConfig
|
||||
if _, err := os.Stat("coffeemachineConfig/coffeethai02_" + master_version + ".json"); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed. Master file not found: ", err)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat("coffeemachineConfig/coffeethai02_" + dev_version + ".json"); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed. Dev file not found: ", err)
|
||||
return
|
||||
}
|
||||
database := data.NewData()
|
||||
|
||||
repo_path := "coffeemachineConfig/coffeethai02_"
|
||||
master_path := repo_path + master_version + ".json"
|
||||
dev_path := repo_path + dev_version + ".json"
|
||||
|
||||
// output path
|
||||
output_path := ""
|
||||
|
||||
// changelog path
|
||||
changelog_path := ""
|
||||
|
||||
// TODO: Call merge api if found
|
||||
err = exec.Command("python", "merge", master_path, dev_path, output_path, changelog_path).Run()
|
||||
if err != nil {
|
||||
log.Fatalln("Merge request failed. Python merge failed: ", err)
|
||||
}
|
||||
// Auth Router
|
||||
r.Group(func(r chi.Router) {
|
||||
ar := routers.NewAuthRouter()
|
||||
ar.Route(r)
|
||||
})
|
||||
|
||||
// Protect Group
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
cookie, err := r.Cookie("access_token")
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// verify token by request to google api
|
||||
res, err := http.Get("https://www.googleapis.com/oauth2/v3/tokeninfo?id_token=" + url.QueryEscape(cookie.Value))
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
var tokenInfo map[string]interface{}
|
||||
json.NewDecoder(res.Body).Decode(&tokenInfo)
|
||||
|
||||
log.Println(tokenInfo)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
// Recipe Router
|
||||
rr := routers.NewRecipeRouter(database)
|
||||
rr.Route(r)
|
||||
|
||||
r.Post("/merge", func(w http.ResponseWriter, r *http.Request) {
|
||||
var targetMap map[string]interface{}
|
||||
err := json.NewDecoder(r.Body).Decode(&targetMap)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed: ", err)
|
||||
return
|
||||
}
|
||||
log.Println(targetMap)
|
||||
master_version := targetMap["master"].(string)
|
||||
dev_version := targetMap["dev"].(string)
|
||||
|
||||
// find target file in the cofffeemachineConfig
|
||||
if _, err := os.Stat("coffeemachineConfig/coffeethai02_" + master_version + ".json"); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed. Master file not found: ", err)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat("coffeemachineConfig/coffeethai02_" + dev_version + ".json"); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Fatalln("Merge request failed. Dev file not found: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
repo_path := "coffeemachineConfig/coffeethai02_"
|
||||
master_path := repo_path + master_version + ".json"
|
||||
dev_path := repo_path + dev_version + ".json"
|
||||
|
||||
// output path
|
||||
output_path := ""
|
||||
|
||||
// changelog path
|
||||
changelog_path := ""
|
||||
|
||||
// TODO: Call merge api if found
|
||||
err = exec.Command("python", "merge", master_path, dev_path, output_path, changelog_path).Run()
|
||||
if err != nil {
|
||||
log.Fatalln("Merge request failed. Python merge failed: ", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
rr := routers.NewRecipeRouter(data.NewData())
|
||||
rr.Route(r)
|
||||
|
||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue