B: Big squash

B: Trying tailwind things

update gitignore

B: Updates after moving machines

B: Action Skeleton

B: Add pco vendor to service directory. And tests

B: add extra pco structs

B: Catch up commit
This commit is contained in:
Preston Baxter 2023-11-07 21:34:57 -06:00 committed by Preston Baxter
parent 96be01ea72
commit 360163f2dd
44 changed files with 4505 additions and 174 deletions

1
libs/oauth2 Submodule

@ -0,0 +1 @@
Subproject commit d87fd3250ebcb3e014780fe332c13067aa665174

46
service/.air.toml Normal file
View File

@ -0,0 +1,46 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "make local-build"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "dist", "docker", "node_modules"]
exclude_file = []
exclude_regex = ["_test.go", "_templ.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "templ", "css"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

1
service/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
tmp/

View File

@ -1,5 +1,7 @@
BASE_URL="us-central1-docker.pkg.dev/pbaxter-infra/capstone-repo"
local-build:
GOEXPERIMENT=loopvar go build -o ./tmp/main .
build:
docker build . -t webhook-service:latest

View File

@ -0,0 +1,32 @@
package controllers
import (
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
var (
log *logrus.Logger
mongo *db.DB
)
func BuildRouter(r *gin.Engine) {
conf := config.Config()
log = logrus.New()
log.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
})
var err error
mongo, err = db.NewClient(conf.Mongo.Uri)
if err != nil {
panic(err)
}
pco := r.Group("/pco")
pco.Use(ValidatePcoWebhook)
pco.POST("/:userid", ConsumePcoWebhook)
}

View File

@ -0,0 +1,50 @@
package controllers
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"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_VALIDATE_HEADER = "X-PCO-Webhooks-Authenticity"
func ValidatePcoWebhook(c *gin.Context) {
conf := config.Config()
//get remote version from header
remoteDigestStr := c.GetHeader(PCO_VALIDATE_HEADER)
if remoteDigestStr == "" {
log.Warnf("Request was sent with no %s header. Rejecting", PCO_VALIDATE_HEADER)
c.AbortWithStatus(401)
return
}
pcoSig := make([]byte, len(remoteDigestStr)/2)
_, err := hex.Decode(pcoSig, []byte(remoteDigestStr))
//clone request to harmlessly inspect the body
bodyReader := c.Request.Clone(context.Background()).Body
body, err := io.ReadAll(bodyReader)
if err != nil {
log.WithError(err).Error("Failed to read body while validating PCO webhook")
c.AbortWithError(501, err)
return
}
//Get secret
key := conf.Vendors[models.PCO_VENDOR_NAME].WebhookSecret
//Get HMAC
hmacSig := hmac.New(sha256.New, []byte(key))
hmacSig.Write(body)
if !hmac.Equal(hmacSig.Sum(nil), pcoSig) {
log.Warn("")
c.AbortWithStatus(401)
}
}

View File

@ -0,0 +1,98 @@
package controllers
import (
"errors"
"fmt"
"regexp"
"sync"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
"github.com/gin-gonic/gin"
"github.com/google/jsonapi"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var (
eventRegexKeys = map[string]string{"plan": `^services\.v2\.events\.plan\..*`}
actionFuncMap = map[string]actionFunc{"youtube.livestream": ScheduleLiveStreamFromWebhook}
)
type actionFunc func(*gin.Context, *webhooks.EventDelivery) error
func ConsumePcoWebhook(c *gin.Context) {
userId := c.Param("userid")
if userId == "" {
log.Warn("Webhook did not contain user id. Rejecting")
c.AbortWithStatus(404)
return
}
//get actions for user
userObjectId, err := primitive.ObjectIDFromHex(userId)
if err != nil {
log.WithError(err).Warn("User Id was malformed")
c.AbortWithStatus(400)
return
}
//read body and handle io in parallel because IO shenanigains
wg := new(sync.WaitGroup)
wg.Add(2)
var actionMappings []models.ActionMapping
var webhookBody *webhooks.EventDelivery
errs := make([]error, 2)
go func(wg *sync.WaitGroup) {
actionMappings, errs[0] = mongo.FindActionMappingsByUser(userObjectId)
wg.Done()
}(wg)
go func(wg *sync.WaitGroup) {
errs[1] = jsonapi.UnmarshalPayload(c.Request.Body, webhookBody)
wg.Done()
}(wg)
wg.Wait()
if err := errors.Join(errs...); err != nil {
log.WithError(err).Errorf("Failed to do the IO parts")
_ = c.AbortWithError(501, err)
return
}
//perform actions
//loop through all actions a user has
for _, mapping := range actionMappings {
//find the ones that are runable by this function
if mapping.SourceEvent.VendorName == models.PCO_VENDOR_NAME && eventMatch(webhookBody.Name) {
//generate lookup key for function
actionKey := fmt.Sprintf("%s:%s", mapping.Action.VendorName, mapping.Action.Type)
//if function exists run the function
if action, ok := actionFuncMap[actionKey]; ok {
err = action(c, webhookBody)
//handle error
if err != nil {
log.WithError(err).Errorf("Failed to execute action: %s. From event source: %s:%s", actionKey, mapping.SourceEvent.VendorName, mapping.SourceEvent.Key)
_ = c.AbortWithError(501, err)
}
}
}
}
}
func eventMatch(event string) bool {
if regexString, ok := eventRegexKeys[event]; ok {
reg := regexp.MustCompile(regexString)
return reg.MatchString(event)
} else {
return false
}
}
func ScheduleLiveStreamFromWebhook(c *gin.Context, body *webhooks.EventDelivery) error {
return nil
}

View File

@ -1,32 +1,70 @@
module preston-baxter.com/capstone/webhook-service
module git.preston-baxter.com/Preston_PLB/capstone/webhook-service
go 1.19
require github.com/gin-gonic/gin v1.9.1
require (
git.preston-baxter.com/Preston_PLB/capstone/frontend-service v0.0.0-00010101000000-000000000000
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/assert/v2 v2.2.0
github.com/google/jsonapi v1.0.0
github.com/sirupsen/logrus v1.9.3
go.mongodb.org/mongo-driver v1.12.1
golang.org/x/oauth2 v0.14.0
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.17.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace git.preston-baxter.com/Preston_PLB/capstone/frontend-service => ../ui
replace golang.org/x/oauth2 => ../libs/oauth2

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
package main
import (
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
@ -8,10 +10,17 @@ import (
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
r.POST("/webhook", func(c *gin.Context) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
panic(err)
}
fmt.Printf("captured: %s\n", string(body))
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
r.Run("0.0.0.0:8081") // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

64
service/vendors/pco/pco.go vendored Normal file
View File

@ -0,0 +1,64 @@
package pco
import (
"context"
"net/http"
"net/url"
"golang.org/x/oauth2"
)
const PCO_API_URL = "https://api.planningcenteronline.com"
type PcoApiClient struct {
oauth *oauth2.Config
token *oauth2.Token
client *http.Client
url *url.URL
}
func NewClient() *PcoApiClient {
pco_url, err := url.Parse(PCO_API_URL)
if err != nil {
panic(err)
}
pco := &PcoApiClient{
oauth: &oauth2.Config{},
token: &oauth2.Token{},
url: pco_url,
}
return pco
}
func NewClientWithOauthConfig(conf *oauth2.Config, token *oauth2.Token) *PcoApiClient {
pco_url, err := url.Parse(PCO_API_URL)
if err != nil {
panic(err)
}
pco := &PcoApiClient{
oauth: conf,
token: token,
url: pco_url,
}
return pco
}
func (api *PcoApiClient) getClient() *http.Client {
if api.client == nil {
api.client = api.oauth.Client(context.Background(), api.token)
}
return api.client
}
func (api *PcoApiClient) Url() *url.URL {
return api.url
}
func (api *PcoApiClient) Do(req *http.Request) (*http.Response, error) {
return api.getClient().Do(req)
}

61
service/vendors/pco/services.go vendored Normal file
View File

@ -0,0 +1,61 @@
package pco
import (
"fmt"
"net/http"
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/services"
"github.com/google/jsonapi"
)
func (api *PcoApiClient) GetPlan(service_type_id, plan_id string) (*services.Plan, error){
api.Url().Path = fmt.Sprintf("/services/v2/service_types/%s/plans/%s", service_type_id, plan_id)
req, err := http.NewRequest(http.MethodGet, api.Url().String(), nil)
if err != nil {
return nil, err
}
resp, err := api.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 || resp.StatusCode < 200 {
return nil, fmt.Errorf("Failed to retrieve plan with status code: %d", resp.StatusCode)
}
plan := &services.Plan{}
err = jsonapi.UnmarshalPayload(resp.Body, plan)
if err != nil {
return nil, err
}
return plan, nil
}
func (api *PcoApiClient) GetPlanTimes(service_type_id, plan_id string) (*services.PlanTime, error) {
api.Url().Path = fmt.Sprintf("/services/v2/service_types/%s/plans/%s/plan_times", service_type_id, plan_id)
req, err := http.NewRequest(http.MethodGet, api.Url().String(), nil)
if err != nil {
return nil, err
}
resp, err := api.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode > 299 || resp.StatusCode < 200 {
return nil, fmt.Errorf("Failed to retrieve plan with status code: %d", resp.StatusCode)
}
planTime := &services.PlanTime{}
err = jsonapi.UnmarshalPayload(resp.Body, planTime)
if err != nil {
return nil, err
}
return planTime, nil
}

View File

@ -0,0 +1,5 @@
package services
type AttachmentType struct {
Id string `jsonapi:"primary,AttachmentType"`
}

View File

@ -0,0 +1,5 @@
package services
type LinkedPublishingEpisode struct {
Id string `jsonapi:"primary,LinkedPublishingEpisode"`
}

View File

@ -0,0 +1,5 @@
package services
type Organization struct {
Id string `jsonapi:"primary,Organization"`
}

View File

@ -0,0 +1,5 @@
package services
type Person struct {
Id string `jsonapi:"primary,Person"`
}

42
service/vendors/pco/services/plan.go vendored Normal file
View File

@ -0,0 +1,42 @@
package services
import "time"
type Plan struct {
Id string `jsonapi:"primary,Plan"`
//attrs
CanViewOrder bool `jsonapi:"attr,can_view_order,omitempty"`
CreatedAt time.Time `jsonapi:"attr,created_at,rfc3339,omitempty"`
Dates string `jsonapi:"attr,dates,omitempty"`
FilesExpireAt time.Time `jsonapi:"attr,files_expire_at,rfc3339,omitempty"`
ItemsCount int `jsonapi:"attr,items_count,omitempty"`
LastTimeAt time.Time `jsonapi:"attr,last_time_at,rfc3339,omitempty"`
MultiDay bool `jsonapi:"attr,multi_day,omitempty"`
NeededPositiionsCount int `jsonapi:"attr,needed_positions_count,omitempty"`
OtherTimeCount int `jsonapi:"attr,other_time_count,omitempty"`
Permissions string `jsonapi:"attr,permissions,omitempty"`
PlanNotesCount int `jsonapi:"attr,plan_notes_count,omitempty"`
PlanPeopleCount int `jsonapi:"attr,plan_people_count,omitempty"`
PlanningCenterUrl string `jsonapi:"attr,planning_center_url,omitempty"`
PerfersOrderView bool `jsonapi:"attr,prefers_order_view,omitempty"`
Public bool `jsonapi:"attr,public,omitempty"`
Rehearsable bool `jsonapi:"attr,rehearsable,omitempty"`
RehearsableTimeCount int `jsonapi:"attr,rehearsable_time_count,omitempty"`
RemindersDisabled bool `jsonapi:"attr,reminders_disabled,omitempty"`
SeriesTitle string `jsonapi:"attr,series_title,omitempty"`
ServiceTimeCount int `jsonapi:"attr,service_time_count,omitempty"`
ShortDates string `jsonapi:"attr,short_dates,omitempty"`
SortDate time.Time `jsonapi:"attr,sort_date,rfc3339,omitempty"`
Title string `jsonapi:"attr,title,omitempty"`
TotalLength int `jsonapi:"attr,total_length,omitempty"`
UpdatedAt time.Time `jsonapi:"attr,updated_at,rfc3339,omitempty"`
//relations
ServiceType *ServiceType `jsonapi:"relation,service_type,omitempty"`
NextPlan *Plan `jsonapi:"relation,next_plan,omitempty"`
PreviousPlan *Plan `jsonapi:"relation,previous_plan,omitempty"`
AttachmentTypes *[]AttachmentType `jsonapi:"relation,AttachmentTypes,omitempty"`
Series *Series `jsonapi:"relation,series,omitempty"`
CreatedBy *Person `jsonapi:"relation,created_by,omitempty"`
UpdatedBy *Person `jsonapi:"relation,updated_by,omitempty"`
LinkedPublishingEpisode *LinkedPublishingEpisode `jsonapi:"relation,linked_publishing_episode,omitempty"`
}

View File

@ -0,0 +1,19 @@
package services
import "time"
type PlanTime struct {
//id
Id string `jsonapi:"primary,PlanTime"`
//attributes
CreatedAt time.Time `jsonapi:"attr,created_at,rfc3339,omitempty"`
StartsAt time.Time `jsonapi:"attr,live_starts_at,rfc3339,omitempty"`
EndsAt time.Time `jsonapi:"attr,ends_at,rfc3339,omitempty"`
LiveEndsAt time.Time `jsonapi:"attr,live_ends_at,rfc3339,omitempty"`
LiveStartsAt time.Time `jsonapi:"attr,live_starts_at,rfc3339,omitempty"`
TeamReminders []interface{} `jsonapi:"attr,team_reminders,rfc3339,omitempty"`
TimeType string `jsonapi:"attr,time_type,omitempty"`
UpdatedAt time.Time `jsonapi:"attr,updated_at,rfc3339,omitempty"`
//relations
AssignedTeams *[]Team `jsonapi:"relation,assigned_teams,omitempty"`
}

View File

@ -0,0 +1,6 @@
package services
type Series struct {
Id string `jsonapi:"primary,Series"`
}

View File

@ -0,0 +1,6 @@
package services
type ServiceType struct {
Id string `jsonapi:"primary,ServiceType"`
}

View File

@ -0,0 +1,84 @@
package services_test
import (
"bytes"
"strings"
"testing"
"time"
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/services"
"github.com/go-playground/assert/v2"
"github.com/google/jsonapi"
)
const valid_string = `{"data":{"type":"Plan","id":"69052110","attributes":{"can_view_order":true,"created_at":"2023-11-11T16:29:47Z","dates":"No dates","items_count":0,"multi_day":false,"needed_positions_count":0,"other_time_count":0,"permissions":"Administrator","plan_notes_count":0,"plan_people_count":0,"planning_center_url":"https://services.planningcenteronline.com/plans/69052110","prefers_order_view":true,"public":false,"rehearsable":true,"rehearsal_time_count":0,"reminders_disabled":false,"service_time_count":0,"short_dates":"No dates","sort_date":"2023-11-11T16:29:47Z","total_length":0,"updated_at":"2023-11-11T16:29:47Z"}}}`
func TestStructs(t *testing.T) {
created_at, err := time.Parse(time.RFC3339, "2023-11-11T16:29:47Z")
if err != nil {
t.Fatal(err)
return
}
sort_date, err := time.Parse(time.RFC3339, "2023-11-11T16:29:47Z")
if err != nil {
t.Fatal(err)
return
}
updated_at, err := time.Parse(time.RFC3339, "2023-11-11T16:29:47Z")
if err != nil {
t.Fatal(err)
return
}
plan := services.Plan{
Id: "69052110",
CanViewOrder: true,
CreatedAt: created_at,
Dates: "No dates",
ItemsCount: 0,
MultiDay: false,
NeededPositiionsCount: 0,
OtherTimeCount: 0,
Permissions: "Administrator",
PlanNotesCount: 0,
PlanPeopleCount: 0,
PlanningCenterUrl: "https://services.planningcenteronline.com/plans/69052110",
PerfersOrderView: true,
Public: false,
Rehearsable: true,
RehearsableTimeCount: 0,
RemindersDisabled: false,
ServiceTimeCount: 0,
ShortDates: "No dates",
Title: "",
TotalLength: 0,
SortDate: sort_date,
UpdatedAt: updated_at,
}
valid_plan := &services.Plan{}
test_plan := &services.Plan{}
err = jsonapi.UnmarshalPayload(strings.NewReader(valid_string), valid_plan)
if err != nil {
t.Fatal(err)
return
}
buf := bytes.NewBuffer([]byte{})
err = jsonapi.MarshalPayload(buf, &plan)
if err != nil {
t.Fatal(err)
return
}
err = jsonapi.UnmarshalPayload(buf, test_plan)
if err != nil {
t.Fatal(err)
return
}
assert.Equal(t, test_plan, valid_plan)
}

5
service/vendors/pco/services/team.go vendored Normal file
View File

@ -0,0 +1,5 @@
package services
type Team struct {
Id string `jsonapi:"primary,Team"`
}

18
service/vendors/pco/webhooks/structs.go vendored Normal file
View File

@ -0,0 +1,18 @@
package webhooks
import "git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/services"
//Structure delivered to target when sending webhooks
type EventDelivery struct {
//uuid of the EventDelivery
ID string `jsonapi:"primary,EventDelivery"`
//name of the event being sent. ex: services.v2.events.plan.updated
//this coressponds to the scopes you set when configuring webhooks
Name string `jsonapi:"attr,name"`
//number of attemts taken to deliver the event
Attempt int `jsonapi:"attr,attempt"`
//JSON:API string of the event
Payload string `jsonapi:"attr,attempt"`
//Owner Organization of the event
Organization *services.Organization `jsonapi:"relation,organization"`
}

BIN
service/webhook-service Executable file

Binary file not shown.

View File

@ -7,7 +7,7 @@ tmp_dir = "tmp"
bin = "./tmp/main"
cmd = "make local-build"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "dist", "docker"]
exclude_dir = ["assets", "tmp", "vendor", "testdata", "dist", "docker", "node_modules"]
exclude_file = []
exclude_regex = ["_test.go", "_templ.go"]
exclude_unchanged = false

4
ui/.gitignore vendored
View File

@ -3,3 +3,7 @@ tmp/*
**/*_templ.go
templates/*.html
docker/tmp/*
node_modules/*
node_modules
config.yaml
conf.yaml

View File

@ -5,6 +5,7 @@ import (
"strings"
"github.com/spf13/viper"
"golang.org/x/oauth2"
)
type config struct {
@ -18,6 +19,8 @@ type MongoConfig struct {
Uri string `mapstructure:"uri"`
EntDb string `mapstructure:"ent_db"`
EntCol string `mapstructure:"ent_col"`
LockDb string `mapstructure:"lock_db"`
LockCol string `mapstructure:"lock_col"`
}
type VendorConfig struct {
@ -27,14 +30,29 @@ type VendorConfig struct {
AuthUri string `mapstructure:"auth_uri"`
TokenUri string `mapstructure:"token_uri"`
RefreshEncode string `mapstructure:"refresh_encode"`
WebhookSecret string `mapstructure:"webhook_secret"`
scope string
}
func (pco *VendorConfig) Scope() string {
if pco.scope == "" {
pco.scope = strings.Join(pco.Scopes, " ")
func (vendor *VendorConfig) Scope() string {
if vendor.scope == "" {
vendor.scope = strings.Join(vendor.Scopes, " ")
}
return vendor.scope
}
func (vendor *VendorConfig) OauthConfig() *oauth2.Config {
return &oauth2.Config{
ClientID: vendor.ClientId,
ClientSecret: vendor.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: vendor.AuthUri,
TokenURL: vendor.TokenUri,
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "",
Scopes: vendor.Scopes,
}
return pco.scope
}
var cfg *config

61
ui/controllers/actions.go Normal file
View File

@ -0,0 +1,61 @@
package controllers
import (
"strings"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
"github.com/gin-gonic/gin"
)
func AddActionFromForm(c *gin.Context) {
user := getUserFromContext(c)
if user == nil {
log.Warnf("Could not find user in context. Trying to redner Action form")
badRequest(c, "No user available in context")
return
}
//parse the form
c.Request.ParseForm()
var source []string
var action []string
//validate source
if str := c.Request.FormValue("source"); str != "" {
source = strings.Split(str, ".")
} else {
log.Warnf("Form request was partially or fully blank")
badRequest(c, "Form request was partially or fully blank")
return
}
//validate action
if str := c.Request.FormValue("action"); str != "" {
action = strings.Split(str, ".")
} else {
log.Warnf("Form request was partially or fully blank")
badRequest(c, "Form request was partially or fully blank")
return
}
//setup action
//
am := &models.ActionMapping{
UserId: user.Id,
SourceEvent: &models.Event{
VendorName: source[0],
Key: source[1],
Fields: map[string]string{},
},
Action: &models.Action{
VendorName: action[0],
Type: action[1],
Fields: map[string]string{},
},
}
mongo.SaveModel(am)
c.Redirect(302, "/dashboard")
}

View File

@ -41,6 +41,9 @@ func BuildRouter(r *gin.Engine) {
dashboard := r.Group("/dashboard")
dashboard.Use(AuthMiddleware(true))
dashboard.GET("", DashboardPage)
//Dashboard Actions
dashboardActions := dashboard.Group("/action")
dashboardActions.POST("/add", AddActionFromForm)
//Dashboard Forms
dashboardForms := dashboard.Group("/forms")
dashboardForms.GET("/addAction", GetAddActionForm)

View File

@ -41,7 +41,7 @@ func InitiateYoutubeOuath(c *gin.Context) {
func ReceiveYoutubeOauth(c *gin.Context) {
conf := config.Config()
vendorConfig := conf.Vendors[models.PCO_VENDOR_NAME]
vendorConfig := conf.Vendors[models.YOUTUBE_VENDOR_NAME]
user := getUserFromContext(c)
if user == nil {

View File

@ -1,8 +1,14 @@
package models
import "go.mongodb.org/mongo-driver/bson/primitive"
import (
"time"
const ACTION_MAPPING_TYPE = "action"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
ACTION_MAPPING_TYPE = "action"
)
type ActionMapping struct {
*CommonFields `bson:"obj_info"`
@ -21,4 +27,25 @@ type Action struct {
type Event struct {
VendorName string `bson:"vendor_name,omitempty"`
Key string `bson:"key,omitempty"`
Fields map[string]string `bson:"fields,omitempty"`
}
func (am *ActionMapping) MongoId() primitive.ObjectID {
if am.Id.IsZero() {
now := time.Now()
am.Id = primitive.NewObjectIDFromTimestamp(now)
}
return am.Id
}
func (am *ActionMapping) UpdateObjectInfo() {
now := time.Now()
if am.CommonFields == nil {
am.CommonFields = new(CommonFields)
am.EntityType = ACTION_MAPPING_TYPE
am.CreatedAt = now
}
am.UpdatedAt = now
}

View File

@ -1,16 +1,7 @@
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 {
@ -29,58 +20,4 @@ type OauthRefreshBody struct {
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

@ -4,6 +4,7 @@ import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
"golang.org/x/oauth2"
)
const VENDOR_ACCOUNT_TYPE = "vendor_account"
@ -13,7 +14,6 @@ const (
PCO_VENDOR_NAME = "pco"
)
type VendorAccount struct {
*CommonFields `bson:"obj_info"`
Id primitive.ObjectID `bson:"_id,omitempty"`
@ -21,6 +21,7 @@ type VendorAccount struct {
Secret string `bson:"secret,omitempty"`
OauthCredentials *OauthCredential `bson:"ouath_credentials,omitempty"`
Name string `bson:"name"`
Locked string `bson:"locked"`
}
func (va *VendorAccount) MongoId() primitive.ObjectID {
@ -42,3 +43,13 @@ func (va *VendorAccount) UpdateObjectInfo() {
va.UpdatedAt = now
}
func (va *VendorAccount) Token() *oauth2.Token {
return &oauth2.Token{
AccessToken: va.OauthCredentials.AccessToken,
TokenType: va.OauthCredentials.TokenType,
RefreshToken: va.OauthCredentials.RefreshToken,
Expiry: va.OauthCredentials.ExpiresAt,
}
}

44
ui/db/tokenSource.go Normal file
View File

@ -0,0 +1,44 @@
package db
import (
"context"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"golang.org/x/oauth2"
)
type VendorTokenSource struct {
db *DB
vendor *models.VendorAccount
}
func (db *DB) NewVendorTokenSource(vendor *models.VendorAccount) *VendorTokenSource {
return &VendorTokenSource{db: db, vendor: vendor}
}
//Not threadsafe, please wrap in a oauth2.RefreshToken
func (ts *VendorTokenSource) Token() *oauth2.Token {
conf := config.Config()
//get locking collection
col := ts.db.client.Database(conf.Mongo.LockDb).Collection(conf.Mongo.LockCol)
//try and aquire lock
opts := options.InsertOne()
res, err := col.InsertOne(context.Background(), bson.M{"token_id": ts.vendor.OauthCredentials.AccessToken},opts)
if err != nil {
//If we didn't get the lock. Wait until whoever did refreshed the token
if err == mongo.ErrInvalidIndexValue {
return ts.waitForToken()
}
//other error return nil
return nil
}
//Refresh token we have the lock
}

View File

@ -2,9 +2,6 @@ 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"
@ -34,22 +31,3 @@ 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

@ -5,4 +5,5 @@ services:
ports:
- '27017:27017'
volumes:
- ./tmp/mongo:/data/db
- /etc/capstone/mongo:/data/db

BIN
ui/frontend-service Executable file

Binary file not shown.

View File

@ -4,12 +4,14 @@ go 1.19
require (
github.com/a-h/templ v0.2.408
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/google/uuid v1.1.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.17.0
go.mongodb.org/mongo-driver v1.12.1
golang.org/x/crypto v0.13.0
golang.org/x/crypto v0.15.0
golang.org/x/oauth2 v0.12.0
)
require (
@ -17,13 +19,13 @@ require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/cors v1.4.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.0 // indirect
@ -38,7 +40,6 @@ require (
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
@ -54,11 +55,14 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace golang.org/x/oauth2 => ../libs/oauth2

1098
ui/go.sum

File diff suppressed because it is too large Load Diff

1009
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

8
ui/package.json Normal file
View File

@ -0,0 +1,8 @@
{
"dependencies": {
"@material-tailwind/html": "^2.0.0"
},
"devDependencies": {
"tailwindcss": "^3.3.2"
}
}

View File

@ -1,5 +1,10 @@
module.exports = {
content: ["./templates/*.html"],
theme: { extend: {}, },
/** @type {import('tailwindcss').Config} */
const withMT = require("@material-tailwind/html/utils/withMT");
module.exports = withMT({
content: ["./templates/*.templ"],
plugins: [],
}
theme: {
extend: {},
}
})

View File

@ -99,7 +99,7 @@ templ DashboardNav(user *models.User) {
<input
type="text"
placeholder="Search"
class="border-0 px-3 py-2 h-12 border border-solid border-blueGray-500 placeholder-blueGray-300 text-blueGray-600 bg-white rounded text-base leading-snug shadow-none outline-none focus:outline-none w-full font-normal"
class="px-3 py-2 h-12 border border-solid border-blueGray-500 placeholder-blueGray-300 text-blueGray-600 bg-white rounded text-base leading-snug shadow-none outline-none focus:outline-none w-full font-normal"
/>
</div>
</form>
@ -155,12 +155,12 @@ templ DashboardContentNav(user *models.User) {
>
<div class="relative flex w-full flex-wrap items-stretch">
<span
class="z-10 h-full leading-snug font-normal absolute text-center text-blueGray-300 absolute bg-transparent rounded text-base items-center justify-center w-8 pl-3 py-3"
class="z-10 h-full leading-snug font-normal text-center text-blueGray-300 absolute bg-transparent rounded text-base items-center justify-center w-8 pl-3 py-3"
><i class="fas fa-search"></i></span>
<input
type="text"
placeholder="Search here..."
class="border-0 px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm shadow outline-none focus:outline-none focus:ring w-full pl-10"
class="border-0 px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white rounded text-sm shadow outline-none focus:outline-none focus:ring w-full pl-10"
/>
</div>
</form>
@ -322,13 +322,13 @@ templ DashboardVendorWidget(vendors []models.VendorAccount) {
templ DashboardActionModalForm(vendors []models.VendorAccount) {
<div class="relative p-6 flex-auto">
<form class="space-y-4 text-gray-700">
<form class="space-y-4 text-gray-700" action="/dashboard/action/add" method="POST">
<div class="flex flex-wrap">
<div class="w-full">
<div class="relative inline-block w-full text-gray-700">
<select class="w-full h-10 pl-3 pr-6 text-base placeholder-gray-600 border rounded-lg appearance-none focus:shadow-outline" placeholder="Choose action source">
<select class="w-full h-10 pl-3 pr-6 text-base placeholder-gray-600 border rounded-lg appearance-none focus:shadow-outline" placeholder="Choose action source" name="source">
if hasPco(vendors) {
<option value="plan">Plan</option>
<option value="pco.plan">Plan</option>
<option value="calendar" disabled>Calendar</option>
} else {
<option value="nil">None Available</option>
@ -343,10 +343,10 @@ templ DashboardActionModalForm(vendors []models.VendorAccount) {
<div class="flex flex-wrap -mx-2 space-y-4 md:space-y-0">
<div class="w-full">
<div class="relative inline-block w-full text-gray-700">
<select class="w-full h-10 pl-3 pr-6 text-base placeholder-gray-600 border rounded-lg appearance-none focus:shadow-outline" placeholder="Choose action source">
<select class="w-full h-10 pl-3 pr-6 text-base placeholder-gray-600 border rounded-lg appearance-none focus:shadow-outline" placeholder="Choose action source" name="action">
if hasYoutube(vendors) {
<option value="plan">Livestream</option>
<option value="calendar" disabled>Video</option>
<option value="youtube.livestream">Livestream</option>
<option value="video" disabled>Video</option>
} else {
<option value="nil">None Available</option>
}
@ -357,6 +357,14 @@ templ DashboardActionModalForm(vendors []models.VendorAccount) {
</div>
</div>
</div>
<div class="flex items-center justify-end p-6 border-t border-solid border-blueGray-200 rounded-b">
<button class="text-gray-400 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150" type="button" onclick="toggleModal(&#39;add-action-modal&#39;)">
Close
</button>
<button class="bg-blue-700 text-white active:bg-blue-900 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150" type="submit">
Save Changes
</button>
</div>
</form>
</div>
}
@ -380,14 +388,6 @@ templ DashboardActionModal(vendors []models.VendorAccount) {
<!--body-->
@DashboardActionModalForm(vendors)
<!--footer-->
<div class="flex items-center justify-end p-6 border-t border-solid border-blueGray-200 rounded-b">
<button class="text-red-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150" type="button" onclick="toggleModal(&#39;add-action-modal&#39;)">
Close
</button>
<button class="bg-emerald-500 text-white active:bg-emerald-600 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150" type="button" onclick="toggleModal(&#39;add-action-modal&#39;)">
Save Changes
</button>
</div>
</div>
</div>
</div>

View File

@ -24,6 +24,10 @@ templ Head(msg string) {
rel="stylesheet"
href="/static/output.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/creativetimofficial/tailwind-starter-kit/compiled-tailwind.min.css"
/>
<title>{ msg } | Capstone - Pbaxt10</title>
<script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"></script>
</head>