From 5c7a72cf0fd693542ec98950f7bc79da8cd22c5d Mon Sep 17 00:00:00 2001 From: Preston Baxter Date: Sat, 4 Nov 2023 17:52:41 -0500 Subject: [PATCH] B: vendors working. How to make rules work --- ui/config/config.go | 47 +++++------ ui/controllers/controllers.go | 1 + ui/controllers/pco.go | 125 +++++++++++++++++++++++++++++- ui/controllers/youtube.go | 22 +++--- ui/db/models/oauth.go | 86 ++++++++++++++++++++ ui/db/models/vendor.go | 16 ++-- ui/db/vendors.go | 22 ++++++ ui/templates/dashboard_page.templ | 11 +-- 8 files changed, 279 insertions(+), 51 deletions(-) create mode 100644 ui/db/models/oauth.go diff --git a/ui/config/config.go b/ui/config/config.go index 977ad6b..e3f55fa 100644 --- a/ui/config/config.go +++ b/ui/config/config.go @@ -8,11 +8,10 @@ import ( ) type config struct { - Mongo *MongoConfig `mapstructure:"mongo"` - YoutubeConfig *YoutubeConfig `mapstructure:"youtube"` - PcoConfig *PcoConfig `mapstructure:"pco"` - JwtSecret string `mapstructure:"jwt_secret"` - Env string `mapstructure:"env"` + Mongo *MongoConfig `mapstructure:"mongo"` + Vendors map[string]*VendorConfig `mapstructure:"vendors"` + JwtSecret string `mapstructure:"jwt_secret"` + Env string `mapstructure:"env"` } type MongoConfig struct { @@ -21,30 +20,21 @@ type MongoConfig struct { EntCol string `mapstructure:"ent_col"` } -type YoutubeConfig struct { - ClientId string `mapstructure:"client_id"` - ClientSecret string `mapstructure:"client_secret"` - Scopes []string `mapstructure:"scopes"` - AuthUri string `mapstructure:"auth_uri"` - TokenUri string `mapstructure:"token_uri"` - scope string +type VendorConfig struct { + ClientId string `mapstructure:"client_id"` + ClientSecret string `mapstructure:"client_secret"` + Scopes []string `mapstructure:"scopes"` + AuthUri string `mapstructure:"auth_uri"` + TokenUri string `mapstructure:"token_uri"` + RefreshEncode string `mapstructure:"refresh_encode"` + scope string } -func (yt *YoutubeConfig) Scope() string { - if yt.scope == "" { - for i, str := range yt.Scopes { - yt.Scopes[i] = fmt.Sprintf("https://www.googleapis.com%s", str) - } - yt.scope = strings.Join(yt.Scopes, " ") +func (pco *VendorConfig) Scope() string { + if pco.scope == "" { + pco.scope = strings.Join(pco.Scopes, " ") } - return yt.scope -} - -type PcoConfig struct { - ClientId string `mapstructure:"client_id"` - ClientSecret string `mapstructure:"client_secret"` - AuthUri string `mapstructure:"auth_uri"` - TokenUri string `mapstructure:"token_uri"` + return pco.scope } var cfg *config @@ -65,6 +55,11 @@ func Init() { if err != nil { panic(err) } + + fmt.Printf("%v\n", cfg) + for key, value := range cfg.Vendors { + fmt.Printf("%s: %v\n", key, value) + } } func Config() *config { diff --git a/ui/controllers/controllers.go b/ui/controllers/controllers.go index 38895d8..6658c37 100644 --- a/ui/controllers/controllers.go +++ b/ui/controllers/controllers.go @@ -50,4 +50,5 @@ func BuildRouter(r *gin.Engine) { pco := vendor.Group("/pco") pco.POST("/initiate", InitiatePCOOuath) + pco.GET("/callback", RecievePCOOuath) } diff --git a/ui/controllers/pco.go b/ui/controllers/pco.go index 39959c1..f9e30e8 100644 --- a/ui/controllers/pco.go +++ b/ui/controllers/pco.go @@ -1,7 +1,128 @@ package controllers -import "github.com/gin-gonic/gin" +import ( + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + "time" + + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config" + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models" + "github.com/gin-gonic/gin" +) + +const PCO_REDIRECT_URI = "https://capstone.preston-baxter.com:8080/vendor/pco/callback" func InitiatePCOOuath(c *gin.Context) { - c.String(200, "ok") + conf := config.Config() + vendorConfig := conf.Vendors[models.PCO_VENDOR_NAME] + + init_url, err := url.Parse(vendorConfig.AuthUri) + if err != nil { + //we should not get here + panic(err) + } + + q := init_url.Query() + q.Add("client_id", vendorConfig.ClientId) + q.Add("redirect_uri", PCO_REDIRECT_URI) + q.Add("response_type", "code") + q.Add("scope", vendorConfig.Scope()) + init_url.RawQuery = q.Encode() + + c.Redirect(302, init_url.String()) +} + +func RecievePCOOuath(c *gin.Context) { + conf := config.Config() + vendorConfig := conf.Vendors[models.PCO_VENDOR_NAME] + user := getUserFromContext(c) + + if user == nil { + log.Error("Unable to find user in context") + c.AbortWithStatus(502) + } + + code := c.Query("code") + //validate returned code + if code == "" { + log.Error("Youtube OAuth response did not contain a code. Possible CSRF") + c.AbortWithStatus(502) + return + } + + client := http.Client{} + + token_url, err := url.Parse(vendorConfig.TokenUri) + if err != nil { + //we should not get here + panic(err) + } + + //Make request to google for credentials + q := token_url.Query() + + q.Add("code", code) + q.Add("client_id", vendorConfig.ClientId) + q.Add("client_secret", vendorConfig.ClientSecret) + q.Add("redirect_uri", PCO_REDIRECT_URI) + q.Add("grant_type", "authorization_code") + + req, err := http.NewRequest("POST", token_url.String(), strings.NewReader(q.Encode())) + if err != nil { + log.WithError(err).Errorf("Failed to generate request with the following url: '%s'", token_url.String()) + c.AbortWithStatus(502) + return + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(req) + if err != nil { + log.WithError(err).Errorf("Failed to make request to the following url: '%s'", token_url.String()) + c.AbortWithStatus(502) + return + } + + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + log.WithError(err).Errorf("Failed to read body from the following url: '%s'", token_url.String()) + c.AbortWithStatus(502) + return + } + + if resp.StatusCode != 200 { + log.Errorf("Response failed with status code: %d. Error: %s", resp.StatusCode ,string(rawBody)) + c.AbortWithStatus(502) + return + } + + oauthResp := &models.OauthCredential{} + err = json.Unmarshal(rawBody, oauthResp) + if err != nil { + log.WithError(err).Errorf("Failed to Unmarshal response from the following url: '%s'", token_url.String()) + c.AbortWithStatus(502) + } + log.Infof("oauthResp: %v", *oauthResp) + //Set expires at time but shave some time off to refresh token before expire date + oauthResp.ExpiresAt = time.Now().Add(time.Duration(oauthResp.ExpiresIn)*time.Second - 10) + + //store credentials + vendor := &models.VendorAccount{ + UserId: user.Id, + OauthCredentials: oauthResp, + Name: models.PCO_VENDOR_NAME, + } + + err = mongo.SaveModel(vendor) + if err != nil { + log.WithError(err).Errorf("Failed to save credentials for user: %s", user.Email) + c.AbortWithStatus(502) + return + } + + c.Redirect(302, "/dashboard") + } diff --git a/ui/controllers/youtube.go b/ui/controllers/youtube.go index 2b8d408..e7b802f 100644 --- a/ui/controllers/youtube.go +++ b/ui/controllers/youtube.go @@ -13,12 +13,13 @@ import ( "github.com/gin-gonic/gin" ) -const REDIRECT_URI = "https://capstone.preston-baxter.com:8080/vendor/youtube/callback" +const YOUTUBE_REDIRECT_URI = "https://capstone.preston-baxter.com:8080/vendor/youtube/callback" func InitiateYoutubeOuath(c *gin.Context) { conf := config.Config() + vendorConfig := conf.Vendors[models.YOUTUBE_VENDOR_NAME] - init_url, err := url.Parse(conf.YoutubeConfig.AuthUri) + init_url, err := url.Parse(vendorConfig.AuthUri) if err != nil { //we should not get here panic(err) @@ -26,10 +27,10 @@ func InitiateYoutubeOuath(c *gin.Context) { q := init_url.Query() //https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#httprest_1 - q.Add("client_id", conf.YoutubeConfig.ClientId) - q.Add("redirect_uri", REDIRECT_URI) + q.Add("client_id", vendorConfig.ClientId) + q.Add("redirect_uri", YOUTUBE_REDIRECT_URI) q.Add("response_type", "code") - q.Add("scope", conf.YoutubeConfig.Scope()) + q.Add("scope", vendorConfig.Scope()) q.Add("access_type", "offline") //used to prevent CSRF q.Add("state", getAuthHash(c)) @@ -40,6 +41,7 @@ func InitiateYoutubeOuath(c *gin.Context) { func ReceiveYoutubeOauth(c *gin.Context) { conf := config.Config() + vendorConfig := conf.Vendors[models.PCO_VENDOR_NAME] user := getUserFromContext(c) if user == nil { @@ -65,7 +67,7 @@ func ReceiveYoutubeOauth(c *gin.Context) { client := http.Client{} - token_url, err := url.Parse(conf.YoutubeConfig.TokenUri) + token_url, err := url.Parse(vendorConfig.TokenUri) if err != nil { //we should not get here panic(err) @@ -75,9 +77,9 @@ func ReceiveYoutubeOauth(c *gin.Context) { q := token_url.Query() q.Add("code", code) - q.Add("client_id", conf.YoutubeConfig.ClientId) - q.Add("client_secret", conf.YoutubeConfig.ClientSecret) - q.Add("redirect_uri", REDIRECT_URI) + q.Add("client_id", vendorConfig.ClientId) + q.Add("client_secret", vendorConfig.ClientSecret) + q.Add("redirect_uri", YOUTUBE_REDIRECT_URI) q.Add("grant_type", "authorization_code") req, err := http.NewRequest("POST", token_url.String(), strings.NewReader(q.Encode())) @@ -124,7 +126,7 @@ func ReceiveYoutubeOauth(c *gin.Context) { vendor := &models.VendorAccount{ UserId: user.Id, OauthCredentials: oauthResp, - Name: "youtube", + Name: models.YOUTUBE_VENDOR_NAME, } err = mongo.SaveModel(vendor) diff --git a/ui/db/models/oauth.go b/ui/db/models/oauth.go new file mode 100644 index 0000000..41b0f3a --- /dev/null +++ b/ui/db/models/oauth.go @@ -0,0 +1,86 @@ +package models + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "strings" + "time" + + "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config" +) + +type OauthCredential struct { + AccessToken string `bson:"access_token,omitempty" json:"access_token,omitempty"` + ExpiresIn int `bson:"expires_in,omitempty" json:"expires_in,omitempty"` + ExpiresAt time.Time `bson:"expires_at,omitempty" json:"expires_at,omitempty"` + TokenType string `bson:"token_type,omitempty" json:"token_type,omitempty"` + Scope string `bson:"scope,omitempty" json:"scope,omitempty"` + RefreshToken string `bson:"refresh_token,omitempty" json:"refresh_token,omitempty"` +} + +type OauthRefreshBody struct { + ClientId string `json:"cleint_id"` + ClientSecret string `json:"cleint_secret"` + GrantType string `json:"grant_type"` + RefreshToken string `json:"refresh_token"` +} + +func (oc *OauthCredential) RefreshAccessToken(vendor string) error { + conf := config.Config() + vendorConfig := conf.Vendors[vendor] + + refresh_url, err := url.Parse(vendorConfig.TokenUri) + if err != nil { + return err + } + + var body io.Reader + switch vendorConfig.RefreshEncode { + case "json": + refreshBody := OauthRefreshBody{ + ClientId: vendorConfig.ClientId, + ClientSecret: vendorConfig.ClientSecret, + GrantType: "refresh_token", + RefreshToken: oc.RefreshToken, + } + raw, err := json.Marshal(&refreshBody) + if err != nil { + panic(err) + } + body = bytes.NewReader(raw) + case "url": + q := refresh_url.Query() + q.Add("client_id", vendorConfig.ClientId) + q.Add("client_secret", vendorConfig.ClientSecret) + q.Add("code", oc.RefreshToken) + q.Add("grant_type", "refresh_token") + + body = strings.NewReader(q.Encode()) + default: + panic(errors.New("Unkoown Encode Scheme")) + } + + client := http.Client{} + req, err := http.NewRequest("POST", refresh_url.String(), body) + if err != nil { + return err + } + + resp, err := client.Do(req) + if err != nil { + return err + } + rawBody, err := io.ReadAll(resp.Body) + + err = json.Unmarshal(rawBody, oc) + if err != nil { + return err + } + oc.ExpiresAt = time.Now().Add(time.Duration(oc.ExpiresIn)*time.Second - 10) + + return nil +} diff --git a/ui/db/models/vendor.go b/ui/db/models/vendor.go index 7c3d87d..8c6dbce 100644 --- a/ui/db/models/vendor.go +++ b/ui/db/models/vendor.go @@ -1,9 +1,11 @@ package models import ( + "net/http" "time" "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" ) const VENDOR_ACCOUNT_TYPE = "vendor_account" @@ -13,14 +15,6 @@ const ( PCO_VENDOR_NAME = "PCO" ) -type OauthCredential struct { - AccessToken string `bson:"access_token,omitempty" json:"access_token,omitempty"` - ExpiresIn int `bson:"expires_in,omitempty" json:"expires_in,omitempty"` - ExpiresAt time.Time `bson:"expires_at,omitempty" json:"expires_at,omitempty"` - TokenType string `bson:"token_type,omitempty" json:"token_type,omitempty"` - Scope string `bson:"scope,omitempty" json:"scope,omitempty"` - RefreshToken string `bson:"refresh_token,omitempty" json:"refresh_token,omitempty"` -} type VendorAccount struct { *CommonFields `bson:"obj_info"` @@ -49,3 +43,9 @@ func (va *VendorAccount) UpdateObjectInfo() { } va.UpdatedAt = now } + +func (va *VendorAccount) MakeRequest(req *http.Request, db *mongo.Client) error { + if va.OauthCredentials.ExpiresAt.Before(time.Now()) { + va.OauthCredentials.RefreshAccessToken(va.Name) + } +} diff --git a/ui/db/vendors.go b/ui/db/vendors.go index 13d56eb..a80b297 100644 --- a/ui/db/vendors.go +++ b/ui/db/vendors.go @@ -2,6 +2,9 @@ package db import ( "context" + "fmt" + "net/http" + "time" "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config" "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models" @@ -31,3 +34,22 @@ func (db *DB) FindVendorAccountByUser(userId primitive.ObjectID) ([]models.Vendo return vendors, nil } + +//Make +func (db *DB) MakeRequestWithAccount(req *http.Request, va *models.VendorAccount) (*http.Response, error) { + //make new credential and save new credentials to DB + if va.OauthCredentials.ExpiresAt.Before(time.Now()) { + err := va.OauthCredentials.RefreshAccessToken(va.Name) + if err != nil { + return nil, err + } + err = db.SaveModel(va) + if err != nil { + return nil, err + } + } + + client := http.Client{} + req.Header.Add("Authorization", fmt.Sprintf("%s: %s", va.OauthCredentials.TokenType, va.OauthCredentials.AccessToken)) + return client.Do(req) +} diff --git a/ui/templates/dashboard_page.templ b/ui/templates/dashboard_page.templ index 38e8d72..de496a5 100644 --- a/ui/templates/dashboard_page.templ +++ b/ui/templates/dashboard_page.templ @@ -2,7 +2,6 @@ package templates import ( "fmt" - "time" "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models" ) @@ -227,9 +226,11 @@ templ DashboardVendorDropDown() { Youtube - - PCO - +
+ +
@@ -278,7 +279,7 @@ templ DashboardVendorWidget(vendors []models.VendorAccount) { { vendor.Name } - if vendor.OauthCredentials != nil && vendor.OauthCredentials.AccessToken != "" && vendor.OauthCredentials.ExpiresAt.Before(time.Now()) { + if vendor.OauthCredentials != nil && vendor.OauthCredentials.AccessToken != "" { Active } else {