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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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
</button>
</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">
PCO
</a>
<form action="/vendor/pco/initiate" method="POST">
<button type="submit" class="text-sm align-left py-2 px-4 font-normal block w-full whitespace-nowrap bg-transparent text-blueGray-700">
Planning Center
</button>
</form>
</div>
</div>
</div>
@ -278,7 +279,7 @@ templ DashboardVendorWidget(vendors []models.VendorAccount) {
{ vendor.Name }
</th>
<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
} else {
<button>Log in</button>