B: vendors working. How to make rules work

This commit is contained in:
Preston Baxter 2023-11-04 17:52:41 -05:00
parent d36eb955ab
commit 5c7a72cf0f
8 changed files with 279 additions and 51 deletions

View File

@ -9,8 +9,7 @@ import (
type config struct { type config struct {
Mongo *MongoConfig `mapstructure:"mongo"` Mongo *MongoConfig `mapstructure:"mongo"`
YoutubeConfig *YoutubeConfig `mapstructure:"youtube"` Vendors map[string]*VendorConfig `mapstructure:"vendors"`
PcoConfig *PcoConfig `mapstructure:"pco"`
JwtSecret string `mapstructure:"jwt_secret"` JwtSecret string `mapstructure:"jwt_secret"`
Env string `mapstructure:"env"` Env string `mapstructure:"env"`
} }
@ -21,30 +20,21 @@ type MongoConfig struct {
EntCol string `mapstructure:"ent_col"` EntCol string `mapstructure:"ent_col"`
} }
type YoutubeConfig struct { type VendorConfig struct {
ClientId string `mapstructure:"client_id"` ClientId string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"` ClientSecret string `mapstructure:"client_secret"`
Scopes []string `mapstructure:"scopes"` Scopes []string `mapstructure:"scopes"`
AuthUri string `mapstructure:"auth_uri"` AuthUri string `mapstructure:"auth_uri"`
TokenUri string `mapstructure:"token_uri"` TokenUri string `mapstructure:"token_uri"`
RefreshEncode string `mapstructure:"refresh_encode"`
scope string scope string
} }
func (yt *YoutubeConfig) Scope() string { func (pco *VendorConfig) Scope() string {
if yt.scope == "" { if pco.scope == "" {
for i, str := range yt.Scopes { pco.scope = strings.Join(pco.Scopes, " ")
yt.Scopes[i] = fmt.Sprintf("https://www.googleapis.com%s", str)
} }
yt.scope = strings.Join(yt.Scopes, " ") return pco.scope
}
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"`
} }
var cfg *config var cfg *config
@ -65,6 +55,11 @@ func Init() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Printf("%v\n", cfg)
for key, value := range cfg.Vendors {
fmt.Printf("%s: %v\n", key, value)
}
} }
func Config() *config { func Config() *config {

View File

@ -50,4 +50,5 @@ func BuildRouter(r *gin.Engine) {
pco := vendor.Group("/pco") pco := vendor.Group("/pco")
pco.POST("/initiate", InitiatePCOOuath) pco.POST("/initiate", InitiatePCOOuath)
pco.GET("/callback", RecievePCOOuath)
} }

View File

@ -1,7 +1,128 @@
package controllers 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) { 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")
} }

View File

@ -13,12 +13,13 @@ import (
"github.com/gin-gonic/gin" "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) { func InitiateYoutubeOuath(c *gin.Context) {
conf := config.Config() 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 { if err != nil {
//we should not get here //we should not get here
panic(err) panic(err)
@ -26,10 +27,10 @@ func InitiateYoutubeOuath(c *gin.Context) {
q := init_url.Query() q := init_url.Query()
//https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#httprest_1 //https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#httprest_1
q.Add("client_id", conf.YoutubeConfig.ClientId) q.Add("client_id", vendorConfig.ClientId)
q.Add("redirect_uri", REDIRECT_URI) q.Add("redirect_uri", YOUTUBE_REDIRECT_URI)
q.Add("response_type", "code") q.Add("response_type", "code")
q.Add("scope", conf.YoutubeConfig.Scope()) q.Add("scope", vendorConfig.Scope())
q.Add("access_type", "offline") q.Add("access_type", "offline")
//used to prevent CSRF //used to prevent CSRF
q.Add("state", getAuthHash(c)) q.Add("state", getAuthHash(c))
@ -40,6 +41,7 @@ func InitiateYoutubeOuath(c *gin.Context) {
func ReceiveYoutubeOauth(c *gin.Context) { func ReceiveYoutubeOauth(c *gin.Context) {
conf := config.Config() conf := config.Config()
vendorConfig := conf.Vendors[models.PCO_VENDOR_NAME]
user := getUserFromContext(c) user := getUserFromContext(c)
if user == nil { if user == nil {
@ -65,7 +67,7 @@ func ReceiveYoutubeOauth(c *gin.Context) {
client := http.Client{} client := http.Client{}
token_url, err := url.Parse(conf.YoutubeConfig.TokenUri) token_url, err := url.Parse(vendorConfig.TokenUri)
if err != nil { if err != nil {
//we should not get here //we should not get here
panic(err) panic(err)
@ -75,9 +77,9 @@ func ReceiveYoutubeOauth(c *gin.Context) {
q := token_url.Query() q := token_url.Query()
q.Add("code", code) q.Add("code", code)
q.Add("client_id", conf.YoutubeConfig.ClientId) q.Add("client_id", vendorConfig.ClientId)
q.Add("client_secret", conf.YoutubeConfig.ClientSecret) q.Add("client_secret", vendorConfig.ClientSecret)
q.Add("redirect_uri", REDIRECT_URI) q.Add("redirect_uri", YOUTUBE_REDIRECT_URI)
q.Add("grant_type", "authorization_code") q.Add("grant_type", "authorization_code")
req, err := http.NewRequest("POST", token_url.String(), strings.NewReader(q.Encode())) req, err := http.NewRequest("POST", token_url.String(), strings.NewReader(q.Encode()))
@ -124,7 +126,7 @@ func ReceiveYoutubeOauth(c *gin.Context) {
vendor := &models.VendorAccount{ vendor := &models.VendorAccount{
UserId: user.Id, UserId: user.Id,
OauthCredentials: oauthResp, OauthCredentials: oauthResp,
Name: "youtube", Name: models.YOUTUBE_VENDOR_NAME,
} }
err = mongo.SaveModel(vendor) err = mongo.SaveModel(vendor)

86
ui/db/models/oauth.go Normal file
View File

@ -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
}

View File

@ -1,9 +1,11 @@
package models package models
import ( import (
"net/http"
"time" "time"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
) )
const VENDOR_ACCOUNT_TYPE = "vendor_account" const VENDOR_ACCOUNT_TYPE = "vendor_account"
@ -13,14 +15,6 @@ const (
PCO_VENDOR_NAME = "PCO" 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 { type VendorAccount struct {
*CommonFields `bson:"obj_info"` *CommonFields `bson:"obj_info"`
@ -49,3 +43,9 @@ func (va *VendorAccount) UpdateObjectInfo() {
} }
va.UpdatedAt = now 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)
}
}

View File

@ -2,6 +2,9 @@ package db
import ( import (
"context" "context"
"fmt"
"net/http"
"time"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config" "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models" "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 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)
}

View File

@ -2,7 +2,6 @@ package templates
import ( import (
"fmt" "fmt"
"time"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models" "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
) )
@ -227,9 +226,11 @@ templ DashboardVendorDropDown() {
Youtube Youtube
</button> </button>
</form> </form>
<a hx-post="/vendor/pco/initiate" href="" class="text-sm py-2 px-4 font-normal block w-full whitespace-nowrap bg-transparent text-blueGray-700"> <form action="/vendor/pco/initiate" method="POST">
PCO <button type="submit" class="text-sm align-left py-2 px-4 font-normal block w-full whitespace-nowrap bg-transparent text-blueGray-700">
</a> Planning Center
</button>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -278,7 +279,7 @@ templ DashboardVendorWidget(vendors []models.VendorAccount) {
{ vendor.Name } { vendor.Name }
</th> </th>
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left"> <th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
if vendor.OauthCredentials != nil && vendor.OauthCredentials.AccessToken != "" && vendor.OauthCredentials.ExpiresAt.Before(time.Now()) { if vendor.OauthCredentials != nil && vendor.OauthCredentials.AccessToken != "" {
Active Active
} else { } else {
<button>Log in</button> <button>Log in</button>