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:
parent
96be01ea72
commit
360163f2dd
|
@ -0,0 +1 @@
|
|||
Subproject commit d87fd3250ebcb3e014780fe332c13067aa665174
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
tmp/
|
|
@ -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
|
||||
|
@ -7,4 +9,4 @@ build:
|
|||
|
||||
deploy: build
|
||||
docker push $(BASE_URL)/webhook-service:latest
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
1593
service/go.sum
1593
service/go.sum
File diff suppressed because it is too large
Load Diff
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package services
|
||||
|
||||
type AttachmentType struct {
|
||||
Id string `jsonapi:"primary,AttachmentType"`
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package services
|
||||
|
||||
type LinkedPublishingEpisode struct {
|
||||
Id string `jsonapi:"primary,LinkedPublishingEpisode"`
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package services
|
||||
|
||||
type Organization struct {
|
||||
Id string `jsonapi:"primary,Organization"`
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package services
|
||||
|
||||
type Person struct {
|
||||
Id string `jsonapi:"primary,Person"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package services
|
||||
|
||||
|
||||
type Series struct {
|
||||
Id string `jsonapi:"primary,Series"`
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package services
|
||||
|
||||
|
||||
type ServiceType struct {
|
||||
Id string `jsonapi:"primary,ServiceType"`
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package services
|
||||
|
||||
type Team struct {
|
||||
Id string `jsonapi:"primary,Team"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -3,3 +3,7 @@ tmp/*
|
|||
**/*_templ.go
|
||||
templates/*.html
|
||||
docker/tmp/*
|
||||
node_modules/*
|
||||
node_modules
|
||||
config.yaml
|
||||
conf.yaml
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const VENDOR_ACCOUNT_TYPE = "vendor_account"
|
||||
|
@ -13,14 +14,14 @@ const (
|
|||
PCO_VENDOR_NAME = "pco"
|
||||
)
|
||||
|
||||
|
||||
type VendorAccount struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
Id primitive.ObjectID `bson:"_id,omitempty"`
|
||||
Id primitive.ObjectID `bson:"_id,omitempty"`
|
||||
UserId primitive.ObjectID `bson:"user_id,omitempty"`
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -5,4 +5,5 @@ services:
|
|||
ports:
|
||||
- '27017:27017'
|
||||
volumes:
|
||||
- ./tmp/mongo:/data/db
|
||||
- /etc/capstone/mongo:/data/db
|
||||
|
||||
|
|
Binary file not shown.
20
ui/go.mod
20
ui/go.mod
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@material-tailwind/html": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.3.2"
|
||||
}
|
||||
}
|
|
@ -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: {},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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('add-action-modal')">
|
||||
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('add-action-modal')">
|
||||
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('add-action-modal')">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue