Compare commits
93 Commits
ba843f89eb
...
98bebfaafe
Author | SHA1 | Date |
---|---|---|
Preston Baxter | 98bebfaafe | |
Preston Baxter | c1de3cd2b9 | |
Preston Baxter | 0e39465d81 | |
Preston Baxter | d2b9436c04 | |
Preston Baxter | 916498b474 | |
Preston Baxter | f0bdb07248 | |
Preston Baxter | a2fde64459 | |
Preston Baxter | 8da136dc34 | |
Preston Baxter | 1ba7327742 | |
Preston Baxter | d632f714d0 | |
Preston Baxter | cf1c32208e | |
Preston Baxter | 16a236aba1 | |
Preston Baxter | 9c70466f30 | |
Preston Baxter | 9ad4170b82 | |
Preston Baxter | 5f967bcde1 | |
Preston Baxter | 523994ed26 | |
Preston Baxter | 515bfd5ae5 | |
Preston Baxter | b5967c8e8c | |
Preston Baxter | 2759c78061 | |
Preston Baxter | a33c58d0d8 | |
Preston Baxter | b77f45ccf6 | |
Preston Baxter | a1eecdd7f8 | |
Preston Baxter | f430e33a28 | |
Preston Baxter | e0f0bb3b5f | |
Preston Baxter | fdc2b3ab1b | |
Preston Baxter | c6ae07ccf7 | |
Preston Baxter | 374826b577 | |
Preston Baxter | 563b935fe7 | |
Preston Baxter | 746886fcd9 | |
Preston Baxter | eadfc6b56e | |
Preston Baxter | 8b8320eeb1 | |
Preston Baxter | 36b39e78c8 | |
Preston Baxter | 8601297bb4 | |
Preston Baxter | 6f3ec2375b | |
Preston Baxter | 4c5f29c0b9 | |
Preston Baxter | 46f9460a37 | |
Preston Baxter | 920e203b9d | |
Preston Baxter | d78ca20541 | |
Preston Baxter | ebd193ab38 | |
Preston Baxter | 3049da7bcd | |
Preston Baxter | 9bc2e8d758 | |
Preston Baxter | 3e4257cba3 | |
Preston Baxter | 113a0e9287 | |
Preston Baxter | 7adc41a260 | |
Preston Baxter | 2f3c293aad | |
Preston Baxter | 6f308e5e23 | |
Preston Baxter | 6f1ce7dcc1 | |
Preston Baxter | f703f2d1ab | |
Preston Baxter | 1ecc00a86e | |
Preston Baxter | 5ea803b67f | |
Preston Baxter | 9c9c419055 | |
Preston Baxter | 8e58dfafa5 | |
Preston Baxter | f678a738b4 | |
Preston Baxter | 61aacac37c | |
Preston Baxter | abac8d7822 | |
Preston Baxter | 4079ff16a6 | |
Preston Baxter | f20f622335 | |
Preston Baxter | d3bd82c4f0 | |
Preston Baxter | aee35ac9d4 | |
Preston Baxter | 77b000f9a3 | |
Preston Baxter | 712791d297 | |
Preston Baxter | 360163f2dd | |
Preston Baxter | 96be01ea72 | |
Preston Baxter | 7ab96371c4 | |
Preston Baxter | dff5fd149b | |
Preston Baxter | 175b6cae8c | |
Preston Baxter | 65167c3e58 | |
Preston Baxter | 5c7a72cf0f | |
Preston Baxter | d36eb955ab | |
Preston Baxter | 4866071689 | |
Preston Baxter | 2c1d2d2520 | |
Preston Baxter | f58298d125 | |
Preston Baxter | 7ecfae9ee6 | |
Preston Baxter | cacf039ac4 | |
Preston Baxter | ca70d241d6 | |
Preston Baxter | b1a573b840 | |
Preston Baxter | 4317b6b8c9 | |
Preston Baxter | aa0dd18e22 | |
Preston Baxter | 791a00c695 | |
Preston Baxter | a9d1d62df7 | |
Preston Baxter | f860da87ce | |
Preston Baxter | d5f3f5e783 | |
Preston Baxter | 344c17fb27 | |
Preston Baxter | 1bfcdce01a | |
Preston Baxter | f7dc37fb02 | |
Preston Baxter | ce0a301449 | |
Preston Baxter | db799805f9 | |
Preston Baxter | 43d111cb0e | |
Preston Baxter | ce2644a1ac | |
Preston Baxter | 19a0334a92 | |
Preston Baxter | c20929b06f | |
Preston Baxter | 010acc6d85 | |
Preston Baxter | 8e45bd11bb |
|
@ -0,0 +1,39 @@
|
|||
infra/main.*.tfvars
|
||||
|
||||
# Canned terraform ignores#
|
||||
infra/Local .terraform directories
|
||||
**/.terraform/*
|
||||
|
||||
# .tfstate files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
|
||||
# Crash log files
|
||||
infra/crash.log
|
||||
infra/crash.*.log
|
||||
|
||||
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
|
||||
# password, private keys, and other secrets. These should not be part of version
|
||||
# control as they are data points which are potentially sensitive and subject
|
||||
# to change depending on the environment.
|
||||
infra/*.tfvars
|
||||
infra/*.tfvars.json
|
||||
|
||||
# Ignore override files as they are usually used to override resources locally and so
|
||||
# are not checked in
|
||||
infra/override.tf
|
||||
infra/override.tf.json
|
||||
infra/*_override.tf
|
||||
infra/*_override.tf.json
|
||||
|
||||
# Include override files you do wish to add to version control using negated pattern
|
||||
# !example_override.tf
|
||||
|
||||
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
|
||||
# example: *tfplan*
|
||||
|
||||
# Ignore CLI configuration files
|
||||
infra/.terraformrc
|
||||
infra/terraform.rc
|
||||
|
||||
secrets/*
|
|
@ -0,0 +1,6 @@
|
|||
[submodule "libs/oauth2"]
|
||||
path = libs/oauth2
|
||||
url = https://git.preston-baxter.com/Preston_PLB/oauth2.git
|
||||
[submodule "libs/jsonapi"]
|
||||
path = libs/jsonapi
|
||||
url = https://git.preston-baxter.com/Preston_PLB/jsonapi.git
|
|
@ -0,0 +1,40 @@
|
|||
BASE_URL=us-central1-docker.pkg.dev/pbaxter-infra/capstone-repo
|
||||
FRONTEND_VERSION=$(shell jq -rc ".frontend_version" versions.json)
|
||||
WEBHOOK_VERSION=$(shell jq -rc ".webhook_version" versions.json)
|
||||
|
||||
deploy: deploy-ui deploy-service deploy-tf
|
||||
|
||||
deploy-tf:
|
||||
cd infra; make deploy-yes
|
||||
|
||||
deploy-ui: build-ui
|
||||
docker push $(BASE_URL)/frontend-service:latest
|
||||
docker push $(BASE_URL)/frontend-service:$(FRONTEND_VERSION)
|
||||
|
||||
deploy-service: build-service
|
||||
docker push $(BASE_URL)/webhook-service:latest
|
||||
docker push $(BASE_URL)/webhook-service:$(WEBHOOK_VERSION)
|
||||
|
||||
build: build-ui build-service
|
||||
|
||||
build-ui:
|
||||
docker build -f ./docker/ui.dockerfile . -t frontend-service:latest
|
||||
docker build -f ./docker/ui.dockerfile . -t frontend-service:$(FRONTEND_VERSION)
|
||||
docker tag frontend-service:latest $(BASE_URL)/frontend-service:latest
|
||||
docker tag frontend-service:$(FRONTEND_VERSION) $(BASE_URL)/frontend-service:$(FRONTEND_VERSION)
|
||||
|
||||
build-service:
|
||||
docker build -f ./docker/service.dockerfile . -t webhook-service:latest
|
||||
docker build -f ./docker/service.dockerfile . -t webhook-service:$(WEBHOOK_VERSION)
|
||||
docker tag webhook-service:latest $(BASE_URL)/webhook-service:latest
|
||||
docker tag webhook-service:$(WEBHOOK_VERSION) $(BASE_URL)/webhook-service:$(WEBHOOK_VERSION)
|
||||
|
||||
image: SHELL := /bin/bash
|
||||
image:
|
||||
[[ -d "/tmp/capstone" ]] || mkdir /tmp/capstone
|
||||
cp -R infra/ /tmp/capstone/
|
||||
cp -R service/ /tmp/capstone/
|
||||
cp -R ui/ /tmp/capstone/
|
||||
rm -rf /tmp/capstone/ui/templates/*_templ.go
|
||||
codevis -i /tmp/capstone --whitelist-extension go,hcl,tf,templ -o ./out.png
|
||||
rm -rf /tmp/capstone/*
|
139
README.md
|
@ -5,3 +5,142 @@
|
|||
1. To meet and fufil the criteria of the WGU Software Engineering Capstone performance assessment.
|
||||
1. To demonstrate my skills in designing and implementing complex systems
|
||||
1. Build a service that fixes a problem my peers and I have
|
||||
|
||||
## Code Base
|
||||
|
||||
I like to keep track of what the codebase looks like
|
||||
|
||||
![picture](code.png)
|
||||
|
||||
# Requirements
|
||||
|
||||
Make sure you have isntalled:
|
||||
- make
|
||||
- jq
|
||||
- GNU sed (If on macos `bew install gnu-sed` and change references to sed -> gsed)
|
||||
- docker
|
||||
- [OpenTofu](https://opentofu.org/) _open source terraform_
|
||||
- go
|
||||
|
||||
Optional
|
||||
- [codevis](https://github.com/sloganking/codevis) - _make the pretty picture_
|
||||
- [air](https://github.com/cosmtrek/air) - hot reload
|
||||
|
||||
# How to run
|
||||
|
||||
## Infrastructure
|
||||
|
||||
infrastructure is deployed via terraform. It also makes some assumptions about the GCPenvironment its being deployed into.
|
||||
Those main assumptions that it has the preston-baxter.com hosted zone. You may need to change this to make this terraform template work for You
|
||||
|
||||
### Terraform Variables
|
||||
|
||||
Contents of `terraform.tfvars`
|
||||
|
||||
```toml
|
||||
project_id = "pbaxter-infra"
|
||||
project_region = "us-central1"
|
||||
webhook_service_tag = "0.2.1"
|
||||
frontend_service_tag = "0.2.1"
|
||||
```
|
||||
|
||||
You only need to specify the `project_id` and `project_region`
|
||||
|
||||
Both of the serice tags will be updated automatically via `make deploy`
|
||||
|
||||
### Run Terraform
|
||||
|
||||
You can either cd into the `infra/` direcrotry and run `make deploy`
|
||||
or
|
||||
You can run `make deploy-tf` from the root directory
|
||||
|
||||
## Services
|
||||
|
||||
The webook service is located in the `service/` directory
|
||||
The frontend service is located in the `ui/` directory
|
||||
|
||||
Both services get ran the same way. What works will one will work for the other
|
||||
|
||||
### Config
|
||||
|
||||
Sample config
|
||||
``` yaml
|
||||
jwt_secret: some_random_string_that_is_long_but_not_too_long
|
||||
env: test
|
||||
mongo:
|
||||
uri: "mongodb://localhost:27017"
|
||||
ent_db: capstoneDB
|
||||
ent_col: entities
|
||||
lock_db: capstoneDB
|
||||
lock_col: locks
|
||||
app_settings:
|
||||
webhook_service_url: localhost:8080
|
||||
frontend_service_url: localhost:8080
|
||||
vendors:
|
||||
pco:
|
||||
client_id: "test_client_id"
|
||||
client_secret: "test_secret"
|
||||
scopes:
|
||||
- 'people'
|
||||
- 'calendar'
|
||||
- 'services'
|
||||
auth_uri: "https://api.planningcenteronline.com/oauth/authorize"
|
||||
token_uri: "https://api.planningcenteronline.com/oauth/token"
|
||||
refresh_encode: json
|
||||
youtube:
|
||||
client_id: "test_client_id"
|
||||
client_secret: "test_secret"
|
||||
scopes:
|
||||
- "https://www.googleapis.com/auth/youtube"
|
||||
- "https://www.googleapis.com/auth/youtube.force-ssl"
|
||||
- "https://www.googleapis.com/auth/youtube.download"
|
||||
- "https://www.googleapis.com/auth/youtube.upload"
|
||||
auth_uri: "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
token_uri: "https://oauth2.googleapis.com/token"
|
||||
refresh_encode: url
|
||||
test:
|
||||
client_id: "client_id"
|
||||
client_secret: "client_secret"
|
||||
scopes:
|
||||
- "scope 1"
|
||||
- "scope 2"
|
||||
auth_uri: "server/auth"
|
||||
token_uri: "server/token"
|
||||
refresh_encode: url
|
||||
```
|
||||
|
||||
Config is expected at `/etc/capstone/config.yaml`
|
||||
|
||||
### Run service locally
|
||||
|
||||
Both services are configured with [air](https://github.com/cosmtrek/air). Air is a hot reload tool that speeds up the development process. It is particularly useful for working on the frontend service
|
||||
|
||||
To run locally, cd into the service directory and run `air`
|
||||
|
||||
### Make Docker Container
|
||||
|
||||
from the root directory
|
||||
|
||||
```bash
|
||||
make build-service
|
||||
make build-ui
|
||||
```
|
||||
|
||||
### Run Docker Container Locally
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 -it webhook-service:latest
|
||||
docker run -p 8080:8080 -it frontend-service:latest
|
||||
```
|
||||
|
||||
# Deploy
|
||||
|
||||
Make suer versions are updated in `versions.json`
|
||||
|
||||
```bash
|
||||
make deploy
|
||||
```
|
||||
|
||||
Its that easy
|
||||
|
||||
NOTE: You may be asked to approve a change set.
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
nameserver 8.8.8.8
|
||||
nameserver 8.8.4.4
|
||||
nameserver 1.1.1.1
|
|
@ -0,0 +1,24 @@
|
|||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
WORKDIR /build/service
|
||||
|
||||
COPY service/go.mod service/go.sum ./
|
||||
COPY ./ui ../ui
|
||||
COPY ./libs ../libs
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY ./service .
|
||||
|
||||
RUN GOEXPERIMENT=loopvar go build -o main
|
||||
|
||||
FROM amazonlinux:2023
|
||||
|
||||
COPY docker/resolv.conf /etc/resolv.conf
|
||||
COPY --from=builder /build/service/main /bin/main
|
||||
RUN mkdir -p /etc/capstone
|
||||
COPY secrets/config.yaml /etc/capstone
|
||||
|
||||
EXPOSE "8080"
|
||||
|
||||
ENTRYPOINT ["/bin/main"]
|
|
@ -0,0 +1,43 @@
|
|||
#Build Go stuff
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||
|
||||
WORKDIR /build/ui
|
||||
|
||||
#Setup libs
|
||||
COPY ui/go.mod ui/go.sum ./
|
||||
COPY ./service ../service
|
||||
COPY ./libs ../libs
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY ui/ .
|
||||
|
||||
RUN rm **/*_templ.go; templ generate -path ./templates
|
||||
RUN GOEXPERIMENT=loopvar go build -o main
|
||||
|
||||
#Build NPM stuff
|
||||
FROM node:18-alpine AS node-builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY ui/ .
|
||||
|
||||
RUN npm install
|
||||
RUN npx tailwindcss -i tailwind/index.css -o dist/output.css
|
||||
|
||||
#Final Contianer
|
||||
FROM amazonlinux:2023
|
||||
|
||||
COPY docker/resolv.conf /etc/resolv.conf
|
||||
RUN mkdir -p /var/capstone
|
||||
COPY ui/static/ /var/capstone/dist
|
||||
COPY --from=node-builder /build/dist /var/capstone/dist
|
||||
COPY --from=builder /build/ui/main /bin/main
|
||||
RUN mkdir -p /etc/capstone
|
||||
COPY secrets/config.yaml /etc/capstone
|
||||
|
||||
EXPOSE "8080"
|
||||
|
||||
ENTRYPOINT ["/bin/main"]
|
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 1.2 MiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 68 KiB |
|
@ -0,0 +1,44 @@
|
|||
# How to Use
|
||||
|
||||
This is your one stop shop for how to use my capstone project
|
||||
|
||||
# Logging In
|
||||
|
||||
to login click the login button in the top right of the homepage
|
||||
|
||||
![picture](./img/login_home.png)
|
||||
|
||||
or navigate to:
|
||||
|
||||
[https://capstone.preston-baxter.com/login](https://capstone.preston-baxter.com/login)
|
||||
|
||||
# Signing Up
|
||||
If you don't have an account you can sign up by clicking `create an account`
|
||||
|
||||
![picture](./img/signup.png)
|
||||
|
||||
or at [https://capstone.preston-baxter.com/login](https://capstone.preston-baxter.com/login)
|
||||
|
||||
email must be in a valid format
|
||||
|
||||
# Adding Vendors
|
||||
|
||||
Once logged in on the dashboard page you can select the '+' button and the vendor account you would like to add.
|
||||
|
||||
![picture](./img/vendor_add.png)
|
||||
|
||||
You will then be redirected to that vendors login page to ask permission to act on your behalf.
|
||||
|
||||
# Creating Actions
|
||||
|
||||
Once you have some vendors configured you can then add an action. Currently the only action supported is scheduling a live stream off of a plan.
|
||||
|
||||
![picture](./img/action_add.png)
|
||||
|
||||
In the actions table, select the 'Add Action' button and follow the prompts.
|
||||
|
||||
# Auditing
|
||||
|
||||
Once you have an action setup you can view how the system is interacting with events and making descision in the events tab
|
||||
|
||||
![picture](./img/events.png)
|
|
@ -0,0 +1,20 @@
|
|||
# This file is maintained automatically by "tofu init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/hashicorp/google" {
|
||||
version = "5.1.0"
|
||||
constraints = ">= 3.3.0"
|
||||
hashes = [
|
||||
"h1:Mc09uV4QZR+1y2u4W/EO6HRmkiSCUoalXe6/tFfUWXA=",
|
||||
"zh:08333cb1aedbc65137b40dbd3ef3aa9b0d5b4ef9acce156d11cc7b1f07f2804c",
|
||||
"zh:0fb8b9a482e9d8f23ae6caadc1fd8811b15802be3c93b5ecbbd6efd49c62a8f2",
|
||||
"zh:2d5f1dd805871c0490ac4dea783dbe85cd40f0f4fdbe9b2374a42b0a37a6f020",
|
||||
"zh:6899593b6ae15bd8d8cc568d9664818c119d42da6c158c5869a94b8846cef733",
|
||||
"zh:75dbb0cea493220e8c0e0c704d7ac2570121f2af582dfd205cd0f4749f8dcc79",
|
||||
"zh:862b885016962ddce51f916db9a7d759f7bd136b4c29388187acd16298875eb7",
|
||||
"zh:8f3804aaa5b8c1bbdaa6b43a02cc6f8d6f91595b8bf4eb588f9df1ea4fb00009",
|
||||
"zh:a825327149dd2e85fc7cc22cc5466101f710d63f6320b2ccbd64362afabfff1b",
|
||||
"zh:a926a757aa8f3b5b1147fcc3ec75cd55fb0d97b2165a83d075cded76f59e8dd9",
|
||||
"zh:cd4d6bb663cac37633ae20d719ccd8ecc8f4cd983058c322404072ba9fabe232",
|
||||
]
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
FRONTEND_VERSION=$(shell jq -rc ".frontend_version" ../versions.json)
|
||||
WEBHOOK_VERSION=$(shell jq -rc ".webhook_version" ../versions.json)
|
||||
|
||||
replace: SHELL := /bin/bash
|
||||
replace:
|
||||
sed -i -Ee "s/(webhook_service_tag = \").*(\")/\1$(WEBHOOK_VERSION)\2/g" terraform.tfvars
|
||||
sed -i -Ee "s/(frontend_service_tag = \").*(\")/\1$(FRONTEND_VERSION)\2/g" terraform.tfvars
|
||||
|
||||
deploy: SHELL := /bin/bash
|
||||
deploy: replace
|
||||
tofu apply
|
||||
|
||||
deploy-yes: SHELL := /bin/bash
|
||||
deploy-yes: replace
|
||||
tofu apply -auto-approve
|
|
@ -0,0 +1,160 @@
|
|||
terraform {
|
||||
required_version = ">= 0.14"
|
||||
|
||||
required_providers {
|
||||
google = ">= 3.3"
|
||||
}
|
||||
}
|
||||
|
||||
variable "project_id" {
|
||||
description = "The GCP project ID where the infra will be built"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "project_region" {
|
||||
description = "The GCP region where the infra will be built"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "webhook_service_tag" {
|
||||
description = "Tag for the webhook service collector image"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "frontend_service_tag" {
|
||||
description = "Tag for the frontend service collector image"
|
||||
type = string
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
}
|
||||
|
||||
resource "google_project_service" "run_api" {
|
||||
service = "run.googleapis.com"
|
||||
|
||||
disable_on_destroy = true
|
||||
}
|
||||
|
||||
resource "google_project_service" "artifact_api" {
|
||||
service = "artifactregistry.googleapis.com"
|
||||
|
||||
disable_on_destroy = true
|
||||
}
|
||||
|
||||
resource "google_project_service" "serverless_vpc_api" {
|
||||
service = "vpcaccess.googleapis.com"
|
||||
|
||||
disable_on_destroy = true
|
||||
}
|
||||
|
||||
resource "google_artifact_registry_repository" "capstone_repo" {
|
||||
location = var.project_region
|
||||
repository_id = "capstone-repo"
|
||||
description = "Images for capstone project"
|
||||
format = "DOCKER"
|
||||
|
||||
docker_config {
|
||||
immutable_tags = false
|
||||
}
|
||||
|
||||
depends_on = [ google_project_service.artifact_api ]
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service" "webhook_service_cr" {
|
||||
name = "webhook-service-cr"
|
||||
location = var.project_region
|
||||
|
||||
template {
|
||||
containers {
|
||||
image = "${var.project_region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.capstone_repo.name}/webhook-service:${var.webhook_service_tag}"
|
||||
}
|
||||
}
|
||||
depends_on = [ google_project_service.run_api, google_artifact_registry_repository.capstone_repo ]
|
||||
}
|
||||
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "webhook_service_run_all_users" {
|
||||
project = var.project_id
|
||||
name = google_cloud_run_v2_service.webhook_service_cr.name
|
||||
location = var.project_region
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service" "frontend_service_cr" {
|
||||
name = "frontend-service-cr"
|
||||
location = var.project_region
|
||||
|
||||
template {
|
||||
containers {
|
||||
image = "${var.project_region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.capstone_repo.name}/frontend-service:${var.frontend_service_tag}"
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [ google_project_service.run_api, google_artifact_registry_repository.capstone_repo ]
|
||||
}
|
||||
|
||||
|
||||
resource "google_cloud_run_v2_service_iam_member" "frontend_service_run_all_users" {
|
||||
project = var.project_id
|
||||
name = google_cloud_run_v2_service.frontend_service_cr.name
|
||||
location = var.project_region
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
data "google_dns_managed_zone" "preston_baxter_zone" {
|
||||
name = "pbaxter-main-zone"
|
||||
}
|
||||
|
||||
resource "google_dns_record_set" "webhook_cname" {
|
||||
name = "capstone-webhook.${data.google_dns_managed_zone.preston_baxter_zone.dns_name}"
|
||||
managed_zone = data.google_dns_managed_zone.preston_baxter_zone.name
|
||||
type = "CNAME"
|
||||
ttl = 300
|
||||
rrdatas = [
|
||||
"ghs.googlehosted.com."
|
||||
]
|
||||
|
||||
depends_on = [ google_cloud_run_v2_service.webhook_service_cr ]
|
||||
}
|
||||
|
||||
resource "google_dns_record_set" "frontend_cname" {
|
||||
name = "capstone.${data.google_dns_managed_zone.preston_baxter_zone.dns_name}"
|
||||
managed_zone = data.google_dns_managed_zone.preston_baxter_zone.name
|
||||
type = "CNAME"
|
||||
ttl = 300
|
||||
rrdatas = [
|
||||
"ghs.googlehosted.com."
|
||||
]
|
||||
|
||||
depends_on = [ google_cloud_run_v2_service.frontend_service_cr ]
|
||||
}
|
||||
|
||||
resource "google_cloud_run_domain_mapping" "frontend_cname_mapping" {
|
||||
location = "us-central1"
|
||||
name = trimsuffix("capstone.${data.google_dns_managed_zone.preston_baxter_zone.dns_name}", ".")
|
||||
|
||||
metadata {
|
||||
namespace = var.project_id
|
||||
}
|
||||
|
||||
spec {
|
||||
route_name = google_cloud_run_v2_service.frontend_service_cr.name
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_cloud_run_domain_mapping" "webhook_cname_mapping" {
|
||||
location = "us-central1"
|
||||
name = trimsuffix("capstone-webhook.${data.google_dns_managed_zone.preston_baxter_zone.dns_name}", ".")
|
||||
|
||||
metadata {
|
||||
namespace = var.project_id
|
||||
}
|
||||
|
||||
|
||||
spec {
|
||||
route_name = google_cloud_run_v2_service.webhook_service_cr.name
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 403bd2e40b5f46dd52413a0f298557b1bd7499e6
|
|
@ -0,0 +1 @@
|
|||
Subproject commit f088706217d0f51a8ed30f73791e3b3aebce5fda
|
|
@ -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/
|
|
@ -0,0 +1,12 @@
|
|||
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
|
||||
docker tag webhook-service:latest $(BASE_URL)/webhook-service:latest
|
||||
|
||||
deploy: build
|
||||
docker push $(BASE_URL)/webhook-service:latest
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"google.golang.org/api/youtube/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
log *logrus.Logger
|
||||
mongo *db.DB
|
||||
ytClientMap map[primitive.ObjectID]*youtube.Service
|
||||
pcoClientMap map[primitive.ObjectID]*pco.PcoApiClient
|
||||
)
|
||||
|
||||
func BuildRouter(r *gin.Engine) {
|
||||
conf := config.Config()
|
||||
|
||||
log = logrus.New()
|
||||
log.SetFormatter(&logrus.TextFormatter{
|
||||
DisableColors: true,
|
||||
})
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
|
||||
var err error
|
||||
mongo, err = db.NewClient(conf.Mongo.Uri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ytClientMap = make(map[primitive.ObjectID]*youtube.Service)
|
||||
pcoClientMap = make(map[primitive.ObjectID]*pco.PcoApiClient)
|
||||
|
||||
pco := r.Group("/pco")
|
||||
pco.POST("/:userid", ValidatePcoWebhook, ConsumePcoWebhook)
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/jsonapi"
|
||||
)
|
||||
|
||||
const PCO_VALIDATE_HEADER = "X-PCO-Webhooks-Authenticity"
|
||||
|
||||
func ValidatePcoWebhook(c *gin.Context) {
|
||||
//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))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to decode byte digest")
|
||||
_ = c.AbortWithError(501, err)
|
||||
return
|
||||
}
|
||||
|
||||
//clone request body to harmlessly inspect the body
|
||||
bodyCopy := bytes.NewBuffer([]byte{})
|
||||
_, err = io.Copy(bodyCopy, c.Request.Body)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to copy body while validating PCO webhook")
|
||||
_ = c.AbortWithError(501, err)
|
||||
return
|
||||
}
|
||||
body := bodyCopy.Bytes()
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
//Get secret
|
||||
key, err := getAuthSecret(c, body)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to find auth secret for event. It may not be setup")
|
||||
_ = c.AbortWithError(501, err)
|
||||
return
|
||||
}
|
||||
|
||||
//Get HMAC
|
||||
hmacSig := hmac.New(sha256.New, []byte(key))
|
||||
hmacSig.Write(body)
|
||||
|
||||
if !hmac.Equal(hmacSig.Sum(nil), pcoSig) {
|
||||
log.Warn("")
|
||||
c.AbortWithStatus(401)
|
||||
}
|
||||
}
|
||||
|
||||
func getAuthSecret(c *gin.Context, body []byte) (string, error) {
|
||||
userObjectId := userIdFromContext(c)
|
||||
log.Debug(string(body))
|
||||
|
||||
//Pco is weird and sends a data array instead of an object. Yet there is only one event. Fun times
|
||||
event, err := jsonapi.UnmarshalManyPayload[webhooks.EventDelivery](bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return "", errors.Join(fmt.Errorf("Failed to unmarshall event delivery from PCO"), err)
|
||||
}
|
||||
|
||||
if len(event) == 0 {
|
||||
return "", fmt.Errorf("There are no events in the delivery. Something is wrong")
|
||||
}
|
||||
|
||||
webhook, err := mongo.FindPcoSubscriptionForUser(*userObjectId, event[0].Name)
|
||||
if err != nil {
|
||||
return "", errors.Join(fmt.Errorf("Failed to find pco subscription for user: %s and event: %s", userObjectId.Hex(), event[0].Name), err)
|
||||
}
|
||||
|
||||
if webhook == nil {
|
||||
return "", fmt.Errorf("Could not find subscription for user: %s and name %s", userObjectId.Hex(), event[0].Name)
|
||||
}
|
||||
|
||||
return webhook.Details.AuthenticitySecret, nil
|
||||
}
|
|
@ -0,0 +1,421 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/services"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
|
||||
yt_helpers "git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/youtube"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/jsonapi"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/youtube/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
eventRegexKeys = map[string]string{"plan": `^services\.v2\.events\.plan\..*`}
|
||||
actionFuncMap = map[string]actionFunc{"youtube.livestream": ScheduleBroadcastFromWebhook}
|
||||
|
||||
//Error definintions
|
||||
errorOkMap = map[error]bool{NotSchedulableTime: true, UnknownEventErr: true, AlreadyScheduledBroadcast: true, NoBroadcastToDelete: true}
|
||||
NotSchedulableTime = errors.New("This time is not schedulable")
|
||||
UnknownEventErr = errors.New("Event sent is unkown")
|
||||
AlreadyScheduledBroadcast = errors.New("This broadcast has already been scheduled")
|
||||
NoBroadcastToDelete = errors.New("No Broadcasts to destroy")
|
||||
)
|
||||
|
||||
const (
|
||||
CREATED_BROADCAST = "Created Broadcast"
|
||||
UPDATED_BROADCAST = "Updated Broadcast"
|
||||
DELETED_BROADCAST = "Deleted Broadcast"
|
||||
)
|
||||
|
||||
type actionFunc func(*gin.Context, *webhooks.EventDelivery) error
|
||||
|
||||
func userIdFromContext(c *gin.Context) *primitive.ObjectID {
|
||||
if id, ok := c.Get("user_bson_id"); !ok {
|
||||
userId := c.Param("userid")
|
||||
|
||||
if userId == "" {
|
||||
log.Warn("Webhook did not contain user id. Rejecting")
|
||||
c.AbortWithStatus(404)
|
||||
return nil
|
||||
}
|
||||
|
||||
userObjectId, err := primitive.ObjectIDFromHex(userId)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("User Id was malformed")
|
||||
c.AbortWithStatus(400)
|
||||
return nil
|
||||
}
|
||||
c.Set("user_bson_id", userObjectId)
|
||||
return &userObjectId
|
||||
} else {
|
||||
if objId, ok := id.(primitive.ObjectID); ok {
|
||||
return &objId
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ConsumePcoWebhook(c *gin.Context) {
|
||||
userObjectId := userIdFromContext(c)
|
||||
|
||||
//read body and handle io in parallel because IO shenanigains
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(2)
|
||||
|
||||
//get actions for user
|
||||
var actionMappings []models.ActionMapping
|
||||
var webhookBody *webhooks.EventDelivery
|
||||
errs := make([]error, 2)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
actionMappings, errs[0] = mongo.FindActionMappingsByUser(*userObjectId)
|
||||
}(wg)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
var payload []webhooks.EventDelivery
|
||||
payload, errs[1] = jsonapi.UnmarshalManyPayload[webhooks.EventDelivery](c.Request.Body)
|
||||
webhookBody = &payload[0]
|
||||
}(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(mapping.SourceEvent.Key, 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 {
|
||||
//if err is in the ok map, return 200
|
||||
if pass, ok := errorOkMap[err]; ok && pass {
|
||||
log.Warnf("Continueing after error: %s. From action: %s. From event source: %s:%s", err, actionKey, mapping.SourceEvent.VendorName, mapping.SourceEvent.Key)
|
||||
c.Status(200)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Succesfully proccessed: %s for %s", webhookBody.Name, userObjectId.Hex())
|
||||
c.Status(200)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Warnf("No errors, but also no work...")
|
||||
c.Status(200)
|
||||
}
|
||||
|
||||
func eventMatch(key, event string) bool {
|
||||
if regexString, ok := eventRegexKeys[key]; ok {
|
||||
reg := regexp.MustCompile(regexString) //TODO: Make this regex cache-able
|
||||
return reg.MatchString(event)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func pcoServiceForUser(userId primitive.ObjectID) (*pco.PcoApiClient, error) {
|
||||
//add youtube client to map if its not there
|
||||
if client, ok := pcoClientMap[userId]; !ok {
|
||||
pcoAccount, err := mongo.FindVendorAccountByUser(userId, models.PCO_VENDOR_NAME)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Build our fancy token source
|
||||
tokenSource := oauth2.ReuseTokenSource(pcoAccount.Token(), mongo.NewVendorTokenSource(pcoAccount))
|
||||
|
||||
//init service
|
||||
conf := config.Config()
|
||||
client := pco.NewClientWithOauthConfig(conf.Vendors[models.PCO_VENDOR_NAME].OauthConfig(), tokenSource)
|
||||
|
||||
//add user to map
|
||||
pcoClientMap[userId] = client
|
||||
|
||||
return client, nil
|
||||
} else {
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
func youtubeServiceForUser(userId primitive.ObjectID) (*youtube.Service, error) {
|
||||
//add youtube client to map if its not there
|
||||
if client, ok := ytClientMap[userId]; !ok {
|
||||
ytAccount, err := mongo.FindVendorAccountByUser(userId, models.YOUTUBE_VENDOR_NAME)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Build our fancy token source
|
||||
tokenSource := oauth2.ReuseTokenSource(ytAccount.Token(), mongo.NewVendorTokenSource(ytAccount))
|
||||
|
||||
//init service
|
||||
client, err := youtube.NewService(context.Background(), option.WithTokenSource(tokenSource))
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to init youtube service")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//add user to map
|
||||
ytClientMap[userId] = client
|
||||
|
||||
return client, nil
|
||||
} else {
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Revisit the structure of this function
|
||||
func ScheduleBroadcastFromWebhook(c *gin.Context, body *webhooks.EventDelivery) error {
|
||||
//get uid from context. Lots of sanitizing just incase
|
||||
uid := userIdFromContext(c)
|
||||
|
||||
//Check if this is a redilivery.
|
||||
|
||||
//Load ytClient for user. It is fetched from cache or created
|
||||
ytClient, err := youtubeServiceForUser(*uid)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to initialize youtube client")
|
||||
return err
|
||||
}
|
||||
|
||||
//Load pcoClient for user. It is fetched from cache or created
|
||||
pcoClient, err := pcoServiceForUser(*uid)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to initialize youtube client")
|
||||
return err
|
||||
}
|
||||
|
||||
//deserialize the payload
|
||||
payload := &services.Plan{}
|
||||
err = body.UnmarshallPayload(payload)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to unmarshall body")
|
||||
return err
|
||||
}
|
||||
|
||||
//Save audit point
|
||||
eventRecievedAudit := &models.EventRecieved{
|
||||
UserId: *uid,
|
||||
VendorName: models.PCO_VENDOR_NAME,
|
||||
VendorId: body.ID,
|
||||
CorrelationId: payload.Id,
|
||||
Type: body.Name,
|
||||
}
|
||||
|
||||
if err := mongo.SaveModel(eventRecievedAudit); err != nil {
|
||||
log.WithError(err).WithField("EventRecieved", eventRecievedAudit).Error("Failed to save audit event. Logging here and resuming")
|
||||
}
|
||||
|
||||
//Check to see if we have scheduled a broadcast befre
|
||||
broadcasts, err := mongo.FindAllBroadcastsByCorrelationId(*uid, payload.Id)
|
||||
if err != nil {
|
||||
return errors.Join(fmt.Errorf("Failed to find broadcasts for user: %s and CorrelationId: %s", uid.Hex(), payload.Id), err)
|
||||
}
|
||||
|
||||
var result string
|
||||
if len(broadcasts) > 0 {
|
||||
//What do we do when we have already scheduled the broadcast
|
||||
switch body.Name {
|
||||
//If we get plan created event for this, return already scheduled error
|
||||
case "services.v2.events.plan.created":
|
||||
return AlreadyScheduledBroadcast
|
||||
//update the broadcast
|
||||
case "services.v2.events.plan.updated":
|
||||
//TODO: Update Broadcast
|
||||
err := updateBroadcastFromWebhook(c, broadcasts, payload, ytClient, pcoClient)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to update broadcast from updated event")
|
||||
return err
|
||||
}
|
||||
result = UPDATED_BROADCAST
|
||||
//delete the broadcast
|
||||
case "services.v2.events.plan.destroyed":
|
||||
//TODO: Delete broadcast
|
||||
err := deleteBroadcastFromWebhook(c, broadcasts, payload, ytClient, pcoClient)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to delete broadcast from updated event")
|
||||
return err
|
||||
}
|
||||
result = DELETED_BROADCAST
|
||||
default:
|
||||
return UnknownEventErr
|
||||
}
|
||||
actionTaken := &models.ActionTaken{
|
||||
UserId: *uid,
|
||||
TriggeringEvent: eventRecievedAudit.MongoId(),
|
||||
Result: result,
|
||||
CorrelationId: payload.Id,
|
||||
VendorName: models.YOUTUBE_VENDOR_NAME,
|
||||
}
|
||||
|
||||
//save audit trail
|
||||
err = mongo.SaveModels(actionTaken)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to save broadcastModel and actionTaken")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
//No broadcast is scheduled
|
||||
//create the broadcast
|
||||
var broadcast *youtube.LiveBroadcast
|
||||
switch body.Name {
|
||||
case "services.v2.events.plan.created":
|
||||
broadcast, err = scheduleNewBroadcastFromWebhook(c, payload, ytClient, pcoClient)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to schedule broadcast from created event")
|
||||
return err
|
||||
}
|
||||
result = CREATED_BROADCAST
|
||||
case "services.v2.events.plan.updated":
|
||||
broadcast, err = scheduleNewBroadcastFromWebhook(c, payload, ytClient, pcoClient)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to schedule broadcast from updated event")
|
||||
return err
|
||||
}
|
||||
result = CREATED_BROADCAST
|
||||
case "services.v2.events.plan.destroyed":
|
||||
return NoBroadcastToDelete
|
||||
default:
|
||||
return fmt.Errorf("Unkown event error: %s", body.Name)
|
||||
}
|
||||
|
||||
//build audit trail after action was taken
|
||||
broadcastModel := &models.YoutubeBroadcast{
|
||||
UserId: *uid,
|
||||
CorrelationId: payload.Id,
|
||||
Details: broadcast,
|
||||
}
|
||||
|
||||
actionTaken := &models.ActionTaken{
|
||||
UserId: *uid,
|
||||
TriggeringEvent: eventRecievedAudit.MongoId(),
|
||||
Result: result,
|
||||
CorrelationId: payload.Id,
|
||||
VendorName: models.YOUTUBE_VENDOR_NAME,
|
||||
}
|
||||
|
||||
//save audit trail
|
||||
err = mongo.SaveModels(broadcastModel, actionTaken)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to save broadcastModel and actionTaken")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scheduleNewBroadcastFromWebhook(c *gin.Context, plan *services.Plan, ytClient *youtube.Service, pcoClient *pco.PcoApiClient) (*youtube.LiveBroadcast, error) {
|
||||
times, err := pcoClient.GetPlanTimes(plan.ServiceType.Id, plan.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startTime := times[0].StartsAt
|
||||
// endTime := times[len(times) - 1].EndsAt TODO: this will be used later
|
||||
|
||||
//if starttime is before now, skip with a passable error
|
||||
if startTime.Before(time.Now()) {
|
||||
return nil, NotSchedulableTime
|
||||
}
|
||||
|
||||
var title string
|
||||
if plan.Title == "" {
|
||||
title = "Live Stream Scheduled By Capstone"
|
||||
} else {
|
||||
title = plan.Title
|
||||
}
|
||||
|
||||
log.Debugf("Trying to schedule time at: %s", startTime.Format(yt_helpers.ISO_8601))
|
||||
return yt_helpers.InsertBroadcast(ytClient, title, startTime, yt_helpers.STATUS_PRIVATE)
|
||||
}
|
||||
|
||||
func updateBroadcastFromWebhook(c *gin.Context, broadcasts []models.YoutubeBroadcast, plan *services.Plan, ytClient *youtube.Service, pcoClient *pco.PcoApiClient) error {
|
||||
times, err := pcoClient.GetPlanTimes(plan.ServiceType.Id, plan.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
startTime := times[0].StartsAt
|
||||
// endTime := times[len(times) - 1].EndsAt TODO: this will be used later
|
||||
|
||||
//if starttime is before now, skip with a passable error
|
||||
if startTime.Before(time.Now()) {
|
||||
return NotSchedulableTime
|
||||
}
|
||||
|
||||
var title string
|
||||
if plan.Title == "" {
|
||||
title = "Live Stream Scheduled By Capstone"
|
||||
} else {
|
||||
title = plan.Title
|
||||
}
|
||||
|
||||
//create list of errors to process all of the broadcasts and then error
|
||||
errs := make([]error, 0, len(broadcasts))
|
||||
bcs := make([]*models.YoutubeBroadcast, 0, len(broadcasts))
|
||||
for index, broadcast := range broadcasts {
|
||||
liveBroadcast, err := yt_helpers.UpdateBroadcast(ytClient, broadcast.Details.Id, title, startTime, yt_helpers.STATUS_PRIVATE)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
broadcasts[index].Details = liveBroadcast
|
||||
bcs = append(bcs, &broadcasts[index])
|
||||
}
|
||||
}
|
||||
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.SaveModelSlice(mongo, bcs...)
|
||||
}
|
||||
|
||||
func deleteBroadcastFromWebhook(c *gin.Context, broadcasts []models.YoutubeBroadcast, plan *services.Plan, ytClient *youtube.Service, pcoClient *pco.PcoApiClient) error {
|
||||
errs := make([]error, 0, len(broadcasts))
|
||||
bcs := make([]*models.YoutubeBroadcast, 0, len(broadcasts))
|
||||
for index, broadcast := range broadcasts {
|
||||
err := yt_helpers.DeleteBroadcast(ytClient, broadcast.Details.Id)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
bcs = append(bcs, &broadcasts[index])
|
||||
}
|
||||
}
|
||||
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.DeleteModelSlice(mongo, bcs...)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-playground/assert/v2"
|
||||
)
|
||||
|
||||
func TestPlanEventMatch(t *testing.T) {
|
||||
events := []string{"services.v2.events.plan.updated", "services.v2.events.plan.destroyed", "services.v2.events.plan.created"}
|
||||
|
||||
for _, event := range events {
|
||||
assert.Equal(t, eventMatch("plan", event), true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
module git.preston-baxter.com/Preston_PLB/capstone/webhook-service
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.4
|
||||
|
||||
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.13.0
|
||||
golang.org/x/oauth2 v0.14.0
|
||||
google.golang.org/api v0.150.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
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/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // 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/miekg/dns v1.1.57 // 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/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.opencensus.io v0.24.0 // 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.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||
google.golang.org/grpc v1.59.0 // 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
|
||||
|
||||
replace github.com/google/jsonapi => ../libs/jsonapi
|
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/controllers"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Init()
|
||||
r := gin.Default()
|
||||
|
||||
controllers.BuildRouter(r)
|
||||
|
||||
var addr string
|
||||
if port := os.Getenv("PORT"); port != "" {
|
||||
addr = fmt.Sprintf("0.0.0.0:%s", port)
|
||||
} else {
|
||||
addr = "0.0.0.0:8080"
|
||||
}
|
||||
|
||||
err := r.Run(addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
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
|
||||
tokenSource oauth2.TokenSource
|
||||
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{},
|
||||
url: pco_url,
|
||||
}
|
||||
|
||||
return pco
|
||||
}
|
||||
|
||||
func NewClientWithOauthConfig(conf *oauth2.Config, tokenSource oauth2.TokenSource) *PcoApiClient {
|
||||
pco_url, err := url.Parse(PCO_API_URL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pco := &PcoApiClient{
|
||||
oauth: conf,
|
||||
tokenSource: tokenSource,
|
||||
url: pco_url,
|
||||
}
|
||||
|
||||
return pco
|
||||
}
|
||||
|
||||
func (api *PcoApiClient) getClient() *http.Client {
|
||||
if api.client == nil {
|
||||
api.client = oauth2.NewClient(context.Background(), api.tokenSource)
|
||||
}
|
||||
|
||||
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,60 @@
|
|||
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)
|
||||
}
|
||||
|
||||
planTimes, err := jsonapi.UnmarshalManyPayload[services.PlanTime](resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return planTimes, 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,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,5 @@
|
|||
package services
|
||||
|
||||
type Series struct {
|
||||
Id string `jsonapi:"primary,Series"`
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package services
|
||||
|
||||
type ServiceType struct {
|
||||
Id string `jsonapi:"primary,ServiceType"`
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
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)
|
||||
}
|
||||
|
||||
func TestMarshalling(t *testing.T) {
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package services
|
||||
|
||||
type Team struct {
|
||||
Id string `jsonapi:"primary,Team"`
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package pco
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
|
||||
"github.com/google/jsonapi"
|
||||
)
|
||||
|
||||
// gets all current subscriptions
|
||||
func (api *PcoApiClient) GetSubscriptions() ([]webhooks.Subscription, error) {
|
||||
api.Url().Path = "/webhooks/v2/subscriptions"
|
||||
|
||||
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 {
|
||||
if raw, err := io.ReadAll(resp.Body); err == nil {
|
||||
return nil, fmt.Errorf("Failed to retrieve subscriptions with status code: %d. Error %s", resp.StatusCode, string(raw))
|
||||
} else {
|
||||
return nil, fmt.Errorf("Failed to retrieve subscriptions with status code: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
subscriptions, err := jsonapi.UnmarshalManyPayload[webhooks.Subscription](resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
// Posts subscriptions to PCO api and returns a new list of subscriptions
|
||||
func (api *PcoApiClient) CreateSubscriptions(subscriptions []webhooks.Subscription) ([]webhooks.Subscription, error) {
|
||||
api.Url().Path = "/webhooks/v2/subscriptions"
|
||||
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
err := jsonapi.MarshalPayload(body, subscriptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, api.Url().String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := api.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode > 299 || resp.StatusCode < 200 {
|
||||
if raw, err := io.ReadAll(resp.Body); err == nil {
|
||||
return nil, fmt.Errorf("Failed to create subscriptions with status code: %d. Error %s", resp.StatusCode, string(raw))
|
||||
} else {
|
||||
return nil, fmt.Errorf("Failed to create subscriptions with status code: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
new_subscriptions, err := jsonapi.UnmarshalManyPayload[webhooks.Subscription](resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return new_subscriptions, nil
|
||||
}
|
||||
|
||||
// Posts subcription to PCO api and updates the subscription at the pointer that was passed to the fuinction with the server response
|
||||
func (api *PcoApiClient) CreateSubscription(subscription *webhooks.Subscription) error {
|
||||
api.Url().Path = "/webhooks/v2/subscriptions"
|
||||
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
err := jsonapi.MarshalPayload(body, subscription)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, api.Url().String(), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := api.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode > 299 || resp.StatusCode < 200 {
|
||||
if raw, err := io.ReadAll(resp.Body); err == nil {
|
||||
return fmt.Errorf("Failed to create subscriptions with status code: %d. Error %s", resp.StatusCode, string(raw))
|
||||
} else {
|
||||
return fmt.Errorf("Failed to create subscription with status code: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
err = jsonapi.UnmarshalPayload(resp.Body, subscription)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/services"
|
||||
"github.com/google/jsonapi"
|
||||
)
|
||||
|
||||
// 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,payload"`
|
||||
//Owner Organization of the event
|
||||
Organization *services.Organization `jsonapi:"relation,organization"`
|
||||
}
|
||||
|
||||
// Unmarshall payload of EventDelivery into the struct you think it is
|
||||
func (event *EventDelivery) UnmarshallPayload(obj any) error {
|
||||
return jsonapi.UnmarshalPayload(strings.NewReader(event.Payload), obj)
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package webhooks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/services"
|
||||
"github.com/go-playground/assert/v2"
|
||||
"github.com/google/jsonapi"
|
||||
)
|
||||
|
||||
func TestUnmarshallPayload(t *testing.T) {
|
||||
raw := `{"data":[{"id":"87f49852-1a2a-45cb-b4d2-0fa30eda0823","type":"EventDelivery","attributes":{"name":"services.v2.events.plan.created","attempt":1,"payload":"{\"data\":{\"type\":\"Plan\",\"id\":\"69259663\",\"attributes\":{\"can_view_order\":true,\"created_at\":\"2023-11-23T13:34:09Z\",\"dates\":\"No dates\",\"files_expire_at\":null,\"items_count\":0,\"last_time_at\":null,\"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/69259663\",\"prefers_order_view\":false,\"public\":false,\"rehearsable\":true,\"rehearsal_time_count\":0,\"reminders_disabled\":false,\"series_title\":null,\"service_time_count\":0,\"short_dates\":\"No dates\",\"sort_date\":\"2023-11-23T13:34:09Z\",\"title\":null,\"total_length\":0,\"updated_at\":\"2023-11-23T13:34:09Z\"},\"relationships\":{\"service_type\":{\"data\":{\"type\":\"ServiceType\",\"id\":\"1429991\"}},\"next_plan\":{\"data\":null},\"previous_plan\":{\"data\":null},\"attachment_types\":{\"data\":[]},\"series\":{\"data\":null},\"created_by\":{\"data\":{\"type\":\"Person\",\"id\":\"136901110\"}},\"updated_by\":{\"data\":{\"type\":\"Person\",\"id\":\"136901110\"}},\"linked_publishing_episode\":{\"data\":null}},\"links\":{\"all_attachments\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/all_attachments\",\"attachments\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/attachments\",\"attendances\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/attendances\",\"contributors\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/contributors\",\"import_template\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/import_template\",\"item_reorder\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/item_reorder\",\"items\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/items\",\"live\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/live\",\"my_schedules\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/my_schedules\",\"needed_positions\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/needed_positions\",\"next_plan\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/next_plan\",\"notes\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/notes\",\"plan_times\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/plan_times\",\"previous_plan\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/previous_plan\",\"series\":null,\"signup_teams\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/signup_teams\",\"team_members\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663/team_members\",\"self\":\"https://api.planningcenteronline.com/services/v2/service_types/1429991/plans/69259663\",\"html\":\"https://services.planningcenteronline.com/plans/69259663\"}},\"included\":[],\"meta\":{\"can_include\":[\"contributors\",\"my_schedules\",\"plan_times\",\"series\"],\"parent\":{\"id\":\"1429991\",\"type\":\"ServiceType\"},\"event_time\":\"2023-11-23T13:34:09Z\"}}"},"relationships":{"organization":{"data":{"type":"Organization","id":"456240"}}}}]}`
|
||||
|
||||
deliveries, err := jsonapi.UnmarshalManyPayload[EventDelivery](strings.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
plan := &services.Plan{}
|
||||
err = deliveries[0].UnmarshallPayload(plan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, plan.Id, "69259663")
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package webhooks
|
||||
|
||||
import "time"
|
||||
|
||||
type Subscription struct {
|
||||
Id string `jsonapi:"primary,Subscription" bson:"id"`
|
||||
//attrs
|
||||
Active bool `jsonapi:"attr,active,omitempty" bson:"active"`
|
||||
ApplicationId int `jsonapi:"attr,application_id,omitempty" bson:"application_id"`
|
||||
AuthenticitySecret string `jsonapi:"attr,authenticity_secret,omitempty" bson:"authenticity_secret"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created_at,rfc3339,omitempty" bson:"created_at"`
|
||||
UpdatedAt time.Time `jsonapi:"attr,updated_at,rfc3339,omitempty" bson:"updated_at"`
|
||||
Name string `jsonapi:"attr,name,omitempty" bson:"name"`
|
||||
Url string `jsonapi:"attr,url,omitempty" bson:"url"`
|
||||
}
|
||||
|
||||
type WebhookSubscription struct {
|
||||
Id string `jsonapi:"primary,WebhookSubscription" bson:"id"`
|
||||
//attrs
|
||||
Active bool `jsonapi:"attr,active,omitempty" bson:"active"`
|
||||
ApplicationId string `jsonapi:"attr,application_id,omitempty" bson:"application_id"`
|
||||
AuthenticitySecret string `jsonapi:"attr,authenticity_secret,omitempty" bson:"authenticity_secret"`
|
||||
CreatedAt time.Time `jsonapi:"attr,created_at,rfc3339,omitempty" bson:"created_at"`
|
||||
UpdatedAt time.Time `jsonapi:"attr,updated_at,rfc3339,omitempty" bson:"updated_at"`
|
||||
Name string `jsonapi:"attr,name,omitempty" bson:"name"`
|
||||
Url string `jsonapi:"attr,url,omitempty" bson:"url"`
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package pco
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
var pcoMockAccount models.VendorAccount = models.VendorAccount{
|
||||
OauthCredentials: &models.OauthCredential{
|
||||
AccessToken: "asdf;alskdfgha;dklrha;ldkfga;sldkf",
|
||||
ExpiresIn: 1234786012983,
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
TokenType: "bearer",
|
||||
RefreshToken: "asdfas;lkdjfas;dlkfj;asdlkj;aslf",
|
||||
},
|
||||
Name: "pco",
|
||||
}
|
||||
|
||||
func TestCreateSubscriptions(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Resp: %s", string(raw))
|
||||
w.Write([]byte(`{"data":[{"type":"Subscription","attributes":{"active":true,"name":"eventsandstuff","url":"https://thing.com/asdf/asdf/asdf"}},{"type":"Subscription","attributes":{"active":true,"name":"eventsandstuff","url":"https://thing.com/asdf/asdf/asdf"}}]}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
tokenSource := oauth2.StaticTokenSource(pcoMockAccount.Token())
|
||||
|
||||
mockPco := config.VendorConfig{
|
||||
ClientId: "as;dlkfja;slkdfj;aslkdfj;asdkl",
|
||||
ClientSecret: "as;dlfkjas;ldkfja;slkdfj;alsdkfjas;dklj",
|
||||
Scopes: []string{},
|
||||
AuthUri: ts.URL,
|
||||
TokenUri: ts.URL,
|
||||
RefreshEncode: "json",
|
||||
WebhookSecret: "as;dlfja;slkdja;slkdfj;alskdfj;alskdfa;slkdj",
|
||||
}
|
||||
|
||||
pcoApi := NewClientWithOauthConfig(mockPco.OauthConfig(), tokenSource)
|
||||
if newUrl, err := url.Parse(ts.URL); err == nil {
|
||||
pcoApi.url = newUrl
|
||||
} else {
|
||||
t.Fatalf("%s", err)
|
||||
}
|
||||
|
||||
mockSubscriptoins := []webhooks.Subscription{
|
||||
{
|
||||
Active: true,
|
||||
Name: "eventsandstuff",
|
||||
Url: "https://thing.com/asdf/asdf/asdf",
|
||||
},
|
||||
{
|
||||
Active: true,
|
||||
Name: "eventsandstuff",
|
||||
Url: "https://thing.com/asdf/asdf/asdf",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := pcoApi.CreateSubscriptions(mockSubscriptoins)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package youtube
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"google.golang.org/api/youtube/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
STATUS_PRIVATE = "private"
|
||||
STATUS_PUBLIC = "public"
|
||||
ISO_8601 = "2006-01-02T15:04:05.000Z"
|
||||
)
|
||||
|
||||
// Inserts Broadcast into youtube
|
||||
func InsertBroadcast(service *youtube.Service, title string, startTime time.Time, privacyStatus string) (*youtube.LiveBroadcast, error) {
|
||||
liveBroadcast := &youtube.LiveBroadcast{
|
||||
Snippet: &youtube.LiveBroadcastSnippet{
|
||||
Title: title,
|
||||
ScheduledStartTime: startTime.Format(ISO_8601),
|
||||
},
|
||||
Status: &youtube.LiveBroadcastStatus{
|
||||
PrivacyStatus: privacyStatus,
|
||||
},
|
||||
}
|
||||
return service.LiveBroadcasts.Insert([]string{"snippet", "status"}, liveBroadcast).Do()
|
||||
}
|
||||
|
||||
// given a broadcast ID update the broadcast
|
||||
func UpdateBroadcast(service *youtube.Service, id, title string, startTime time.Time, privacyStatus string) (*youtube.LiveBroadcast, error) {
|
||||
liveBroadcast := &youtube.LiveBroadcast{
|
||||
Id: id,
|
||||
Snippet: &youtube.LiveBroadcastSnippet{
|
||||
Title: title,
|
||||
ScheduledStartTime: startTime.Format(ISO_8601),
|
||||
},
|
||||
Status: &youtube.LiveBroadcastStatus{
|
||||
PrivacyStatus: privacyStatus,
|
||||
},
|
||||
}
|
||||
return service.LiveBroadcasts.Update([]string{"snippet", "status"}, liveBroadcast).Do()
|
||||
}
|
||||
|
||||
func DeleteBroadcast(service *youtube.Service, id string) error {
|
||||
return service.LiveBroadcasts.Delete(id).Do()
|
||||
}
|
|
@ -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,9 @@
|
|||
dist/*
|
||||
tmp/*
|
||||
**/*_templ.go
|
||||
templates/*.html
|
||||
docker/tmp/*
|
||||
node_modules/*
|
||||
node_modules
|
||||
config.yaml
|
||||
conf.yaml
|
|
@ -0,0 +1,23 @@
|
|||
BASE_URL="us-central1-docker.pkg.dev/pbaxter-infra/capstone-repo"
|
||||
|
||||
local-build:
|
||||
rm -f templates/*.go; templ generate -path ./templates
|
||||
npx tailwindcss -i tailwind/index.css -o dist/output.css
|
||||
GOEXPERIMENT=loopvar go build -o ./tmp/main .
|
||||
|
||||
local-run: local-build
|
||||
./tmp/main
|
||||
|
||||
infra-clean:
|
||||
cd docker; docker compose down
|
||||
|
||||
infra:
|
||||
cd docker; docker compose up -d --remove-orphans
|
||||
|
||||
|
||||
build:
|
||||
docker build -f ../docker/ui.dockerfile . -t frontend-service:latest
|
||||
docker tag frontend-service:latest $(BASE_URL)/frontend-service:latest
|
||||
|
||||
deploy: build
|
||||
docker push $(BASE_URL)/frontend-service:latest
|
|
@ -0,0 +1,85 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Mongo *MongoConfig `mapstructure:"mongo"`
|
||||
Vendors map[string]*VendorConfig `mapstructure:"vendors"`
|
||||
JwtSecret string `mapstructure:"jwt_secret"`
|
||||
Env string `mapstructure:"env"`
|
||||
AppSettings *AppSettings `mapstructure:"app_settings"`
|
||||
}
|
||||
|
||||
type AppSettings struct {
|
||||
WebhookServiceUrl string `mapstructure:"webhook_service_url"`
|
||||
FrontendServiceUrl string `mapstructure:"frontend_service_url"`
|
||||
}
|
||||
|
||||
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 {
|
||||
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"`
|
||||
WebhookSecret string `mapstructure:"webhook_secret"`
|
||||
scope string
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
var cfg *config
|
||||
|
||||
func Init() {
|
||||
viper.SetConfigName("config") // name of config file (without extension)
|
||||
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
|
||||
viper.AddConfigPath("/etc/capstone") // path to look for the config file in
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cfg = &config{}
|
||||
|
||||
err = viper.Unmarshal(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Config() *config {
|
||||
return cfg
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type actionFunc func(user *models.User) error
|
||||
|
||||
var (
|
||||
actionFuncs map[string]actionFunc = map[string]actionFunc{"pco.plan": setupPcoSubscriptions}
|
||||
webhooksTemplate map[string]webhooks.Subscription = map[string]webhooks.Subscription{
|
||||
"services.v2.events.plan.created": {
|
||||
Active: true,
|
||||
Name: "services.v2.events.plan.created",
|
||||
Url: "https://%s/pco/%s",
|
||||
},
|
||||
"services.v2.events.plan.updated": {
|
||||
Active: true,
|
||||
Name: "services.v2.events.plan.updated",
|
||||
Url: "https://%s/pco/%s",
|
||||
},
|
||||
"services.v2.events.plan.deleted": {
|
||||
Active: true,
|
||||
Name: "services.v2.events.plan.destroyed",
|
||||
Url: "https://%s/pco/%s",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
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 listener
|
||||
if afunc, ok := actionFuncs[strings.Join(source, ".")]; ok {
|
||||
err := afunc(user)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to setup actions")
|
||||
serverError(c, "Failed to setup actions")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Build mappings
|
||||
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{},
|
||||
},
|
||||
}
|
||||
|
||||
err := mongo.SaveModel(am)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to setup actions")
|
||||
serverError(c, "Failed to setup actions")
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(302, "/dashboard")
|
||||
}
|
||||
|
||||
func setupPcoSubscriptions(user *models.User) error {
|
||||
// Get PCO vendor account
|
||||
conf := config.Config()
|
||||
pcoAccount, err := mongo.FindVendorAccountByUser(user.Id, models.PCO_VENDOR_NAME)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//build pco api
|
||||
tokenSource := oauth2.ReuseTokenSource(pcoAccount.Token(), mongo.NewVendorTokenSource(pcoAccount))
|
||||
pcoApi := pco.NewClientWithOauthConfig(conf.Vendors[models.PCO_VENDOR_NAME].OauthConfig(), tokenSource)
|
||||
|
||||
//Check if subscriptions already exist
|
||||
webhookMap := make(map[string]webhooks.Subscription)
|
||||
subscriptions, err := pcoApi.GetSubscriptions()
|
||||
if err != nil {
|
||||
return errors.Join(fmt.Errorf("Failed to find subscriptions for user: %s", user.Id), err)
|
||||
}
|
||||
//Loop through found subscriptions
|
||||
for _, sub := range subscriptions {
|
||||
//if subsciption is in the templates look to add it to our map
|
||||
if templ, ok := webhooksTemplate[sub.Name]; ok {
|
||||
//if the subscription is for our url add it to our map
|
||||
url := fmt.Sprintf(templ.Url, conf.AppSettings.WebhookServiceUrl, user.Id.Hex())
|
||||
if url == sub.Url {
|
||||
webhookMap[sub.Name] = sub
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builtHooks := make([]webhooks.Subscription, 0, len(webhooksTemplate))
|
||||
//Build subscriptions
|
||||
for _, templ := range webhooksTemplate {
|
||||
if _, ok := webhookMap[templ.Name]; !ok {
|
||||
builtHooks = append(builtHooks, webhooks.Subscription{
|
||||
Active: true,
|
||||
Name: templ.Name,
|
||||
Url: fmt.Sprintf(templ.Url, conf.AppSettings.WebhookServiceUrl, user.Id.Hex()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Todo: save subscriptions for succesfull hooksetups
|
||||
for index := range builtHooks {
|
||||
err = pcoApi.CreateSubscription(&builtHooks[index])
|
||||
if err != nil {
|
||||
return errors.Join(fmt.Errorf("Failed to create subscription: %s for user: %s", builtHooks[index].Name, user.Id), err)
|
||||
}
|
||||
}
|
||||
|
||||
//Save Subscriptions
|
||||
err = mongo.SaveSubscriptionsForUser(user.Id, builtHooks...)
|
||||
if err != nil {
|
||||
return errors.Join(fmt.Errorf("Failed to save subscriptions for user: %s", user.Id), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const AUTH_COOKIE_NAME = "authorization"
|
||||
|
||||
var VALIDATE_EMAIL_REGEX = regexp.MustCompile(`^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$`)
|
||||
|
||||
type LoginPostBody struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func SignUpHandler(c *gin.Context) {
|
||||
//get uname and password.
|
||||
conf := config.Config()
|
||||
reqBody := &LoginPostBody{}
|
||||
c.Request.ParseForm()
|
||||
reqBody.Email = c.Request.FormValue("email")
|
||||
reqBody.Password = c.Request.FormValue("password")
|
||||
|
||||
if reqBody.Email == "" {
|
||||
log.Warn("Request contained no email")
|
||||
renderTempl(c, templates.SignupPage("Please provide an email"))
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.Password == "" {
|
||||
log.Warn("Request contained no password")
|
||||
renderTempl(c, templates.SignupPage("Please provide a password"))
|
||||
return
|
||||
}
|
||||
|
||||
//Verify username and password
|
||||
if ok := VALIDATE_EMAIL_REGEX.Match([]byte(reqBody.Email)); !ok {
|
||||
log.Warnf("User provided email field is not valid: %s", reqBody.Email)
|
||||
renderTempl(c, templates.SignupPage("You eamil is invalid. Please try again"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := mongo.FindUserByEmail(reqBody.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to lookup user: %s", reqBody.Email)
|
||||
renderTempl(c, templates.SignupPage("Error occured. Please try again later"))
|
||||
return
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
log.Warnf("User: %s, already exists", reqBody.Email)
|
||||
renderTempl(c, templates.SignupPage(fmt.Sprintf("user already exists for %s", reqBody.Email)))
|
||||
return
|
||||
}
|
||||
|
||||
//create new user
|
||||
user = &models.User{}
|
||||
|
||||
passHash, err := bcrypt.GenerateFromPassword([]byte(reqBody.Password), 10)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Passowrd hash failed for user: %s", reqBody.Email)
|
||||
renderTempl(c, templates.SignupPage("Signup failed. Please try again later"))
|
||||
return
|
||||
}
|
||||
|
||||
user.PassowrdHash = string(passHash)
|
||||
user.Email = reqBody.Email
|
||||
|
||||
err = mongo.SaveModel(user)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to write user to DB for user: %s", reqBody.Email)
|
||||
renderTempl(c, templates.SignupPage("Signup failed. Please try again later"))
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
exp := time.Now().Add(12 * time.Hour).Unix()
|
||||
//build jwt
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
|
||||
AuthClaims{
|
||||
Subject: user.MongoId().Hex(),
|
||||
Expires: exp,
|
||||
IssuedAt: now,
|
||||
NotBefore: now,
|
||||
Issuer: "capstone.preston-baxter.com",
|
||||
Audience: "capstone.preston-baxter.com",
|
||||
},
|
||||
)
|
||||
|
||||
jwtStr, err := token.SignedString([]byte(conf.JwtSecret))
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to encode jwt for user: %s", reqBody.Email)
|
||||
renderTempl(c, templates.SignupPage("Signup failed. Please try again later"))
|
||||
return
|
||||
}
|
||||
|
||||
//store jwt as cookie
|
||||
//TODO: Make sure set secure for prd deployment
|
||||
c.SetCookie(AUTH_COOKIE_NAME, jwtStr, 3600*24, "", "", false, true)
|
||||
|
||||
c.Redirect(302, "/dashboard")
|
||||
}
|
||||
|
||||
func LoginHandler(c *gin.Context) {
|
||||
//get uname and password.
|
||||
conf := config.Config()
|
||||
reqBody := &LoginPostBody{}
|
||||
c.Request.ParseForm()
|
||||
reqBody.Email = c.Request.FormValue("email")
|
||||
reqBody.Password = c.Request.FormValue("password")
|
||||
|
||||
if reqBody.Email == "" {
|
||||
log.Warn("Request contained no email")
|
||||
renderTempl(c, templates.LoginPage("Please provide an email"))
|
||||
return
|
||||
}
|
||||
|
||||
if reqBody.Password == "" {
|
||||
log.Warn("Request contained no password")
|
||||
renderTempl(c, templates.LoginPage("Please provide a password"))
|
||||
return
|
||||
}
|
||||
|
||||
//Verify username and password
|
||||
if ok := VALIDATE_EMAIL_REGEX.Match([]byte(reqBody.Email)); !ok {
|
||||
log.Warnf("User provided email field is not valid: %s", reqBody.Email)
|
||||
renderTempl(c, templates.SignupPage("You eamil is invalid. Please try again"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := mongo.FindUserByEmail(reqBody.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to lookup user: %s", reqBody.Email)
|
||||
renderTempl(c, templates.LoginPage(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
log.Warnf("No user was found for: %s", reqBody.Email)
|
||||
renderTempl(c, templates.LoginPage(fmt.Sprintf("No user found for %s", reqBody.Email)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PassowrdHash), []byte(reqBody.Password)); err != nil {
|
||||
log.Warnf("Password does not match for user: %s", reqBody.Email)
|
||||
renderTempl(c, templates.LoginPage("Email or password are incorrect"))
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
exp := time.Now().Add(12 * time.Hour).Unix()
|
||||
//build jwt
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
|
||||
AuthClaims{
|
||||
Subject: user.MongoId().Hex(),
|
||||
Expires: exp,
|
||||
IssuedAt: now,
|
||||
NotBefore: now,
|
||||
Issuer: "capstone.preston-baxter.com",
|
||||
Audience: "capstone.preston-baxter.com",
|
||||
},
|
||||
)
|
||||
|
||||
jwtStr, err := token.SignedString([]byte(conf.JwtSecret))
|
||||
if err != nil {
|
||||
renderTempl(c, templates.LoginPage("An error occured. Please try again later"))
|
||||
}
|
||||
|
||||
//store jwt as cookie
|
||||
var secure bool
|
||||
if conf.Env == "dev" {
|
||||
secure = false
|
||||
} else {
|
||||
secure = true
|
||||
}
|
||||
c.SetCookie(AUTH_COOKIE_NAME, jwtStr, 3600*24, "", "", secure, true)
|
||||
|
||||
c.Redirect(302, "/dashboard")
|
||||
}
|
||||
|
||||
func LogoutHandler(c *gin.Context) {
|
||||
conf := config.Config()
|
||||
|
||||
var secure bool
|
||||
if conf.Env == "dev" {
|
||||
secure = false
|
||||
} else {
|
||||
secure = true
|
||||
}
|
||||
c.SetCookie("authorization", "", 3600*24, "", "", secure, true)
|
||||
|
||||
c.Redirect(302, "/login")
|
||||
}
|
||||
|
||||
func getAuthHash(c *gin.Context) string {
|
||||
jwtToken, err := c.Cookie(AUTH_COOKIE_NAME)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(jwtToken))
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"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"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const USER_OBJ_KEY = "userObj"
|
||||
|
||||
type AuthClaims struct {
|
||||
Subject string `json:"sub"`
|
||||
Expires int64 `json:"exp"`
|
||||
IssuedAt int64 `json:"iat"`
|
||||
NotBefore int64 `json:"nbf"`
|
||||
Issuer string `json:"iss"`
|
||||
Audience string `json:"aud"`
|
||||
}
|
||||
|
||||
func (claims AuthClaims) GetExpirationTime() (*jwt.NumericDate, error) {
|
||||
time := time.Unix(claims.Expires, 0)
|
||||
return jwt.NewNumericDate(time), nil
|
||||
}
|
||||
|
||||
func (claims AuthClaims) GetIssuedAt() (*jwt.NumericDate, error) {
|
||||
time := time.Unix(claims.IssuedAt, 0)
|
||||
return jwt.NewNumericDate(time), nil
|
||||
}
|
||||
|
||||
func (claims AuthClaims) GetNotBefore() (*jwt.NumericDate, error) {
|
||||
time := time.Unix(claims.NotBefore, 0)
|
||||
return jwt.NewNumericDate(time), nil
|
||||
}
|
||||
|
||||
func (claims AuthClaims) GetIssuer() (string, error) {
|
||||
return claims.Issuer, nil
|
||||
}
|
||||
|
||||
func (claims AuthClaims) GetSubject() (string, error) {
|
||||
return claims.Subject, nil
|
||||
}
|
||||
|
||||
func (claims AuthClaims) GetAudience() (jwt.ClaimStrings, error) {
|
||||
return []string{claims.Subject}, nil
|
||||
}
|
||||
|
||||
func AuthMiddleware(strict bool) gin.HandlerFunc {
|
||||
conf := config.Config()
|
||||
return func(c *gin.Context) {
|
||||
//check for cookie
|
||||
token, err := c.Cookie("authorization")
|
||||
if err != nil {
|
||||
if err == http.ErrNoCookie {
|
||||
if strict {
|
||||
c.Redirect(301, "/login")
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.WithError(err).Error("Unable to get cookie from browser")
|
||||
c.AbortWithError(504, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
claims := &AuthClaims{}
|
||||
|
||||
parsedToken, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) {
|
||||
return []byte(conf.JwtSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
if err == jwt.ErrTokenExpired {
|
||||
log.Warn("Redirecting, jwt expired")
|
||||
c.Redirect(301, "/login")
|
||||
return
|
||||
} else {
|
||||
if strict {
|
||||
log.Warnf("Redirecting, jwt issue: %s", err)
|
||||
c.Redirect(301, "/login")
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Jwt is invalid, but auth is not strict. Reason: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !parsedToken.Valid {
|
||||
if strict {
|
||||
log.Warn("Redirecting, jwt invalid")
|
||||
c.Redirect(301, "/login")
|
||||
return
|
||||
} else {
|
||||
log.Warn("Jwt is invalid, but auth is not strict")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := mongo.FindUserById(claims.Subject)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Unable to get user: %s from DB", claims.Subject)
|
||||
c.AbortWithError(502, err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
log.Errorf("Unable to find user: %s in DB", claims.Subject)
|
||||
c.AbortWithError(502, nil)
|
||||
}
|
||||
|
||||
//store user object reference in session.
|
||||
c.Set(USER_OBJ_KEY, user)
|
||||
}
|
||||
}
|
||||
|
||||
func getUserFromContext(c *gin.Context) *models.User {
|
||||
if raw, exists := c.Get(USER_OBJ_KEY); exists {
|
||||
if user, ok := raw.(*models.User); ok {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
)
|
||||
|
||||
func GetAddActionForm(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
|
||||
}
|
||||
|
||||
accounts, err := mongo.FindAllVendorAccountsByUser(user.Id)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to find vendor accounts for: %s", user.Email)
|
||||
serverError(c, "No user available in context")
|
||||
return
|
||||
}
|
||||
|
||||
renderTempl(c, templates.DashboardActionModal(accounts))
|
||||
}
|
||||
|
||||
type DashboardMetric struct {
|
||||
Title string
|
||||
PrimaryValue string
|
||||
SecondaryValue string
|
||||
Subtitle string
|
||||
}
|
||||
|
||||
type dashboardMetricFunc func(c *gin.Context) *DashboardMetric
|
||||
|
||||
var metricFuncMap = map[string]dashboardMetricFunc{"default": defaultMetricFunction, "events_received": eventsRecievedMetricFunction, "streams_scheduled": streamsScheduledMetricFunction}
|
||||
|
||||
func GetMetricCard(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
|
||||
}
|
||||
|
||||
if metric, ok := c.GetQuery("metric"); ok {
|
||||
if metricFunc, mok := metricFuncMap[metric]; mok {
|
||||
renderDashboardMetric(c, &metricFunc)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//send default metric function
|
||||
log.Warn("Failed to find metricfunc")
|
||||
defaultFunc := metricFuncMap["default"]
|
||||
renderDashboardMetric(c, &defaultFunc)
|
||||
}
|
||||
|
||||
func defaultMetricFunction(c *gin.Context) *DashboardMetric {
|
||||
return &DashboardMetric{
|
||||
Title: "Err",
|
||||
PrimaryValue: "0.00",
|
||||
SecondaryValue: "0.00",
|
||||
Subtitle: "something went wrong",
|
||||
}
|
||||
}
|
||||
|
||||
func renderDashboardMetric(c *gin.Context, metricFunc *dashboardMetricFunc) {
|
||||
metric := (*metricFunc)(c)
|
||||
renderTempl(c, templates.DashboardCard(metric.Title, metric.PrimaryValue, metric.SecondaryValue, metric.Subtitle))
|
||||
}
|
||||
|
||||
func eventsRecievedMetricFunction(c *gin.Context) *DashboardMetric {
|
||||
user := getUserFromContext(c)
|
||||
|
||||
events, err := mongo.AggregateVendorEventReport(user.Id)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to find events for user: %s", user.Id.Hex())
|
||||
return defaultMetricFunction(c)
|
||||
}
|
||||
|
||||
totalEvents := 0
|
||||
biggestVendor := 0
|
||||
for index, event := range events {
|
||||
totalEvents += event.Count
|
||||
if events[biggestVendor].Count < event.Count {
|
||||
biggestVendor = index
|
||||
}
|
||||
}
|
||||
|
||||
p := message.NewPrinter(language.English)
|
||||
metric := &DashboardMetric{
|
||||
Title: "Events Recieved",
|
||||
PrimaryValue: p.Sprintf("%d", totalEvents),
|
||||
}
|
||||
if len(events) > 0 {
|
||||
metric.Subtitle = fmt.Sprintf("Most events from: %s", events[biggestVendor].Name)
|
||||
}
|
||||
|
||||
return metric
|
||||
}
|
||||
|
||||
func streamsScheduledMetricFunction(c *gin.Context) *DashboardMetric {
|
||||
user := getUserFromContext(c)
|
||||
|
||||
events, err := mongo.AggregateBroadcastReport(user.Id)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to find broadcast report for user: %s", user.Id.Hex())
|
||||
return defaultMetricFunction(c)
|
||||
}
|
||||
|
||||
totalEvents := 0
|
||||
for _, event := range events {
|
||||
totalEvents += event.Count
|
||||
}
|
||||
|
||||
p := message.NewPrinter(language.English)
|
||||
return &DashboardMetric{
|
||||
Title: "Broadcasts scheduled",
|
||||
PrimaryValue: p.Sprintf("%d", totalEvents),
|
||||
SecondaryValue: "",
|
||||
Subtitle: "Scheduled to youtube",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
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-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var mongo *db.DB
|
||||
var log *logrus.Logger
|
||||
|
||||
func BuildRouter(r *gin.Engine) {
|
||||
conf := config.Config()
|
||||
|
||||
var err error
|
||||
mongo, err = db.NewClient(conf.Mongo.Uri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
log = logrus.New()
|
||||
log.SetFormatter(&logrus.TextFormatter{
|
||||
DisableColors: true,
|
||||
})
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
|
||||
r.Use(cors.Default())
|
||||
|
||||
r.Static("/static", "/var/capstone/dist")
|
||||
//mainpage
|
||||
r.GET("/", AuthMiddleware(false), LandingPage)
|
||||
|
||||
//Auth
|
||||
r.GET("/login", AuthMiddleware(false), LoginPage)
|
||||
r.GET("/signup", AuthMiddleware(false), SignUpPage)
|
||||
r.POST("/login", LoginHandler)
|
||||
r.POST("/signup", SignUpHandler)
|
||||
r.POST("/logout", LogoutHandler)
|
||||
|
||||
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)
|
||||
//Dashboard Components
|
||||
dashboardComponents := dashboard.Group("/components")
|
||||
dashboardComponents.GET("/metric_card", GetMetricCard)
|
||||
//Dashboard Events
|
||||
dashboardEvents := dashboard.Group("/events")
|
||||
dashboardEvents.GET("", EventsPage)
|
||||
dashboardEventComponents := dashboardEvents.Group("/components")
|
||||
dashboardEventComponents.GET("/table_data", GetTableComponent)
|
||||
|
||||
//Vendor stuff
|
||||
vendor := r.Group("/vendor")
|
||||
vendor.Use(AuthMiddleware(true))
|
||||
|
||||
youtube := vendor.Group("/youtube")
|
||||
youtube.POST("/initiate", InitiateYoutubeOuath)
|
||||
youtube.GET("/callback", ReceiveYoutubeOauth)
|
||||
|
||||
pco := vendor.Group("/pco")
|
||||
pco.POST("/initiate", InitiatePCOOuath)
|
||||
pco.GET("/callback", RecievePCOOuath)
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
eventsTableMap = map[string]eventsTableFunc{"default": defaultTableData, "events_for_user": eventsForUserTableData, "actions_for_user": actionsForUserTableData}
|
||||
)
|
||||
|
||||
type eventsTableFunc func(c *gin.Context) templates.TableData
|
||||
|
||||
func GetTableComponent(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
|
||||
}
|
||||
|
||||
if table, ok := c.GetQuery("table_name"); ok {
|
||||
if tableFunc, mok := eventsTableMap[table]; mok {
|
||||
renderEventTable(c, table, &tableFunc)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//send default metric function
|
||||
log.Warn("Failed to find eventsTableFunc")
|
||||
defaultFunc := eventsTableMap["default"]
|
||||
renderEventTable(c, "default", &defaultFunc)
|
||||
}
|
||||
|
||||
func defaultTableData(c *gin.Context) templates.TableData {
|
||||
return [][]string{{"id", "col 1", "col 2"}, {"row data", "item data", "stuff"}}
|
||||
}
|
||||
|
||||
func renderEventTable(c *gin.Context, title string, tableFunc *eventsTableFunc) {
|
||||
tableData := (*tableFunc)(c)
|
||||
renderTempl(c, templates.EventTableData(tableData, title))
|
||||
}
|
||||
|
||||
func eventsForUserTableData(c *gin.Context) templates.TableData {
|
||||
//User can't be nil because we check before we get here
|
||||
user := getUserFromContext(c)
|
||||
|
||||
events, err := mongo.FindEventsRecievedByUserId(user.Id)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to find events for user: %s to load table data", user.Id.Hex())
|
||||
return defaultTableData(c)
|
||||
}
|
||||
|
||||
//check for filter
|
||||
filter, filter_exists := c.GetQuery("filter")
|
||||
|
||||
table := make([][]string, len(events)+1)
|
||||
index := 1
|
||||
for _, event := range events {
|
||||
arr := []string{event.CreatedAt.Format(time.Stamp), strings.ToUpper(event.VendorName), event.CorrelationId, event.Type}
|
||||
|
||||
if filter_exists {
|
||||
//if the filter exists loop through the row. Check if anything meets the filter
|
||||
pass := false
|
||||
for _, item := range arr {
|
||||
//if we already have a match short circuit. If we don't we can potentially flip from false -> true
|
||||
pass = pass || strings.Contains(item, filter)
|
||||
}
|
||||
//If we did not find a matching item continue
|
||||
if !pass {
|
||||
continue
|
||||
}
|
||||
}
|
||||
//We either had no filter or passed the filter check. Add to the pool
|
||||
table[index] = arr
|
||||
index += 1
|
||||
}
|
||||
table[0] = []string{"Timestamp", "Vendor", "Id", "Event Type"}
|
||||
|
||||
return table[0:index]
|
||||
}
|
||||
|
||||
func actionsForUserTableData(c *gin.Context) templates.TableData {
|
||||
//User can't be nil because we check before we get here
|
||||
user := getUserFromContext(c)
|
||||
|
||||
actions, err := mongo.FindActionTakenByUserId(user.Id)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to find actions for user: %s to load table data", user.Id.Hex())
|
||||
return defaultTableData(c)
|
||||
}
|
||||
|
||||
//check for filter
|
||||
filter, filter_exists := c.GetQuery("filter")
|
||||
index := 1
|
||||
table := make([][]string, len(actions)+1)
|
||||
for _, action := range actions {
|
||||
arr := []string{action.CreatedAt.Format(time.Stamp), action.VendorName, action.CorrelationId, action.Result}
|
||||
if filter_exists {
|
||||
//if the filter exists loop through the row. Check if anything meets the filter
|
||||
pass := false
|
||||
for _, item := range arr {
|
||||
//if we already have a match short circuit. If we don't we can potentially flip from false -> true
|
||||
pass = pass || strings.Contains(item, filter)
|
||||
}
|
||||
//If we did not find a matching item continue
|
||||
if !pass {
|
||||
continue
|
||||
}
|
||||
}
|
||||
table[index] = arr
|
||||
index += 1
|
||||
}
|
||||
table[0] = []string{"Timestamp", "Vendor", "Id", "Result"}
|
||||
|
||||
return table[0:index]
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func LandingPage(c *gin.Context) {
|
||||
if raw, exists := c.Get(USER_OBJ_KEY); exists {
|
||||
if user, ok := raw.(*models.User); ok {
|
||||
renderTempl(c, templates.LandingPage(user))
|
||||
return
|
||||
}
|
||||
}
|
||||
renderTempl(c, templates.LandingPage(nil))
|
||||
}
|
||||
|
||||
func LoginPage(c *gin.Context) {
|
||||
renderTempl(c, templates.LoginPage(""))
|
||||
}
|
||||
|
||||
func SignUpPage(c *gin.Context) {
|
||||
renderTempl(c, templates.SignupPage(""))
|
||||
}
|
||||
|
||||
func DashboardPage(c *gin.Context) {
|
||||
user := getUserFromContext(c)
|
||||
|
||||
if user == nil {
|
||||
log.Error("No user found in context")
|
||||
serverError(c, "No user found in context")
|
||||
return
|
||||
}
|
||||
|
||||
//Split database fetching into go routines
|
||||
var vendors []models.VendorAccount
|
||||
var actions []models.ActionMapping
|
||||
//TODO: find a generic way to do this.
|
||||
errs := make([]error, 2)
|
||||
|
||||
//Use waitgroup to syncronize
|
||||
waitGroup := new(sync.WaitGroup)
|
||||
waitGroup.Add(2)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
vendors, errs[0] = mongo.FindAllVendorAccountsByUser(user.MongoId())
|
||||
wg.Done()
|
||||
}(waitGroup)
|
||||
|
||||
go func(wg *sync.WaitGroup) {
|
||||
actions, errs[1] = mongo.FindActionMappingsByUser(user.MongoId())
|
||||
wg.Done()
|
||||
}(waitGroup)
|
||||
|
||||
//after this line we are in sync
|
||||
waitGroup.Wait()
|
||||
|
||||
//handle errors
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
log.WithError(errors.Join(errs...)).Error("Failed to do database lookup when retrieving dashbDashboardPage")
|
||||
serverError(c, "Failed to do database lookup when retrieving dashbDashboardPage")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
renderTempl(c, templates.DashboardPage(user, vendors, actions))
|
||||
}
|
||||
|
||||
func EventsPage(c *gin.Context) {
|
||||
user := getUserFromContext(c)
|
||||
|
||||
if user == nil {
|
||||
log.Error("No user found in context")
|
||||
serverError(c, "No user found in context")
|
||||
return
|
||||
}
|
||||
|
||||
renderTempl(c, templates.EventsPage(user))
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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://%s/vendor/pco/callback"
|
||||
|
||||
func InitiatePCOOuath(c *gin.Context) {
|
||||
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", fmt.Sprintf(PCO_REDIRECT_URI, conf.AppSettings.FrontendServiceUrl))
|
||||
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", fmt.Sprintf(PCO_REDIRECT_URI, conf.AppSettings.FrontendServiceUrl))
|
||||
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")
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Responds with 200ok and the rendered template
|
||||
func renderTempl(c *gin.Context, tmpl templ.Component) {
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
tmpl.Render(c.Request.Context(), buf)
|
||||
|
||||
c.Data(200, "text/html", buf.Bytes())
|
||||
}
|
||||
|
||||
func badRequest(c *gin.Context, reason string) {
|
||||
c.JSON(400, map[string]string{"error": reason})
|
||||
}
|
||||
|
||||
func serverError(c *gin.Context, reason string) {
|
||||
c.JSON(504, map[string]string{"error": reason})
|
||||
}
|
||||
|
||||
func notFound(c *gin.Context, reason string) {
|
||||
c.JSON(404, map[string]string{"error": reason})
|
||||
}
|
||||
|
||||
func unauthorized(c *gin.Context, reason string) {
|
||||
c.JSON(403, map[string]string{"error": reason})
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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 YOUTUBE_REDIRECT_URI = "https://%s/vendor/youtube/callback"
|
||||
|
||||
func InitiateYoutubeOuath(c *gin.Context) {
|
||||
conf := config.Config()
|
||||
vendorConfig := conf.Vendors[models.YOUTUBE_VENDOR_NAME]
|
||||
|
||||
init_url, err := url.Parse(vendorConfig.AuthUri)
|
||||
if err != nil {
|
||||
//we should not get here
|
||||
panic(err)
|
||||
}
|
||||
|
||||
q := init_url.Query()
|
||||
//https://developers.google.com/youtube/v3/guides/auth/server-side-web-apps#httprest_1
|
||||
q.Add("client_id", vendorConfig.ClientId)
|
||||
q.Add("redirect_uri", fmt.Sprintf(YOUTUBE_REDIRECT_URI, conf.AppSettings.FrontendServiceUrl))
|
||||
q.Add("response_type", "code")
|
||||
q.Add("scope", vendorConfig.Scope())
|
||||
q.Add("access_type", "offline")
|
||||
//used to prevent CSRF
|
||||
q.Add("state", getAuthHash(c))
|
||||
init_url.RawQuery = q.Encode()
|
||||
|
||||
c.Redirect(302, init_url.String())
|
||||
}
|
||||
|
||||
func ReceiveYoutubeOauth(c *gin.Context) {
|
||||
conf := config.Config()
|
||||
vendorConfig := conf.Vendors[models.YOUTUBE_VENDOR_NAME]
|
||||
user := getUserFromContext(c)
|
||||
|
||||
if user == nil {
|
||||
log.Error("Unable to find user in context")
|
||||
c.AbortWithStatus(502)
|
||||
}
|
||||
|
||||
code := c.Query("code")
|
||||
respHash := c.Query("state")
|
||||
//validate returned code
|
||||
if code == "" {
|
||||
log.Error("Youtube OAuth response did not contain a code. Possible CSRF")
|
||||
c.AbortWithStatus(502)
|
||||
return
|
||||
}
|
||||
|
||||
//validate state
|
||||
if respHash == "" || respHash != getAuthHash(c) {
|
||||
log.Error("Youtube OAuth response did not contain the correct hash. 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", fmt.Sprintf(YOUTUBE_REDIRECT_URI, conf.AppSettings.FrontendServiceUrl))
|
||||
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)
|
||||
}
|
||||
//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.YOUTUBE_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")
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
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/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func (db *DB) FindActionMappingsByUser(userId primitive.ObjectID) ([]models.ActionMapping, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.Find()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.ACTION_MAPPING_TYPE}, opts)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actions := []models.ActionMapping{}
|
||||
err = res.All(context.Background(), &actions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return actions, nil
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"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/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// return audit trail for user
|
||||
func (db *DB) FindAuditTrailForUser(userId primitive.ObjectID) ([]models.EventRecieved, []models.ActionTaken, error) {
|
||||
conf := config.Config()
|
||||
|
||||
//Build sync things
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(2)
|
||||
errs := make([]error, 2)
|
||||
|
||||
events := []models.EventRecieved{}
|
||||
actions := []models.ActionTaken{}
|
||||
|
||||
//Spawn event recieved goroutine
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
opts := options.Find()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.EVENT_RECIEVED_TYPE}, opts)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return
|
||||
}
|
||||
errs[0] = err
|
||||
return
|
||||
}
|
||||
|
||||
err = res.All(context.Background(), &events)
|
||||
if err != nil {
|
||||
errs[0] = err
|
||||
return
|
||||
}
|
||||
}(wg)
|
||||
|
||||
//Spawn action taken goroutine
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
opts := options.Find()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.ACTION_MAPPING_TYPE}, opts)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return
|
||||
}
|
||||
errs[1] = err
|
||||
return
|
||||
}
|
||||
|
||||
err = res.All(context.Background(), &actions)
|
||||
if err != nil {
|
||||
errs[1] = err
|
||||
return
|
||||
}
|
||||
}(wg)
|
||||
|
||||
//wait for go routines to finish
|
||||
wg.Wait()
|
||||
|
||||
//if there was an error return the combined error
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return events, actions, nil
|
||||
}
|
||||
|
||||
func (db *DB) FindEventsRecievedByUserId(userId primitive.ObjectID) ([]models.EventRecieved, error) {
|
||||
conf := config.Config()
|
||||
opts := options.Find()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.EVENT_RECIEVED_TYPE}, opts)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := []models.EventRecieved{}
|
||||
err = res.All(context.Background(), &events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (db *DB) FindActionTakenByUserId(userId primitive.ObjectID) ([]models.ActionTaken, error) {
|
||||
conf := config.Config()
|
||||
opts := options.Find()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.ACTION_TAKEN_TYPE}, opts)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := []models.ActionTaken{}
|
||||
err = res.All(context.Background(), &events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
type VendorEventReport struct {
|
||||
Count int `bson:"count"`
|
||||
Name string `bson:"_id"`
|
||||
}
|
||||
|
||||
func (db *DB) AggregateBroadcastReport(userId primitive.ObjectID) ([]VendorEventReport, error) {
|
||||
conf := config.Config()
|
||||
opts := options.Aggregate().SetAllowDiskUse(false)
|
||||
|
||||
aggregation := bson.A{
|
||||
bson.D{{Key: "$match", Value: bson.D{{Key: "obj_info.ent", Value: models.ACTION_TAKEN_TYPE}, {Key: "result", Value: "Created Broadcast"}}}},
|
||||
bson.D{
|
||||
{Key: "$group",
|
||||
Value: bson.D{
|
||||
{Key: "_id", Value: nil},
|
||||
{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Aggregate(context.Background(), aggregation, opts)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := []VendorEventReport{}
|
||||
err = res.All(context.Background(), &events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (db *DB) AggregateVendorEventReport(userId primitive.ObjectID) ([]VendorEventReport, error) {
|
||||
conf := config.Config()
|
||||
opts := options.Aggregate().SetAllowDiskUse(false)
|
||||
|
||||
aggregation := bson.A{
|
||||
bson.D{{Key: "$match", Value: bson.D{{Key: "obj_info.ent", Value: models.EVENT_RECIEVED_TYPE}}}},
|
||||
bson.D{
|
||||
{Key: "$group",
|
||||
Value: bson.D{
|
||||
{Key: "_id", Value: "$vendor_name"},
|
||||
{Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Aggregate(context.Background(), aggregation, opts)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := []VendorEventReport{}
|
||||
err = res.All(context.Background(), &events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
// Interface for any object that wants to take advantage of the DB package
|
||||
type Model interface {
|
||||
|
||||
//Should return the _id field of the object if it exits
|
||||
//if it is new it should generate a new objectId
|
||||
MongoId() primitive.ObjectID
|
||||
|
||||
//It is expected that this will update the CommonFields part of the model
|
||||
UpdateObjectInfo()
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
client *mongo.Client
|
||||
}
|
||||
|
||||
func NewClient(uri string) (*DB, error) {
|
||||
opts := options.Client().ApplyURI(uri).SetConnectTimeout(60 * time.Second)
|
||||
client, err := mongo.Connect(context.Background(), opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := client.Ping(context.Background(), readpref.Primary()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DB{client: client}, nil
|
||||
}
|
||||
|
||||
// Upserts
|
||||
func (db *DB) SaveModel(m Model) error {
|
||||
conf := config.Config()
|
||||
|
||||
m.UpdateObjectInfo()
|
||||
opts := options.Update().SetUpsert(true)
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).UpdateOne(context.Background(), bson.M{"_id": m.MongoId()}, bson.M{"$set": m}, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.MatchedCount == 0 && res.ModifiedCount == 0 && res.UpsertedCount == 0 {
|
||||
return errors.New("Failed to save model properly")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) SaveModels(m ...Model) error {
|
||||
conf := config.Config()
|
||||
|
||||
writeEntry := make([]mongo.WriteModel, len(m))
|
||||
|
||||
for index, model := range m {
|
||||
entry := mongo.NewUpdateOneModel()
|
||||
entry.SetFilter(bson.M{"_id": model.MongoId()})
|
||||
entry.SetUpsert(true)
|
||||
entry.SetUpdate(bson.M{"$set": model})
|
||||
model.UpdateObjectInfo()
|
||||
|
||||
writeEntry[index] = entry
|
||||
}
|
||||
|
||||
opts := options.BulkWrite()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).BulkWrite(context.Background(), writeEntry, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.MatchedCount == 0 && res.ModifiedCount == 0 && res.UpsertedCount == 0 {
|
||||
return errors.New("Failed to save models properly")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveModelSlice[T Model](db *DB, m ...T) error {
|
||||
return saveModels[T](db, m...)
|
||||
}
|
||||
|
||||
// For allowing more varidaic like things
|
||||
func saveModels[T Model](db *DB, m ...T) error {
|
||||
conf := config.Config()
|
||||
|
||||
writeEntry := make([]mongo.WriteModel, len(m))
|
||||
|
||||
for index, model := range m {
|
||||
entry := mongo.NewUpdateOneModel()
|
||||
entry.SetFilter(bson.M{"_id": model.MongoId()})
|
||||
entry.SetUpsert(true)
|
||||
entry.SetUpdate(bson.M{"$set": model})
|
||||
model.UpdateObjectInfo()
|
||||
|
||||
writeEntry[index] = entry
|
||||
}
|
||||
|
||||
opts := options.BulkWrite()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).BulkWrite(context.Background(), writeEntry, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.MatchedCount == 0 && res.ModifiedCount == 0 && res.UpsertedCount == 0 {
|
||||
return errors.New("Failed to save models properly")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteModelSlice[T Model](db *DB, m ...T) error {
|
||||
return deleteModels[T](db, m...)
|
||||
}
|
||||
|
||||
// For allowing more varidaic like things
|
||||
func deleteModels[T Model](db *DB, m ...T) error {
|
||||
conf := config.Config()
|
||||
|
||||
writeEntry := make([]mongo.WriteModel, len(m))
|
||||
|
||||
for index, model := range m {
|
||||
entry := mongo.NewDeleteOneModel()
|
||||
entry.SetFilter(bson.M{"_id": model.MongoId()})
|
||||
model.UpdateObjectInfo()
|
||||
|
||||
writeEntry[index] = entry
|
||||
}
|
||||
|
||||
opts := options.BulkWrite()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).BulkWrite(context.Background(), writeEntry, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.DeletedCount == 0 {
|
||||
return errors.New("Failed to delete models properly")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Doesn't upsert
|
||||
func (db *DB) InsertModel(m Model) error {
|
||||
conf := config.Config()
|
||||
|
||||
m.UpdateObjectInfo()
|
||||
opts := options.InsertOne()
|
||||
_, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).InsertOne(context.Background(), m, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) DeleteModel(m Model) error {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.Delete()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).DeleteOne(context.Background(), bson.M{"_id": m.MongoId()}, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.DeletedCount == 0 {
|
||||
return errors.New("There was no item to delete")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
ACTION_MAPPING_TYPE = "action"
|
||||
)
|
||||
|
||||
type ActionMapping struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
Id primitive.ObjectID `bson:"_id,omitempty"`
|
||||
UserId primitive.ObjectID `bson:"user_id,omitempty"`
|
||||
SourceEvent *Event `bson:"source_event,omitempty"`
|
||||
Action *Action `bson:"action,omitempty"`
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
VendorName string `bson:"vendor_name,omitempty"`
|
||||
Type string `bson:"type,omitempty"`
|
||||
Fields map[string]string `bson:"fields,omitempty"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const (
|
||||
EVENT_RECIEVED_TYPE = "audit_event_recieved"
|
||||
ACTION_TAKEN_TYPE = "audit_action_taken"
|
||||
)
|
||||
|
||||
// Event Recieved
|
||||
type EventRecieved struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
Id primitive.ObjectID `bson:"_id,omitempty"`
|
||||
UserId primitive.ObjectID `bson:"user_id,omitempty"` //what user is this associated too
|
||||
VendorName string `bson:"vendor_name,omitempty"` //Vendor name of who sent us the event
|
||||
VendorId string `bson:"vendor_id,omitempty"`
|
||||
CorrelationId string `bson:"correlation_id,omitempty"` //list of entities effected or created from action
|
||||
Type string `bson:"type,omitempty"` //type of event
|
||||
}
|
||||
|
||||
func (obj *EventRecieved) MongoId() primitive.ObjectID {
|
||||
if obj.Id.IsZero() {
|
||||
now := time.Now()
|
||||
obj.Id = primitive.NewObjectIDFromTimestamp(now)
|
||||
}
|
||||
|
||||
return obj.Id
|
||||
}
|
||||
|
||||
func (obj *EventRecieved) UpdateObjectInfo() {
|
||||
now := time.Now()
|
||||
if obj.CommonFields == nil {
|
||||
obj.CommonFields = new(CommonFields)
|
||||
obj.EntityType = EVENT_RECIEVED_TYPE
|
||||
obj.CreatedAt = now
|
||||
}
|
||||
obj.UpdatedAt = now
|
||||
}
|
||||
|
||||
// Action Taken
|
||||
type ActionTaken struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
Id primitive.ObjectID `bson:"_id,omitempty"`
|
||||
UserId primitive.ObjectID `bson:"user_id,omitempty"` //what user is this associated too
|
||||
TriggeringEvent primitive.ObjectID `bson:"triggering_event,omitempty"` //what triggered this action to be taken
|
||||
Result string `bson:"result,omitempty"` //list of entities effected or created from action
|
||||
CorrelationId string `bson:"correlation_id,omitempty"` //list of entities effected or created from action
|
||||
VendorName string `bson:"vendor_name,omitempty"` //Vendor name that the action was taken against
|
||||
}
|
||||
|
||||
func (obj *ActionTaken) MongoId() primitive.ObjectID {
|
||||
if obj.Id.IsZero() {
|
||||
now := time.Now()
|
||||
obj.Id = primitive.NewObjectIDFromTimestamp(now)
|
||||
}
|
||||
|
||||
return obj.Id
|
||||
}
|
||||
|
||||
func (obj *ActionTaken) UpdateObjectInfo() {
|
||||
now := time.Now()
|
||||
if obj.CommonFields == nil {
|
||||
obj.CommonFields = new(CommonFields)
|
||||
obj.EntityType = ACTION_TAKEN_TYPE
|
||||
obj.CreatedAt = now
|
||||
}
|
||||
obj.UpdatedAt = now
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type CommonFields struct {
|
||||
EntityType string `bson:"ent,omitempty"`
|
||||
CreatedAt time.Time `bson:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `bson:"updated_at,omitempty"`
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
const TOKEN_LOCK_TYPE = "token_lock"
|
||||
|
||||
type TokenLock struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
Id primitive.ObjectID `bson:"_id"`
|
||||
VendorId primitive.ObjectID `bson:"vendor_id"`
|
||||
TokenId string `bson:"token_id"`
|
||||
Refreshed bool `bson:"refreshed"`
|
||||
}
|
||||
|
||||
func (tl *TokenLock) MongoId() primitive.ObjectID {
|
||||
|
||||
if tl.Id.IsZero() {
|
||||
now := time.Now()
|
||||
tl.Id = primitive.NewObjectIDFromTimestamp(now)
|
||||
}
|
||||
|
||||
return tl.Id
|
||||
}
|
||||
|
||||
func (tl *TokenLock) UpdateObjectInfo() {
|
||||
now := time.Now()
|
||||
if tl.CommonFields == nil {
|
||||
tl.CommonFields = new(CommonFields)
|
||||
tl.EntityType = TOKEN_LOCK_TYPE
|
||||
tl.CreatedAt = now
|
||||
}
|
||||
tl.UpdatedAt = now
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const PCO_SUBSCRIPTION_TYPE = "pco_subscription"
|
||||
|
||||
type PcoSubscription struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
Id primitive.ObjectID `bson:"_id,omitempty"`
|
||||
UserId primitive.ObjectID `bson:"user_id,omitempty"`
|
||||
Details *webhooks.Subscription `bson:"details,omitempty"`
|
||||
}
|
||||
|
||||
func (obj *PcoSubscription) MongoId() primitive.ObjectID {
|
||||
if obj.Id.IsZero() {
|
||||
now := time.Now()
|
||||
obj.Id = primitive.NewObjectIDFromTimestamp(now)
|
||||
}
|
||||
|
||||
return obj.Id
|
||||
}
|
||||
|
||||
func (obj *PcoSubscription) UpdateObjectInfo() {
|
||||
now := time.Now()
|
||||
if obj.CommonFields == nil {
|
||||
obj.CommonFields = new(CommonFields)
|
||||
obj.EntityType = PCO_SUBSCRIPTION_TYPE
|
||||
obj.CreatedAt = now
|
||||
}
|
||||
obj.UpdatedAt = now
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const USER_TYPE string = "user"
|
||||
|
||||
type User struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
Id primitive.ObjectID `bson:"_id"`
|
||||
Email string `bson:"email,omitempty"`
|
||||
PassowrdHash string `bson:"password_hash,omitempty"`
|
||||
}
|
||||
|
||||
func (user *User) MongoId() primitive.ObjectID {
|
||||
|
||||
if user.Id.IsZero() {
|
||||
now := time.Now()
|
||||
|
||||
user.Id = primitive.NewObjectIDFromTimestamp(now)
|
||||
}
|
||||
|
||||
return user.Id
|
||||
}
|
||||
|
||||
func (user *User) UpdateObjectInfo() {
|
||||
now := time.Now()
|
||||
if user.CommonFields == nil {
|
||||
user.CommonFields = new(CommonFields)
|
||||
user.EntityType = USER_TYPE
|
||||
user.CreatedAt = now
|
||||
}
|
||||
user.UpdatedAt = now
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const VENDOR_ACCOUNT_TYPE = "vendor_account"
|
||||
|
||||
const (
|
||||
YOUTUBE_VENDOR_NAME = "youtube"
|
||||
PCO_VENDOR_NAME = "pco"
|
||||
)
|
||||
|
||||
type VendorAccount struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (va *VendorAccount) MongoId() primitive.ObjectID {
|
||||
if va.Id.IsZero() {
|
||||
now := time.Now()
|
||||
va.Id = primitive.NewObjectIDFromTimestamp(now)
|
||||
}
|
||||
|
||||
return va.Id
|
||||
}
|
||||
|
||||
func (va *VendorAccount) UpdateObjectInfo() {
|
||||
now := time.Now()
|
||||
if va.CommonFields == nil {
|
||||
va.CommonFields = new(CommonFields)
|
||||
va.EntityType = VENDOR_ACCOUNT_TYPE
|
||||
va.CreatedAt = now
|
||||
}
|
||||
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,37 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"google.golang.org/api/youtube/v3"
|
||||
)
|
||||
|
||||
const YOUTUBE_BROADCAST_TYPE = "youtube_broadcast"
|
||||
|
||||
type YoutubeBroadcast struct {
|
||||
*CommonFields `bson:"obj_info"`
|
||||
Id primitive.ObjectID `bson:"_id,omitempty"`
|
||||
UserId primitive.ObjectID `bson:"user_id,omitempty"`
|
||||
CorrelationId string `bson:"correlation_id,omitempty"`
|
||||
Details *youtube.LiveBroadcast `bson:"details,omitempty"`
|
||||
}
|
||||
|
||||
func (obj *YoutubeBroadcast) MongoId() primitive.ObjectID {
|
||||
if obj.Id.IsZero() {
|
||||
now := time.Now()
|
||||
obj.Id = primitive.NewObjectIDFromTimestamp(now)
|
||||
}
|
||||
|
||||
return obj.Id
|
||||
}
|
||||
|
||||
func (obj *YoutubeBroadcast) UpdateObjectInfo() {
|
||||
now := time.Now()
|
||||
if obj.CommonFields == nil {
|
||||
obj.CommonFields = new(CommonFields)
|
||||
obj.EntityType = YOUTUBE_BROADCAST_TYPE
|
||||
obj.CreatedAt = now
|
||||
}
|
||||
obj.UpdatedAt = now
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
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"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// using userId and event string return PCO Subscriptions saved to the DB
|
||||
func (db *DB) FindPcoSubscriptionForUser(userId primitive.ObjectID, eventName string) (*models.PcoSubscription, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.FindOne()
|
||||
res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.PCO_SUBSCRIPTION_TYPE, "details.name": eventName}, opts)
|
||||
|
||||
if res.Err() != nil {
|
||||
if res.Err() == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, res.Err()
|
||||
}
|
||||
|
||||
subscription := &models.PcoSubscription{}
|
||||
err := res.Decode(subscription)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return subscription, nil
|
||||
}
|
||||
|
||||
// Okay so learned something here. Interfaces are determined implemented for the type a method is related to.
|
||||
// This function is not implemented for DB it is implemented for *DB and that is important
|
||||
func (db *DB) SaveSubscriptionsForUser(userId primitive.ObjectID, subscriptions ...webhooks.Subscription) error {
|
||||
mods := make([]*models.PcoSubscription, 0, len(subscriptions))
|
||||
for _, sub := range subscriptions {
|
||||
mods = append(mods, &models.PcoSubscription{
|
||||
UserId: userId,
|
||||
Details: &sub,
|
||||
})
|
||||
}
|
||||
|
||||
return saveModels(db, mods...)
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"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}
|
||||
}
|
||||
|
||||
// Returns refreshed token from vendor account
|
||||
// Not threadsafe, please wrap in a oauth2.RefreshToken
|
||||
func (ts *VendorTokenSource) Token() (*oauth2.Token, error) {
|
||||
conf := config.Config()
|
||||
|
||||
//get locking collection
|
||||
col := ts.db.client.Database(conf.Mongo.LockDb).Collection(conf.Mongo.LockCol)
|
||||
|
||||
//Define lock
|
||||
token_lock := &models.TokenLock{
|
||||
VendorId: ts.vendor.MongoId(),
|
||||
TokenId: ts.vendor.OauthCredentials.RefreshToken,
|
||||
Refreshed: false,
|
||||
}
|
||||
//Don't forget to create the mongo id
|
||||
token_lock.MongoId()
|
||||
token_lock.UpdateObjectInfo()
|
||||
|
||||
//try and aquire lock
|
||||
opts := options.InsertOne()
|
||||
_, err := col.InsertOne(context.Background(), token_lock, opts)
|
||||
if err != nil {
|
||||
//If we didn't get the lock. Wait until whoever did refreshed the token
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
err = ts.waitForToken(token_lock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//get new vendorAccount
|
||||
va, err := ts.db.FindVendorAccountById(ts.vendor.MongoId())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//re-assign vendor account. Let go garbage collector handle the rest
|
||||
ts.vendor = va
|
||||
return ts.vendor.Token(), nil
|
||||
}
|
||||
//other error return nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Refresh token we have the lock
|
||||
token, err := oauth2.RefreshToken(context.Background(), conf.Vendors[ts.vendor.Name].OauthConfig(), ts.vendor.OauthCredentials.RefreshToken)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
//update vendor
|
||||
ts.vendor.OauthCredentials.RefreshToken = token.RefreshToken
|
||||
|
||||
//save vendor
|
||||
err = ts.db.SaveModel(ts.vendor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//release lock
|
||||
updateOpts := options.Update()
|
||||
_, err = col.UpdateByID(context.Background(), token_lock.MongoId(), bson.M{"$set": bson.M{"refreshed": true}}, updateOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Allow us to check for kind of error at the end
|
||||
var TokenWaitExpired error = errors.New("Waiting for token to refresh took too long")
|
||||
|
||||
// Used to extract the token lock that was updated when the change stream alerts
|
||||
type tokenLockChangeEvent struct {
|
||||
TokenLock *models.TokenLock `bson:"fullDocument"`
|
||||
}
|
||||
|
||||
func (ts *VendorTokenSource) waitForToken(tl *models.TokenLock) error {
|
||||
conf := config.Config()
|
||||
|
||||
//get locking collection
|
||||
col := ts.db.client.Database(conf.Mongo.LockDb).Collection(conf.Mongo.LockCol)
|
||||
|
||||
//Define timeoutfunction
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancelFunc()
|
||||
|
||||
//Get change stream that looks for our token lock
|
||||
opts := options.ChangeStream().SetFullDocument(options.UpdateLookup)
|
||||
changeStream, err := col.Watch(ctx, mongo.Pipeline{
|
||||
{{Key: "$match", Value: bson.D{{Key: "fullDocument.vendor_id", Value: tl.VendorId}, {Key: "fullDocument.token_id", Value: tl.TokenId}}}},
|
||||
}, opts)
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
}
|
||||
defer changeStream.Close(context.Background())
|
||||
|
||||
for changeStream.Next(ctx) {
|
||||
var tl_event tokenLockChangeEvent
|
||||
|
||||
err := changeStream.Decode(&tl_event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tl_event.TokenLock.Refreshed {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//We waited to long check if its refreshed and carry on
|
||||
res := col.FindOne(context.Background(), bson.M{"token_id": tl.TokenId})
|
||||
if res.Err() != nil {
|
||||
return errors.Join(TokenWaitExpired, res.Err())
|
||||
}
|
||||
|
||||
err = res.Decode(tl)
|
||||
if err != nil {
|
||||
return errors.Join(TokenWaitExpired, res.Err())
|
||||
}
|
||||
|
||||
if tl.Refreshed {
|
||||
return nil
|
||||
}
|
||||
|
||||
return TokenWaitExpired
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func newConf(url string) *oauth2.Config {
|
||||
return &oauth2.Config{
|
||||
ClientID: "CLIENT_ID",
|
||||
ClientSecret: "CLIENT_SECRET",
|
||||
RedirectURL: "REDIRECT_URL",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: url + "/auth",
|
||||
TokenURL: url + "/token",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToken(t *testing.T) {
|
||||
config.Init()
|
||||
conf := config.Config()
|
||||
|
||||
client1, err := NewClient(conf.Mongo.Uri)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client2, err := NewClient(conf.Mongo.Uri)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
count := 0
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token":"ACCESS_TOKEN", "scope": "user", "token_type": "bearer", "refresh_token": "NEW_REFRESH_TOKEN"}`))
|
||||
count += 1
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
conf.Vendors["test"].AuthUri = fmt.Sprintf("%s/auth", ts.URL)
|
||||
conf.Vendors["test"].TokenUri = fmt.Sprintf("%s/auth", ts.URL)
|
||||
//tkr := conf.TokenSource(context.Background(), &oauth2.Token{RefreshToken: "OLD_REFRESH_TOKEN"})
|
||||
id, err := primitive.ObjectIDFromHex("6560ecc57567b3806c5dbaf3")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
va1, err := client1.FindVendorAccountById(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
va2, err := client2.FindVendorAccountById(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tkr1 := client1.NewVendorTokenSource(va1)
|
||||
tkr2 := client2.NewVendorTokenSource(va2)
|
||||
|
||||
var tk1 *oauth2.Token
|
||||
var tk2 *oauth2.Token
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(2)
|
||||
|
||||
go func(tkr oauth2.TokenSource, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
var err error
|
||||
tk1, err = tkr.Token()
|
||||
if err != nil {
|
||||
t.Errorf("got err = %v; want none", err)
|
||||
return
|
||||
}
|
||||
}(tkr1, wg)
|
||||
|
||||
go func(tkr oauth2.TokenSource, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
var err error
|
||||
tk2, err = tkr.Token()
|
||||
if err != nil {
|
||||
t.Errorf("got err = %v; want none", err)
|
||||
return
|
||||
}
|
||||
}(tkr2, wg)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if count != 1 {
|
||||
t.Fatalf("Count = %d. Should of only hit the endpoint once.", count)
|
||||
return
|
||||
}
|
||||
|
||||
if tk1.RefreshToken != tk2.RefreshToken && tk1.RefreshToken != "NEW_REFRESH_TOKEN" {
|
||||
t.Fatalf("%s != %s != NEW_REFRESH_TOKN", tk1.RefreshToken, tk2.RefreshToken)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
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/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// seraches for a single user by email address
|
||||
func (db *DB) FindUserByEmail(email string) (*models.User, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.FindOne()
|
||||
res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"email": email, "obj_info.ent": models.USER_TYPE}, opts)
|
||||
|
||||
if res.Err() != nil {
|
||||
if res.Err() == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, res.Err()
|
||||
}
|
||||
|
||||
user := &models.User{}
|
||||
err := res.Decode(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// find user by its unique id
|
||||
func (db *DB) FindUserById(id string) (*models.User, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.FindOne()
|
||||
objId, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"_id": objId, "obj_info.ent": models.USER_TYPE}, opts)
|
||||
|
||||
if res.Err() != nil {
|
||||
if res.Err() == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, res.Err()
|
||||
}
|
||||
|
||||
user := &models.User{}
|
||||
err = res.Decode(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// returns all users
|
||||
func (db *DB) FindAllUsers() ([]models.User, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.Find()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"obj_info.ent": models.USER_TYPE}, opts)
|
||||
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, res.Err()
|
||||
}
|
||||
|
||||
users := []models.User{}
|
||||
err = res.All(context.Background(), users)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
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/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// return all vendor accounts for a user
|
||||
func (db *DB) FindAllVendorAccountsByUser(userId primitive.ObjectID) ([]models.VendorAccount, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.Find()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.VENDOR_ACCOUNT_TYPE}, opts)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vendors := []models.VendorAccount{}
|
||||
err = res.All(context.Background(), &vendors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vendors, nil
|
||||
}
|
||||
|
||||
// find vendor for user by name
|
||||
func (db *DB) FindVendorAccountByUser(userId primitive.ObjectID, name string) (*models.VendorAccount, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.FindOne()
|
||||
res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"user_id": userId, "name": name, "obj_info.ent": models.VENDOR_ACCOUNT_TYPE}, opts)
|
||||
if res.Err() != nil {
|
||||
if res.Err() == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, res.Err()
|
||||
}
|
||||
|
||||
vendor := &models.VendorAccount{}
|
||||
err := res.Decode(vendor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vendor, nil
|
||||
}
|
||||
|
||||
// find vendoraccount by its unique id
|
||||
func (db *DB) FindVendorAccountById(vendorId primitive.ObjectID) (*models.VendorAccount, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.FindOne()
|
||||
res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"_id": vendorId, "obj_info.ent": models.VENDOR_ACCOUNT_TYPE}, opts)
|
||||
if res.Err() != nil {
|
||||
if res.Err() == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, res.Err()
|
||||
}
|
||||
|
||||
vendor := &models.VendorAccount{}
|
||||
err := res.Decode(vendor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vendor, nil
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
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/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func (db *DB) FindAllBroadcastsByCorrelationId(userId primitive.ObjectID, correlationId string) ([]models.YoutubeBroadcast, error) {
|
||||
conf := config.Config()
|
||||
|
||||
opts := options.Find()
|
||||
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"user_id": userId, "obj_info.ent": models.YOUTUBE_BROADCAST_TYPE, "correlation_id": correlationId}, opts)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vendors := []models.YoutubeBroadcast{}
|
||||
err = res.All(context.Background(), &vendors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vendors, nil
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
mongo1:
|
||||
image: mongo:latest
|
||||
container_name: mongo1
|
||||
command: ["--replSet", "my-replica-set", "--bind_ip_all", "--port", "30001"]
|
||||
volumes:
|
||||
- /etc/capstone/db1:/data/db
|
||||
ports:
|
||||
- 30001:30001
|
||||
healthcheck:
|
||||
test: test $$(echo "rs.initiate({_id:'my-replica-set',members:[{_id:0,host:\"mongo1:30001\"},{_id:1,host:\"mongo2:30002\"},{_id:2,host:\"mongo3:30003\"}]}).ok || rs.status().ok" | mongosh --port 30001 --quiet) -eq 1
|
||||
interval: 10s
|
||||
start_period: 30s
|
||||
|
||||
mongo2:
|
||||
image: mongo:latest
|
||||
container_name: mongo2
|
||||
command: ["--replSet", "my-replica-set", "--bind_ip_all", "--port", "30002"]
|
||||
volumes:
|
||||
- /etc/capstone/db2:/data/db
|
||||
ports:
|
||||
- 30002:30002
|
||||
|
||||
mongo3:
|
||||
image: mongo:latest
|
||||
container_name: mongo3
|
||||
command: ["--replSet", "my-replica-set", "--bind_ip_all", "--port", "30003"]
|
||||
volumes:
|
||||
- /etc/capstone/db3:/data/db
|
||||
ports:
|
||||
- 30003:30003
|
|
@ -0,0 +1,90 @@
|
|||
module git.preston-baxter.com/Preston_PLB/capstone/frontend-service
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.4
|
||||
|
||||
require (
|
||||
git.preston-baxter.com/Preston_PLB/capstone/webhook-service v0.0.0-00010101000000-000000000000
|
||||
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/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.17.0
|
||||
go.mongodb.org/mongo-driver v1.13.0
|
||||
golang.org/x/crypto v0.15.0
|
||||
golang.org/x/oauth2 v0.14.0
|
||||
google.golang.org/api v0.150.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
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/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/jsonapi v1.0.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // 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/miekg/dns v1.1.57 // 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/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/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.opencensus.io v0.24.0 // 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/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
||||
google.golang.org/grpc v1.59.0 // 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/webhook-service => ../service
|
||||
|
||||
replace golang.org/x/oauth2 => ../libs/oauth2
|
||||
|
||||
replace github.com/google/jsonapi => ../libs/jsonapi
|
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/controllers"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Init()
|
||||
r := gin.Default()
|
||||
|
||||
controllers.BuildRouter(r)
|
||||
|
||||
var addr string
|
||||
if port := os.Getenv("PORT"); port != "" {
|
||||
addr = fmt.Sprintf("0.0.0.0:%s", port)
|
||||
} else {
|
||||
addr = "0.0.0.0:8080"
|
||||
}
|
||||
|
||||
err := r.Run(addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@material-tailwind/html": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.3.2"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 7.6 KiB |
|
@ -0,0 +1,10 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
const withMT = require("@material-tailwind/html/utils/withMT");
|
||||
|
||||
module.exports = withMT({
|
||||
content: ["./templates/*.templ"],
|
||||
plugins: [],
|
||||
theme: {
|
||||
extend: {},
|
||||
}
|
||||
})
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -0,0 +1,556 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
)
|
||||
|
||||
func hasVendor(name string, vendors []models.VendorAccount) bool {
|
||||
for _, vendor := range vendors {
|
||||
if vendor.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func hasPco(vendors []models.VendorAccount) bool {
|
||||
return hasVendor(models.PCO_VENDOR_NAME, vendors)
|
||||
}
|
||||
|
||||
func hasYoutube(vendors []models.VendorAccount) bool {
|
||||
return hasVendor(models.YOUTUBE_VENDOR_NAME, vendors)
|
||||
}
|
||||
|
||||
templ DashboardPage(user *models.User, vendorAccounts []models.VendorAccount, actionMappings []models.ActionMapping) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@Head("Dashboard")
|
||||
<body class="text-blueGray-700 antialiased">
|
||||
<div
|
||||
id="add-action-modal"
|
||||
aria-hidden="false"
|
||||
tabindex="-1"
|
||||
class="transition-all hidden"
|
||||
></div>
|
||||
<div id="root" class="h-screen overflow-scroll">
|
||||
@DashboardNav(user)
|
||||
@DashboardContent(user, vendorAccounts, actionMappings)
|
||||
</div>
|
||||
@DashboardFooter()
|
||||
</body>
|
||||
@DashboardScript()
|
||||
@toggleDropdown()
|
||||
</html>
|
||||
}
|
||||
|
||||
templ DashboardNav(user *models.User) {
|
||||
<nav
|
||||
class="md:left-0 md:block md:fixed md:top-0 md:bottom-0 md:overflow-y-auto md:flex-row md:flex-nowrap md:overflow-hidden shadow-xl bg-white flex flex-wrap items-center justify-between relative md:w-64 z-10 py-4 px-6"
|
||||
>
|
||||
<div
|
||||
class="md:flex-col md:items-stretch md:min-h-full md:flex-nowrap px-0 flex flex-wrap items-center justify-between w-full mx-auto"
|
||||
>
|
||||
<button
|
||||
class="cursor-pointer text-black opacity-50 md:hidden px-3 py-1 text-xl leading-none bg-transparent rounded border border-solid border-transparent"
|
||||
type="button"
|
||||
onclick="toggleNavbar('example-collapse-sidebar')"
|
||||
>
|
||||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<a
|
||||
class="md:block text-left md:pb-2 text-blueGray-600 mr-0 inline-block whitespace-nowrap text-sm uppercase font-bold p-4 px-0"
|
||||
href="javascript:void(0)"
|
||||
>
|
||||
{ user.Email }
|
||||
</a>
|
||||
<div
|
||||
class="md:flex md:flex-col md:items-stretch md:opacity-100 md:relative md:mt-4 md:shadow-none shadow absolute top-0 left-0 right-0 z-40 overflow-y-auto overflow-x-hidden h-auto items-center flex-1 rounded hidden"
|
||||
id="example-collapse-sidebar"
|
||||
>
|
||||
<div
|
||||
class="md:min-w-full md:hidden block pb-4 mb-4 border-b border-solid border-blueGray-200"
|
||||
>
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-6/12">
|
||||
<a
|
||||
class="md:block text-left md:pb-2 text-blueGray-600 mr-0 inline-block whitespace-nowrap text-sm uppercase font-bold p-4 px-0"
|
||||
href="javascript:void(0)"
|
||||
>
|
||||
{ user.Email }
|
||||
</a>
|
||||
</div>
|
||||
<div class="w-6/12 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer text-black opacity-50 md:hidden px-3 py-1 text-xl leading-none bg-transparent rounded border border-solid border-transparent"
|
||||
onclick="toggleNavbar('example-collapse-sidebar')"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form class="mt-6 mb-4 md:hidden">
|
||||
<div class="mb-3 pt-0">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
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>
|
||||
<ul class="md:flex-col md:min-w-full flex flex-col list-none">
|
||||
@DashboardNavItem("fa-tv", "Dashboard", "/dashboard", true)
|
||||
@DashboardNavItem("fa-chart-bar", "Events", "/dashboard/events", true)
|
||||
<hr/>
|
||||
@DashboardNavItem("fa-newspaper", "Home Page", "/", true)
|
||||
@DashboardNavItem("fa-user-circle", "Profile (SOON)", "#", false)
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
||||
templ DashboardNavItem(icon, name, link string, enabled bool) {
|
||||
<li class="items-center">
|
||||
<a
|
||||
if enabled {
|
||||
class="text-blue-500 hover:text-blue-600 text-xs uppercase py-3 font-bold block"
|
||||
href={ templ.URL(link) }
|
||||
} else {
|
||||
class="text-blueGray-300 text-xs uppercase py-3 font-bold block"
|
||||
href="#"
|
||||
}
|
||||
href="{ link }"
|
||||
>
|
||||
<i
|
||||
if enabled {
|
||||
class={ fmt.Sprintf("fas %s opacity-75 mr-2 text-sm", icon) }
|
||||
} else {
|
||||
class={ fmt.Sprintf("fas %s text-blueGray-300 mr-2 text-sm", icon) }
|
||||
}
|
||||
></i>{ name }
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
//Break this up
|
||||
|
||||
templ DashboardContentNav(user *models.User) {
|
||||
<nav
|
||||
class="absolute top-0 left-0 w-full z-10 bg-transparent md:flex-row md:flex-nowrap md:justify-start flex items-center p-4"
|
||||
>
|
||||
<div
|
||||
class="w-full mx-autp items-center flex justify-between md:flex-nowrap flex-wrap md:px-10 px-4"
|
||||
>
|
||||
<a
|
||||
class="text-white text-sm uppercase hidden lg:inline-block font-semibold"
|
||||
href="./index.html"
|
||||
>Capstone</a>
|
||||
<form
|
||||
class="hidden flex-row flex-wrap items-center lg:ml-auto mr-3"
|
||||
>
|
||||
<div class="relative flex w-full flex-wrap items-stretch">
|
||||
<span
|
||||
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 rounded text-sm shadow outline-none focus:outline-none focus:ring w-full pl-10"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<ul
|
||||
class="hidden flex-col md:flex-row list-none items-center hidden md:flex"
|
||||
>
|
||||
<a class="hidden text-blueGray-500 block" href="#pablo" onclick="openDropdown(event,'user-dropdown')">
|
||||
<div class="items-center flex">
|
||||
<span
|
||||
class="w-12 h-12 text-sm text-white bg-blueGray-200 inline-flex items-center justify-center rounded-full"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
class="hidden bg-white text-base z-50 float-left py-2 list-none text-left rounded shadow-lg mt-1"
|
||||
style="min-width: 12rem;"
|
||||
id="user-dropdown"
|
||||
>
|
||||
<a
|
||||
href="#pablo"
|
||||
class="text-sm py-2 px-4 font-normal block w-full whitespace-nowrap bg-transparent text-blueGray-700"
|
||||
>Action</a><a
|
||||
href="#pablo"
|
||||
class="text-sm py-2 px-4 font-normal block w-full whitespace-nowrap bg-transparent text-blueGray-700"
|
||||
>Another action</a><a
|
||||
href="#pablo"
|
||||
class="text-sm py-2 px-4 font-normal block w-full whitespace-nowrap bg-transparent text-blueGray-700"
|
||||
>Something else here</a>
|
||||
<div class="h-0 my-2 border border-solid border-blueGray-100"></div>
|
||||
<a
|
||||
href="#pablo"
|
||||
class="text-sm py-2 px-4 font-normal block w-full whitespace-nowrap bg-transparent text-blueGray-700"
|
||||
>Seprated link</a>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
||||
templ DashboardCardLoader(kind string) {
|
||||
<div class="w-full lg:w-6/12 xl:w-6/12 px-4" hx-get={ fmt.Sprintf("/dashboard/components/metric_card?metric=%s", kind) } hx-trigger="load" hx-swap="outerHTML">
|
||||
<div class="relative flex flex-col min-w-0 break-words bg-white rounded mb-6 xl:mb-0 shadow-lg opacity-50">
|
||||
<div class="flex-auto p-4">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="relative w-full pr-4 max-w-full flex-grow flex-1">
|
||||
<h5 class="text-blueGray-400 uppercase font-bold text-xs">
|
||||
---
|
||||
</h5>
|
||||
<span class="font-semibold text-xl text-blueGray-700">
|
||||
---
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-auto pl-4 flex-initial">
|
||||
<div class="text-white p-3 text-center inline-flex items-center justify-center w-12 h-12 shadow-lg rounded-full bg-blue-300">
|
||||
<i class="far fa-chart-bar"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-blueGray-400 mt-4">
|
||||
<span class="text-emerald-500 mr-2">
|
||||
---
|
||||
</span>
|
||||
<span class="whitespace-nowrap">
|
||||
---
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@spinnerCentered()
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DashboardCard(title, primaryVal, secondaryVal, subtitle string) {
|
||||
<div class="w-full lg:w-6/12 xl:w-6/12 px-4">
|
||||
<div class="relative flex flex-col min-w-0 break-words bg-white rounded mb-6 xl:mb-0 shadow-lg">
|
||||
<div class="flex-auto p-4">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="relative w-full pr-4 max-w-full flex-grow flex-1">
|
||||
<h5 class="text-blueGray-400 uppercase font-bold text-xs">
|
||||
{ title }
|
||||
</h5>
|
||||
<span class="font-semibold text-xl text-blueGray-700">
|
||||
{ primaryVal }
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative w-auto pl-4 flex-initial">
|
||||
<div class="text-white p-3 text-center inline-flex items-center justify-center w-12 h-12 shadow-lg rounded-full bg-blue-300">
|
||||
<i class="far fa-chart-bar"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-blueGray-400 mt-4">
|
||||
<span class="text-emerald-500 mr-2">
|
||||
{ secondaryVal }
|
||||
</span>
|
||||
<span class="whitespace-nowrap">
|
||||
{ subtitle }
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DashboardVendorDropDown() {
|
||||
<div class="flex flex-wrap float-right">
|
||||
<div class="w-full sm:w-6/12 md:w-4/12 px-4">
|
||||
<div class="relative inline-flex align-middle w-full">
|
||||
<button class="text-white font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 bg-blueGray-500 ease-linear transition-all duration-150" type="button" onclick="openDropdown(event,'dropdown-id')">
|
||||
+
|
||||
</button>
|
||||
<div class="hidden bg-white text-base z-50 float-left py-2 list-none text-left rounded shadow-lg mt-1" style="min-width:12rem" id="dropdown-id">
|
||||
<form action="/vendor/youtube/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">
|
||||
Youtube
|
||||
</button>
|
||||
</form>
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DashboardVendorWidget(vendors []models.VendorAccount) {
|
||||
<div class="w-full xl:w-8/12 mb-12 xl:mb-0 px-4">
|
||||
<div class="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||
<div class="rounded-t mb-0 px-4 py-3 border-0">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div class="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||
<h3 class="font-semibold text-base text-blueGray-700">
|
||||
Vendors
|
||||
</h3>
|
||||
</div>
|
||||
<div class="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||
@DashboardVendorDropDown()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block w-full overflow-x-auto">
|
||||
<!-- Projects table -->
|
||||
<table class="items-center w-full bg-transparent border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
Name
|
||||
</th>
|
||||
<th class="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
Status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
if len(vendors) == 0 {
|
||||
<tr>
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
No accounts are available. Click + to add one
|
||||
</th>
|
||||
</tr>
|
||||
} else {
|
||||
for _, vendor := range vendors {
|
||||
<tr>
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
{ 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 != "" {
|
||||
Active
|
||||
} else {
|
||||
<button>Log in</button>
|
||||
}
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DashboardActionModalForm(vendors []models.VendorAccount) {
|
||||
<div class="relative p-6 flex-auto">
|
||||
<form class="space-y-4 text-gray-700" action="/dashboard/action/add" method="POST">
|
||||
<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" name="source">
|
||||
if hasPco(vendors) {
|
||||
<option value="pco.plan">Plan</option>
|
||||
<option value="calendar" disabled>Calendar</option>
|
||||
} else {
|
||||
<option value="nil">None Available</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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" name="action">
|
||||
if hasYoutube(vendors) {
|
||||
<option value="youtube.livestream">Livestream</option>
|
||||
<option value="video" disabled>Video</option>
|
||||
} else {
|
||||
<option value="nil">None Available</option>
|
||||
}
|
||||
</select>
|
||||
</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-300 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>
|
||||
}
|
||||
|
||||
templ DashboardActionModal(vendors []models.VendorAccount) {
|
||||
<div class="transition-all ease-in-out overflow-x-hidden overflow-y-auto fixed flex inset-0 z-50 outline-none focus:outline-none justify-center items-center" id="add-action-modal">
|
||||
<div class="relative w-auto my-6 mx-auto max-w-3xl">
|
||||
<!--content-->
|
||||
<div class="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||
<!--header-->
|
||||
<div class="flex items-start justify-between p-5 border-b border-solid border-blueGray-200 rounded-t">
|
||||
<h3 class="text-3xl font-semibold">
|
||||
Modal Title
|
||||
</h3>
|
||||
<button class="p-1 ml-auto bg-transparent border-0 float-right text-3xl leading-none font-semibold outline-none focus:outline-none" onclick="toggleModal('add-action-modal')">
|
||||
<span class="bg-transparent text-gray-500 h-6 w-6 text-2xl block outline-none focus:outline-none">
|
||||
×
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!--body-->
|
||||
@DashboardActionModalForm(vendors)
|
||||
<!--footer-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opacity-25 fixed flex inset-0 z-40 bg-black" id="add-action-modal-backdrop"></div>
|
||||
}
|
||||
|
||||
templ DashboardActionDropDown() {
|
||||
<button
|
||||
hx-get="/dashboard/forms/addAction"
|
||||
hx-target="#add-action-modal"
|
||||
hx-swap="outerHTML"
|
||||
class="bg-blue-500 text-white active:bg-blue-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"
|
||||
>
|
||||
Add Action
|
||||
</button>
|
||||
}
|
||||
|
||||
templ DashboardActionEditButton(action *models.ActionMapping) {
|
||||
}
|
||||
|
||||
templ DashboardActionsWidget(actions []models.ActionMapping) {
|
||||
<div class="w-full xl:w-8/12 mb-12 xl:mb-0 px-4">
|
||||
<div class="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||
<div class="rounded-t mb-0 px-4 py-3 border-0">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div class="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||
<h3 class="font-semibold text-base text-blueGray-700">
|
||||
Vendors
|
||||
</h3>
|
||||
</div>
|
||||
<div class="relative w-full px-4 max-w-full flex-grow flex-1 text-right">
|
||||
@DashboardActionDropDown()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block w-full overflow-x-auto">
|
||||
<!-- Projects table -->
|
||||
<table class="items-center w-full bg-transparent border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
Id
|
||||
</th>
|
||||
<th class="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
Event Source
|
||||
</th>
|
||||
<th class="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
Action Destination
|
||||
</th>
|
||||
<th class="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
if len(actions) == 0 {
|
||||
<tr>
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
No actions are available. Click + to add one
|
||||
</th>
|
||||
</tr>
|
||||
} else {
|
||||
for index, action := range actions {
|
||||
<tr>
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
{ strconv.Itoa(index) }
|
||||
</th>
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
{ action.SourceEvent.Key }
|
||||
</th>
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
{ action.Action.VendorName }
|
||||
</th>
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left"> { action.Action.Type }
|
||||
</th>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DashboardContent(user *models.User, vendorAccounts []models.VendorAccount, actions []models.ActionMapping) {
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div class="relative md:ml-64 bg-blueGray-50 h-full">
|
||||
@DashboardContentNav(user)
|
||||
<!-- Header -->
|
||||
<div class="relative bg-blue-600 md:pt-32 pb-32 pt-12">
|
||||
<div class="px-4 md:px-10 mx-auto w-full">
|
||||
<div>
|
||||
<!-- Card stats -->
|
||||
<div class="flex flex-wrap">
|
||||
@DashboardCardLoader("events_received")
|
||||
@DashboardCardLoader("streams_scheduled")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 md:px-10 mx-auto w-full -m-24">
|
||||
<div class="flex flex-wrap">
|
||||
@DashboardVendorWidget(vendorAccounts)
|
||||
</div>
|
||||
<div class="flex flex-wrap">
|
||||
@DashboardActionsWidget(actions)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ DashboardScript() {
|
||||
<script type="text/javascript">
|
||||
function toggleModal(modalID) {
|
||||
document.getElementById(modalID).classList.toggle("hidden");
|
||||
document.getElementById(modalID + "-backdrop").classList.toggle("hidden");
|
||||
document.getElementById(modalID).classList.toggle("flex");
|
||||
document.getElementById(modalID + "-backdrop").classList.toggle("flex");
|
||||
}
|
||||
|
||||
function toggleNavbar(collapseID) {
|
||||
document.getElementById(collapseID).classList.toggle("hidden");
|
||||
document.getElementById(collapseID).classList.toggle("bg-white");
|
||||
document.getElementById(collapseID).classList.toggle("m-2");
|
||||
document.getElementById(collapseID).classList.toggle("py-3");
|
||||
document.getElementById(collapseID).classList.toggle("px-6");
|
||||
}
|
||||
/* Function for dropdowns */
|
||||
function openDropdown(event, dropdownID) {
|
||||
let element = event.target;
|
||||
while (element.nodeName !== "A") {
|
||||
element = element.parentNode;
|
||||
}
|
||||
var popper = Popper.createPopper(element, document.getElementById(dropdownID), {
|
||||
placement: "bottom-end"
|
||||
});
|
||||
document.getElementById(dropdownID).classList.toggle("hidden");
|
||||
document.getElementById(dropdownID).classList.toggle("relative");
|
||||
}
|
||||
</script>
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
)
|
||||
|
||||
type TableData [][]string
|
||||
|
||||
templ EventsPage(user *models.User) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@Head("Events")
|
||||
<body class="text-blueGray-700 antialiased">
|
||||
<div
|
||||
id="add-action-modal"
|
||||
aria-hidden="false"
|
||||
tabindex="-1"
|
||||
class="transition-all hidden"
|
||||
></div>
|
||||
<div id="root" class="h-screen overflow-scroll">
|
||||
@DashboardNav(user)
|
||||
@EventContent(user)
|
||||
</div>
|
||||
@DashboardFooter()
|
||||
</body>
|
||||
@DashboardScript()
|
||||
@toggleDropdown()
|
||||
@updateSearchScript()
|
||||
</html>
|
||||
}
|
||||
|
||||
var sampleData = [][]string{{"head 1", "head 2"}, {"row 1", "row 1"}, {"row 2", "row 2"}}
|
||||
var blankData = [][]string{{"head 1", "head 2"}}
|
||||
|
||||
templ EventContent(user *models.User) {
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div class="relative md:ml-64 bg-blueGray-50">
|
||||
@DashboardContentNav(user)
|
||||
<!-- Header -->
|
||||
<div class="relative bg-blue-600 md:pt-32 pb-32 pt-12">
|
||||
<div class="px-4 md:px-10 mx-auto w-1/4 h-3/4"></div>
|
||||
<div class="px-4 md:px-10 mx-auto w-2/4 h-3/4">
|
||||
<div class="relative flex flex-col min-w-0 break-words bg-white rounded mb-6 xl:mb-0 shadow-lg">
|
||||
<div class="flex-auto p-4">
|
||||
<div
|
||||
class="flex-row flex-wrap items-center lg:ml-auto mr-3"
|
||||
>
|
||||
<div class="relative flex w-full flex-wrap items-stretch">
|
||||
<span
|
||||
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
|
||||
id="search_bar"
|
||||
type="text"
|
||||
onkeyup="onSearchKeyUp()"
|
||||
placeholder="Search here..."
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 md:px-10 mx-auto w-1/4 h-3/4"></div>
|
||||
</div>
|
||||
<div class="px-4 md:px-10 mx-auto w-full -m-24">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="w-full xl:w-6/12 mb-12 xl:mb-0 px-4">
|
||||
@EventTableWidget("Events", "events_for_user")
|
||||
</div>
|
||||
<div class="w-full xl:w-6/12 mb-12 xl:mb-0 px-4">
|
||||
@EventTableWidget("Actions", "actions_for_user")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ EventTableWidget(title, table_name string) {
|
||||
<div class="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded">
|
||||
<div class="rounded-t mb-0 px-4 py-3 border-0">
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div class="relative w-full px-4 max-w-full flex-grow flex-1">
|
||||
<h3 class="font-semibold text-base text-blueGray-700">
|
||||
{ title }
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block w-full overflow-x-auto">
|
||||
<!-- Projects table -->
|
||||
@EventTableDataLoader(table_name)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ EventTableDataLoader(table_name string) {
|
||||
<div class="relative" hx-get={ fmt.Sprintf("/dashboard/events/components/table_data?table_name=%s", table_name) } hx-swap="outerHTML" hx-trigger="load">
|
||||
@EventTableDataLazy(table_name)
|
||||
@spinnerCentered()
|
||||
</div>
|
||||
}
|
||||
|
||||
//hx-get={ fmt.Sprintf("/dashboard/events/components/table_data?table_name=%s", table_name) }
|
||||
|
||||
templ EventTableDataLazy(table_name string) {
|
||||
<table class="items-center opacity-50 w-full bg-transparent border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 bg-blueGray-50 opacity-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
Lorem Ipsum
|
||||
</th>
|
||||
<th class="px-6 bg-blueGray-50 opacity-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
Lorem Ipsum
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="border-t-0 px-6 align-middle opacity-50 border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
No Data Available
|
||||
</th>
|
||||
<th class="border-t-0 px-6 align-middle opacity-50 border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
No Data Available
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
templ EventTableData(data TableData, table_name string) {
|
||||
<table class="items-center w-full bg-transparent border-collapse" hx-get={ fmt.Sprintf("/dashboard/events/components/table_data?table_name=%s", table_name) } hx-params="*" hx-trigger="search delay:200ms from:body">
|
||||
<thead>
|
||||
<tr>
|
||||
for _, header := range data[0] {
|
||||
<th class="px-6 bg-blueGray-50 text-blueGray-500 align-middle border border-solid border-blueGray-100 py-3 text-xs uppercase border-l-0 border-r-0 whitespace-nowrap font-semibold text-left">
|
||||
{ header }
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
if len(data) <= 1 {
|
||||
<tr>
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
No Data Available
|
||||
</th>
|
||||
</tr>
|
||||
} else {
|
||||
for _, row := range data[1:] {
|
||||
<tr>
|
||||
for _, item := range row {
|
||||
<th class="border-t-0 px-6 align-middle border-l-0 border-r-0 text-xs whitespace-nowrap p-4 text-left">
|
||||
{ item }
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
templ updateSearchScript() {
|
||||
<script>
|
||||
const searchEvent = new Event("search");
|
||||
|
||||
function onSearchKeyUp() {
|
||||
// console.log("keyup")
|
||||
document.body.dispatchEvent(searchEvent);
|
||||
}
|
||||
|
||||
document.body.addEventListener('htmx:configRequest', function(evt) {
|
||||
var query = document.getElementById("search_bar").value;
|
||||
evt.detail.parameters['filter'] = query // add a new parameter into the request
|
||||
});
|
||||
</script>
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package templates
|
||||
|
||||
templ LandingFooter() {
|
||||
<footer class="relative w-full bottom-0 bg-gray-900 pb-6">
|
||||
<div class="container mx-auto px-4">
|
||||
<hr class="mb-6 border-b-1 border-gray-700"/>
|
||||
<div
|
||||
class="flex flex-wrap items-center md:justify-between justify-center"
|
||||
>
|
||||
<div class="w-full md:w-4/12 px-4">
|
||||
<div class="text-sm text-white font-semibold py-1">
|
||||
Copyright © 2023
|
||||
<a
|
||||
href="https://git.preston-baxter.com"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold py-1"
|
||||
>Preston Baxter</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-8/12 px-4">
|
||||
<ul
|
||||
class="flex flex-wrap list-none md:justify-end justify-center"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://git.preston-baxter.com"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
|
||||
>Preston Baxter</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/about-us"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
|
||||
>About Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
|
||||
templ DashboardFooter() {
|
||||
<footer class="absolute w-full bottom-0 bg-gray-900 pb-6">
|
||||
<div class="md:ml-64 md:mr-16 px-4">
|
||||
<hr class="mb-6 border-b-1 border-gray-700"/>
|
||||
<div
|
||||
class="flex flex-wrap items-center md:justify-end justify-center"
|
||||
>
|
||||
<div class="w-full md:w-6/12 px-4">
|
||||
<div class="text-sm text-white font-semibold py-1">
|
||||
Copyright © 2023
|
||||
<a
|
||||
href="https://git.preston-baxter.com"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold py-1"
|
||||
>Preston Baxter</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-6/12 px-4">
|
||||
<ul
|
||||
class="flex flex-wrap list-none md:justify-end justify-center"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://git.preston-baxter.com"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
|
||||
>Preston Baxter</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/about-us"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
|
||||
>About Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
|
||||
templ LoginFooter() {
|
||||
<footer class="absolute w-full bottom-0 bg-gray-900 pb-6">
|
||||
<div class="md:mx-16 px-4">
|
||||
<hr class="mb-6 border-b-1 border-gray-700"/>
|
||||
<div
|
||||
class="flex flex-wrap items-center md:justify-end justify-center"
|
||||
>
|
||||
<div class="w-full md:w-6/12 px-4">
|
||||
<div class="text-sm text-white font-semibold py-1">
|
||||
Copyright © 2023
|
||||
<a
|
||||
href="https://git.preston-baxter.com"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold py-1"
|
||||
>Preston Baxter</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-6/12 px-4">
|
||||
<ul
|
||||
class="flex flex-wrap list-none md:justify-end justify-center"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href="https://git.preston-baxter.com"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
|
||||
>Preston Baxter</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/about-us"
|
||||
class="text-white hover:text-gray-400 text-sm font-semibold block py-1 px-3"
|
||||
>About Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
package templates
|
||||
|
||||
templ LandingContent() {
|
||||
<main>
|
||||
<div class="relative pt-16 pb-32 flex content-center items-center justify-center" style="min-height: 75vh;">
|
||||
<div
|
||||
class="absolute top-0 w-full h-full bg-center bg-cover"
|
||||
style="background-image: url("https://images.unsplash.com/photo-1522158637959-30385a09e0da?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1267&q=80");"
|
||||
>
|
||||
<span id="blackOverlay" class="w-full h-full absolute opacity-75 bg-black"></span>
|
||||
</div>
|
||||
<div class="container relative mx-auto">
|
||||
<div class="items-center flex flex-wrap">
|
||||
<div class="w-full lg:w-6/12 px-4 ml-auto mr-auto text-center">
|
||||
<div class="pr-12">
|
||||
<h1 class="text-white font-semibold text-5xl">
|
||||
Automate the things that take time
|
||||
</h1>
|
||||
<p class="mt-4 text-lg text-gray-300">
|
||||
Ministry has enough activities that take time. Automate
|
||||
the things that take you away from the great commision.
|
||||
Have more time to do what you are called to
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="top-auto bottom-0 left-0 right-0 w-full absolute pointer-events-none overflow-hidden"
|
||||
style="height: 70px;"
|
||||
>
|
||||
<svg
|
||||
class="absolute bottom-0 overflow-hidden"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="none"
|
||||
version="1.1"
|
||||
viewBox="0 0 2560 100"
|
||||
x="0"
|
||||
y="0"
|
||||
>
|
||||
<polygon class="text-gray-300 fill-current" points="2560 0 2560 100 0 100"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<section class="pb-20 bg-gray-300 -mt-24">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="lg:pt-12 pt-6 w-full md:w-4/12 px-4 text-center">
|
||||
<div
|
||||
class="relative flex flex-col min-w-0 break-words bg-white w-full mb-8 shadow-lg rounded-lg"
|
||||
>
|
||||
<div class="px-4 py-5 flex-auto">
|
||||
<div
|
||||
class="text-white p-3 text-center inline-flex items-center justify-center w-12 h-12 mb-5 shadow-lg rounded-full bg-red-400"
|
||||
>
|
||||
<i class="fa-brands fa-youtube"></i>
|
||||
</div>
|
||||
<h6 class="text-xl font-semibold">Youtube Live streams</h6>
|
||||
<p class="mt-2 mb-4 text-gray-600">
|
||||
Automatically schedule youtube live streams. Never manage your broadcasts again
|
||||
with automated scheduling
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-4/12 px-4 text-center">
|
||||
<div
|
||||
class="relative flex flex-col min-w-0 break-words bg-white w-full mb-8 shadow-lg rounded-lg"
|
||||
>
|
||||
<div class="px-4 py-5 flex-auto">
|
||||
<div
|
||||
class="text-white p-3 text-center inline-flex items-center justify-center w-12 h-12 mb-5 shadow-lg rounded-full bg-blue-400"
|
||||
>
|
||||
<i class="fas fa-retweet"></i>
|
||||
</div>
|
||||
<h6 class="text-xl font-semibold">Connect it all</h6>
|
||||
<p class="mt-2 mb-4 text-gray-600">
|
||||
Connect the services that take time out of your day.
|
||||
Let us do the hard work of scheduling and managing them
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-6 w-full md:w-4/12 px-4 text-center">
|
||||
<div
|
||||
class="relative flex flex-col min-w-0 break-words bg-white w-full mb-8 shadow-lg rounded-lg"
|
||||
>
|
||||
<div class="px-4 py-5 flex-auto">
|
||||
<div
|
||||
class="text-white p-0 text-center inline-flex items-center justify-center w-12 h-12 mb-5 shadow-lg rounded-full bg-transparent"
|
||||
>
|
||||
<img class="" src="/static/app-icon-services-400.png"></img>
|
||||
</div>
|
||||
<h6 class="text-xl font-semibold">Planning Center</h6>
|
||||
<p class="mt-2 mb-4 text-gray-600">
|
||||
Link your planning center to automatically trigger events.
|
||||
Schedule live streams, and soon social media posts as well.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center mt-32">
|
||||
<div class="w-full md:w-5/12 px-4 mr-auto ml-auto">
|
||||
<div
|
||||
class="text-gray-600 p-3 text-center inline-flex items-center justify-center w-16 h-16 mb-6 shadow-lg rounded-full bg-gray-100"
|
||||
>
|
||||
<i class="fas fa-church text-xl"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl mb-2 font-semibold leading-normal">
|
||||
Built by church people for church people
|
||||
</h3>
|
||||
<p class="text-lg font-light leading-relaxed mt-4 mb-4 text-gray-700">
|
||||
With over half a decade of time spen as the technical director of
|
||||
my local church I know the things that take time away from the
|
||||
important things.
|
||||
</p>
|
||||
<p class="text-lg font-light leading-relaxed mt-0 mb-4 text-gray-700">
|
||||
I have built this to take the hassle out of last minute Planning center
|
||||
changes. No need to update your livestream, facebook posts, and instagram
|
||||
stories. We take care of that for you
|
||||
</p>
|
||||
<a
|
||||
href="https://www.creative-tim.com/learning-lab/tailwind-starter-kit#/presentation"
|
||||
class="font-bold text-gray-800 mt-8"
|
||||
>Check Ministry Auto Tools</a>
|
||||
</div>
|
||||
<div class="w-full md:w-4/12 px-4 mr-auto ml-auto">
|
||||
<div
|
||||
class="relative flex flex-col min-w-0 break-words bg-white w-full mb-6 shadow-lg rounded-lg bg-blue-600"
|
||||
>
|
||||
<img
|
||||
alt="..."
|
||||
src="https://images.unsplash.com/photo-1522202176988-66273c2fd55f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1051&q=80"
|
||||
class="w-full align-middle rounded-t-lg"
|
||||
/>
|
||||
<blockquote class="relative p-8 mb-4">
|
||||
<svg
|
||||
preserveAspectRatio="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 583 95"
|
||||
class="absolute left-0 w-full block"
|
||||
style="height: 95px; top: -94px;"
|
||||
>
|
||||
<polygon points="-30,95 583,95 583,65" class="text-blue-600 fill-current"></polygon>
|
||||
</svg>
|
||||
<h4 class="text-xl font-bold text-white">
|
||||
The best people for your people
|
||||
</h4>
|
||||
<p class="text-md font-light mt-2 text-white">
|
||||
We are here to make sure your people can do what they do best.
|
||||
Be with your people. Stop spending time working on the things
|
||||
that take you away from that.
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pb-20 relative block bg-gray-900">
|
||||
<div
|
||||
class="bottom-auto top-0 left-0 right-0 w-full absolute pointer-events-none overflow-hidden -mt-20"
|
||||
style="height: 80px;"
|
||||
>
|
||||
<svg
|
||||
class="absolute bottom-0 overflow-hidden"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="none"
|
||||
version="1.1"
|
||||
viewBox="0 0 2560 100"
|
||||
x="0"
|
||||
y="0"
|
||||
>
|
||||
<polygon class="text-gray-900 fill-current" points="2560 0 2560 100 0 100"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="container mx-auto px-4 lg:pt-24 lg:pb-64">
|
||||
<div class="flex flex-wrap text-center justify-center">
|
||||
<div class="w-full lg:w-6/12 px-4">
|
||||
<h2 class="text-4xl font-semibold text-white">Take something off your plate</h2>
|
||||
<p class="text-lg leading-relaxed mt-4 mb-4 text-gray-500">
|
||||
There is so much room to take chores of ministry off the plate
|
||||
of volunteers and staff members alike. Let us do that for you.
|
||||
Together we can spend more time with people
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap mt-12 justify-center">
|
||||
<div class="w-full lg:w-3/12 px-4 text-center">
|
||||
<div
|
||||
class="text-gray-900 p-3 w-12 h-12 shadow-lg rounded-full bg-white inline-flex items-center justify-center"
|
||||
>
|
||||
<i class="fas fa-medal text-xl"></i>
|
||||
</div>
|
||||
<h6 class="text-xl mt-5 font-semibold text-white">
|
||||
Excelent Services
|
||||
</h6>
|
||||
<p class="mt-2 mb-4 text-gray-500">
|
||||
With high quality integrations with the services you use every day,
|
||||
we will become your one stop shop
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full lg:w-3/12 px-4 text-center">
|
||||
<div
|
||||
class="text-gray-900 p-3 w-12 h-12 shadow-lg rounded-full bg-white inline-flex items-center justify-center"
|
||||
>
|
||||
<i class="fas fa-poll text-xl"></i>
|
||||
</div>
|
||||
<h5 class="text-xl mt-5 font-semibold text-white">
|
||||
Track the action
|
||||
</h5>
|
||||
<p class="mt-2 mb-4 text-gray-500">
|
||||
With audit and reporting tools, track how an event moves through the system.
|
||||
Make sure what you want is what you get
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-full lg:w-3/12 px-4 text-center">
|
||||
<div
|
||||
class="text-gray-900 p-3 w-12 h-12 shadow-lg rounded-full bg-white inline-flex items-center justify-center"
|
||||
>
|
||||
<i class="fas fa-lightbulb text-xl"></i>
|
||||
</div>
|
||||
<h5 class="text-xl mt-5 font-semibold text-white">Get Creative</h5>
|
||||
<p class="mt-2 mb-4 text-gray-500">
|
||||
If you want simple we have it. If you want complicated we have it.
|
||||
Build what you wnat how you want
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="relative block py-24 lg:pt-0 bg-gray-900">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex flex-wrap justify-center lg:-mt-64 -mt-48">
|
||||
<div class="w-full lg:w-6/12 px-4">
|
||||
<div
|
||||
class="relative flex flex-col min-w-0 break-words w-full mb-6 shadow-lg rounded-lg bg-gray-300"
|
||||
>
|
||||
<div class="flex-auto p-5 lg:p-10">
|
||||
<h4 class="text-2xl font-semibold">Want to try it out?</h4>
|
||||
<p class="leading-relaxed mt-1 mb-4 text-gray-600">
|
||||
Complete this form and we will get back to you in 24 hours.
|
||||
</p>
|
||||
<div class="relative w-full mb-3 mt-8">
|
||||
<label
|
||||
class="block uppercase text-gray-700 text-xs font-bold mb-2"
|
||||
for="full-name"
|
||||
>Full Name</label><input
|
||||
type="text"
|
||||
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
|
||||
placeholder="Full Name"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full mb-3">
|
||||
<label
|
||||
class="block uppercase text-gray-700 text-xs font-bold mb-2"
|
||||
for="email"
|
||||
>Email</label><input
|
||||
type="email"
|
||||
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
|
||||
placeholder="Email"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full mb-3">
|
||||
<label
|
||||
class="block uppercase text-gray-700 text-xs font-bold mb-2"
|
||||
for="message"
|
||||
>Message</label><textarea
|
||||
rows="4"
|
||||
cols="80"
|
||||
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
|
||||
placeholder="Type a message..."
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="text-center mt-6">
|
||||
<button
|
||||
class="bg-gray-900 text-white active:bg-gray-700 text-sm font-bold uppercase px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1"
|
||||
type="button"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package templates
|
||||
|
||||
import "git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
|
||||
//Head for scripts and such
|
||||
|
||||
templ Head(msg string) {
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="theme-color" content="#000000"/>
|
||||
<meta name="view-transition" content="same-origin"/>
|
||||
<meta name="htmx-config" content='{"globalViewTransistions": true}'/>
|
||||
<link rel="shortcut icon" href="/static/favicon.ico"/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="76x76"
|
||||
href="/static/apple-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"
|
||||
/>
|
||||
<link
|
||||
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>
|
||||
}
|
||||
|
||||
templ LandingPage(user *models.User) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@Head("Welcome")
|
||||
<body class="text-gray-800 antialiased">
|
||||
@Nav(user)
|
||||
@LandingContent()
|
||||
</body>
|
||||
@LandingFooter()
|
||||
@toggleNavBar()
|
||||
</html>
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package templates
|
||||
|
||||
templ LoginContent(signup bool, errorMsg string) {
|
||||
<main>
|
||||
<section class="absolute w-full h-full">
|
||||
<div
|
||||
class="absolute top-0 w-full h-full bg-gray-900"
|
||||
style="background-size: 100%; background-repeat: no-repeat;"
|
||||
></div>
|
||||
<div class="container mx-auto px-4 h-full">
|
||||
<div class="flex content-center items-center justify-center h-full">
|
||||
<div class="w-full lg:w-4/12 px-4">
|
||||
<div
|
||||
class="relative flex flex-col min-w-0 break-words w-full mb-6 shadow-lg rounded-lg bg-gray-300 border-0"
|
||||
>
|
||||
<div class="flex-auto px-4 lg:px-10 py-10 pt-10">
|
||||
if errorMsg != "" {
|
||||
<div role="alert">
|
||||
<div class="bg-red-500 text-white font-bold rounded-t px-4 py-2">
|
||||
Error
|
||||
</div>
|
||||
<div class="border border-t-0 border-red-400 rounded-b bg-red-100 px-4 py-3 text-red-700">
|
||||
<p>{ errorMsg }</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="text-gray-500 text-center mb-3 font-bold">
|
||||
<small>Sign in</small>
|
||||
</div>
|
||||
<form
|
||||
if signup {
|
||||
action="/signup"
|
||||
} else {
|
||||
action="/login"
|
||||
}
|
||||
method="POST"
|
||||
>
|
||||
<div class="relative w-full mb-3">
|
||||
<label
|
||||
class="block uppercase text-gray-700 text-xs font-bold mb-2"
|
||||
for="grid-password"
|
||||
>Email</label><input
|
||||
type="email"
|
||||
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
|
||||
placeholder="Email"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
name="email"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative w-full mb-3">
|
||||
<label
|
||||
class="block uppercase text-gray-700 text-xs font-bold mb-2"
|
||||
for="grid-password"
|
||||
>Password</label><input
|
||||
type="password"
|
||||
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
|
||||
placeholder="Password"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
id="customCheckLogin"
|
||||
type="checkbox"
|
||||
class="form-checkbox border-0 rounded text-gray-800 ml-1 w-5 h-5"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
/><span class="ml-2 text-sm font-semibold text-gray-700">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="text-center mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-gray-900 text-white active:bg-gray-700 text-sm font-bold uppercase px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 w-full"
|
||||
type="button"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
>
|
||||
if signup {
|
||||
{ "Signup" }
|
||||
} else {
|
||||
{ "Login" }
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex flex-wrap mt-2 py-5">
|
||||
if !signup {
|
||||
<div class="w-1/2 text-center">
|
||||
<a href="#pablo" class="text-gray-800"><small>Forgot password?</small></a>
|
||||
</div>
|
||||
}
|
||||
if signup {
|
||||
<div class="w-full text-center">
|
||||
<a href="/login" class="text-gray-800"><small>Log in instead</small></a>
|
||||
</div>
|
||||
} else {
|
||||
<div class="w-1/2 text-center">
|
||||
<a href="/signup" class="text-gray-800"><small>Create an account</small></a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package templates
|
||||
|
||||
templ LoginPage(errorMsg string) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@Head("Log in")
|
||||
<body class="text-gray-800 antialiased h-full overflow-hidden">
|
||||
@Nav(nil)
|
||||
@LoginContent(false, errorMsg)
|
||||
</body>
|
||||
@LoginFooter()
|
||||
@toggleNavBar()
|
||||
</html>
|
||||
}
|
||||
|
||||
templ SignupPage(errorMsg string) {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@Head("Sign up")
|
||||
<body class="text-gray-800 antialiased h-full overflow-hidden">
|
||||
@Nav(nil)
|
||||
@LoginContent(true, errorMsg)
|
||||
</body>
|
||||
@LoginFooter()
|
||||
@toggleNavBar()
|
||||
</html>
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
|
||||
)
|
||||
|
||||
templ navTitle(title string) {
|
||||
<a
|
||||
class="text-sm font-bold leading-relaxed inline-block mr-4 py-2 whitespace-nowrap uppercase text-white"
|
||||
href="/"
|
||||
>
|
||||
{ title }
|
||||
</a>
|
||||
<button
|
||||
class="cursor-pointer text-xl leading-none px-3 py-1 border border-solid border-transparent rounded bg-transparent block lg:hidden outline-none focus:outline-none"
|
||||
type="button"
|
||||
onclick="toggleNavbar('example-collapse-navbar')"
|
||||
>
|
||||
<i class="text-white fas fa-bars"></i>
|
||||
</button>
|
||||
}
|
||||
|
||||
templ navItem(name string, link string) {
|
||||
<li class="flex items-center">
|
||||
<a
|
||||
class="lg:text-white lg:hover:text-gray-300 text-gray-800 px-3 py-4 lg:py-2 flex items-center text-xs uppercase font-bold"
|
||||
href="{ link }"
|
||||
>
|
||||
{ name }
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
templ navActionItem(auth bool) {
|
||||
<li class="flex items-center">
|
||||
if auth {
|
||||
<a href="/dashboard">
|
||||
<button
|
||||
class="bg-white text-gray-800 active:bg-gray-100 text-xs font-bold uppercase px-4 py-2 rounded shadow hover:shadow-md outline-none focus:outline-none lg:mr-1 lg:mb-0 ml-3 mb-3"
|
||||
type="button"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
</a>
|
||||
} else {
|
||||
<a href="/login">
|
||||
<button
|
||||
class="bg-white text-gray-800 active:bg-gray-100 text-xs font-bold uppercase px-4 py-2 rounded shadow hover:shadow-md outline-none focus:outline-none lg:mr-1 lg:mb-0 ml-3 mb-3"
|
||||
type="button"
|
||||
style="transition: all 0.15s ease 0s;"
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
|
||||
templ Nav(user *models.User) {
|
||||
<nav class="top-0 absolute z-50 w-full flex flex-wrap items-center justify-between px-2 py-3 ">
|
||||
<div class="container px-4 mx-auto flex flex-wrap items-center justify-between">
|
||||
<div class="w-full relative flex justify-between lg:w-auto lg:static lg:block lg:justify-start">
|
||||
@navTitle("Capstone")
|
||||
</div>
|
||||
<div
|
||||
class="lg:flex flex-grow items-center bg-white lg:bg-transparent lg:shadow-none hidden"
|
||||
id="example-collapse-navbar"
|
||||
>
|
||||
<ul class="flex flex-col lg:flex-row list-none mr-auto">
|
||||
@navItem("About Us", "/about-us")
|
||||
@navItem("Pricing", "/pricing")
|
||||
</ul>
|
||||
<ul class="flex flex-col lg:flex-row list-none lg:ml-auto">
|
||||
if user != nil {
|
||||
@navActionItem(true)
|
||||
} else {
|
||||
@navActionItem(false)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|