Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PROJECT_NAME := ${PROJECT_NAME}

.EXPORT_ALL_VARIABLES:

run: ci_setup
run: ci_setup billing_setup
@echo "\nDone"

ci_setup:
Expand All @@ -23,6 +23,11 @@ ifeq ($(CIVendor), circleci)
ci_setup: circle_ci_setup
endif

billing_setup:
ifeq ($(billingEnabled), yes)
sh scripts/setup-stripe-secrets.sh
endif

circle_ci_setup:
@echo "Set CIRCLECI environment variables\n"
export AWS_ACCESS_KEY_ID=$(shell aws secretsmanager get-secret-value --region ${region} --secret-id=${PROJECT_NAME}-ci-user-aws-keys${randomSeed} | jq -r '.SecretString'| jq -r .access_key_id)
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ This repository is language/business-logic agnostic; mainly showcasing some univ
|-- Makefile #make command triggers the initialization of repository
|-- zero-module.yml #module declares required parameters and credentials
| # files in templates become the repo for users
| scripts/
| | # these are scripts called only once during zero apply, and we don't
| | # expect a need to rerun them throughout development of the repository
| | # used for checking binary requires / setting up CI / secrets
| | |-- check.sh
| | |-- gha-setup.sh
| | |-- required-bins.sh
| | |-- setup-stripe-secrets.sh
| templates/
| | # this makefile is used both during init and
| | # on-going needs/utilities for user to maintain their infrastructure
Expand Down
26 changes: 26 additions & 0 deletions scripts/setup-stripe-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash

# This script runs only when billingEnabled = "yes", invoked from makefile
# modify the kubernetes application secret and appends STRIPE_API_SECRET_KEY
# the deployment by default will pick up all key-value pairs as env-vars from the secret

if [[ "$ENVIRONMENT" == "" ]]; then
echo "Must specify \$ENVIRONMENT to create stripe secret ">&2; exit 1;
elif [[ "$ENVIRONMENT" == "stage" ]]; then
PUBLISHABLE_API_KEY=$stagingStripePublicApiKey
SECRET_API_KEY=$stagingStripeSecretApiKey
elif [[ "$ENVIRONMENT" == "prod" ]]; then
PUBLISHABLE_API_KEY=$productionStripePublicApiKey
SECRET_API_KEY=$productionStripeSecretApiKey
fi

CLUSTER_NAME=${PROJECT_NAME}-${ENVIRONMENT}-${REGION}
NAMESPACE=${PROJECT_NAME}

BASE64_TOKEN=$(printf ${SECRET_API_KEY} | base64)
## Modify existing application secret to have stripe api key
kubectl --context $CLUSTER_NAME -n $NAMESPACE get secret ${PROJECT_NAME} -o json | \
Comment thread
davidcheung marked this conversation as resolved.
jq --arg STRIPE_API_SECRET_KEY $BASE64_TOKEN '.data["STRIPE_API_SECRET_KEY"]=$STRIPE_API_SECRET_KEY' \
| kubectl apply -f -

sh ${PROJECT_DIR}/scripts/stripe-example-setup.sh
4 changes: 2 additions & 2 deletions templates/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
DATABASE_ENGINE=
DATABASE_ENGINE=<% index .Params `database` %>
DATABASE_HOST=
DATABASE_PORT=
DATABASE_PORT=<%if eq (index .Params `database`) "postgres" %>5432<% else if eq (index .Params `database`) "mysql" %>3306<% end %>
DATABASE_PASSWORD=
DATABASE_USERNAME=
DATABASE_NAME=
19 changes: 19 additions & 0 deletions templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,26 @@ ALTER TABLE address
ADD COLUMN city VARCHAR(30) AFTER street_name,
ADD COLUMN province VARCHAR(30) AFTER city
```
<%if eq (index .Params `billingEnabled`) "yes" %>
## Billing example
A subscription and checkout example using [Stripe](https://stripe.com), coupled with the frontend repository to provide an end-to-end checkout example for you to customize. We also setup a webhook and an endpoint in the backend to receive webhook when events occur.

### Setup
The following example content has been set up in Stripe:
- 1 product
- 3 prices(subscriptions) [annual, monthly, daily]
- 1 webhook [`charge.failed`, `charge.succeeded`, `customer.created`, `subscription_schedule.created`]
See link for available webhooks: https://stripe.com/docs/api/webhook_endpoints/create?lang=curl#create_webhook_endpoint-enabled_events

this is setup using the script [scripts/stripe-example-setup.sh](scripts/stripe-example-setup.sh)

### Deployment
The deployment only requires the environment variables:
- STRIPE_API_SECRET_KEY (created in AWS secret then deployed via Kubernetes Secret)
- FRONTEND_URL (used for sending user back to frontend upon checkouts)
- BACKEND_URL (used for redirects after checkout and webhooks)

<% end %>
<!-- Links -->
[base-cronjob]: ./kubernetes/base/cronjob.yml
[base-deployment]: ./kubernetes/base/deployment.yml
Expand Down
16 changes: 5 additions & 11 deletions templates/kubernetes/base/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,17 @@ spec:
envFrom:
- configMapRef:
name: <% .Name %>-config
- secretRef:
name: <% .Name %>
env:
- name: SERVER_PORT
value: "80"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
<% if eq (index .Params `fileUploads`) "yes" %> - name: CF_KEYPAIR_ID
<%- if eq (index .Params `fileUploads`) "yes" %>
- name: CF_KEYPAIR_ID
valueFrom:
secretKeyRef:
name: cf-keypair
Expand All @@ -57,16 +60,7 @@ spec:
secretKeyRef:
name: cf-keypair
key: private_key
<% end %> - name: DATABASE_USERNAME
valueFrom:
secretKeyRef:
name: <% .Name %>
key: DATABASE_USERNAME
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: <% .Name %>
key: DATABASE_PASSWORD
<%- end %>
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
Expand Down
4 changes: 2 additions & 2 deletions templates/kubernetes/overlays/production/auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ metadata:
name: public-backend-endpoints
spec:
match:
url: http://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/status/<.*>
url: http://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/<(status|webhook)\/.*>
---
## Backend User-restricted endpoint
# pattern: http://<proxy>/<not [/.ory/kratos and /status]>
Expand All @@ -21,5 +21,5 @@ metadata:
name: authenticated-backend-endpoints
spec:
match:
url: http://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/<(?!(status|\.ory\/kratos)).*>
url: http://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/<(?!(status|webhook|\.ory\/kratos)).*>

2 changes: 2 additions & 0 deletions templates/kubernetes/overlays/production/kustomization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ configMapGenerator:
literals:
- ENVIRONMENT=production
- DOMAIN=<% index .Params `productionHostRoot` %>
- BACKEND_URL=https://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>
- FRONTEND_URL=https://<% index .Params `productionFrontendSubdomain` %><% index .Params `productionHostRoot` %>
- S3_BUCKET=files.<% index .Params `productionHostRoot` %>
4 changes: 2 additions & 2 deletions templates/kubernetes/overlays/staging/auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ metadata:
name: public-backend-endpoints
spec:
match:
url: http://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/status/<.*>
url: http://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/<(status|webhook)\/.*>
---
## Backend User-restricted endpoint
# pattern: http://<proxy>//api
Expand All @@ -21,4 +21,4 @@ metadata:
name: authenticated-backend-endpoints
spec:
match:
url: http://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/<(?!(status|\.ory\/kratos)).*>
url: http://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/<(?!(status|webhook|\.ory\/kratos)).*>
2 changes: 2 additions & 0 deletions templates/kubernetes/overlays/staging/kustomization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ configMapGenerator:
literals:
- ENVIRONMENT=staging
- DOMAIN=<% index .Params `stagingHostRoot` %>
- BACKEND_URL=https://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>
- FRONTEND_URL=https://<% index .Params `stagingFrontendSubdomain` %><% index .Params `stagingHostRoot` %>
- S3_BUCKET=files.<% index .Params `stagingHostRoot` %>
15 changes: 11 additions & 4 deletions templates/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,29 @@
"author": "",
"license": "ISC",
"dependencies": {
<%if eq (index .Params `apiType`) "graphql" %>
<%- if eq (index .Params `apiType`) "graphql" %>
"apollo-datasource": "^0.7.3",
"apollo-datasource-rest": "^0.9.7",
"apollo-server-express": "^2.19.2",
"graphql": "^15.5.0",
"graphql-combine": "^1.0.1",
<% end %>
<%- end %>
"aws-cloudfront-sign": "^2.2.0",
"aws-sdk": "^2.744.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-busboy": "^8.0.0",
"morgan": "^1.10.0",
<%if eq (index .Params `database`) "postgres" %>"pg": "^8.4.1",
"pg-hstore": "^2.3.3",<% else if eq (index .Params `database`) "mysql" %>"mysql2": "^2.2.5",<% end %>
<%- if eq (index .Params `database`) "postgres" %>
"pg": "^8.4.1",
"pg-hstore": "^2.3.3",
<%- else if eq (index .Params `database`) "mysql" %>
"mysql2": "^2.2.5",
<%- end %>
"sequelize": "^6.3.5"
<%- if eq (index .Params `billingEnabled`) "yes" %>,
"stripe": "^8.143.0"
<%- end %>
},
"devDependencies": {
"eslint": "^7.10.0",
Expand Down
70 changes: 70 additions & 0 deletions templates/scripts/stripe-example-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/bin/bash
Comment thread
davidcheung marked this conversation as resolved.
set -e

# Creates stripe example for frontend/backend for checkout and subscription
# the script uses the token and creates the following for the end-to-end example to work
# - 1 product
# - 3 plans
# - 1 webhook
#
# If you want to recreate this you can use the curl requests as an example below.

PROJECT_NAME=<% .Name %>
RANDOM_SEED=<% index .Params `randomSeed` %>
REGION=<% index .Params `region` %>

echo "Running on ${ENVIRONMENT}"
if [[ "$ENVIRONMENT" == "" ]]; then
exit 1;
elif [[ "$ENVIRONMENT" == "stage" ]]; then
BACKEND_API_WEBHOOK_ENDPOINT="https://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>/webhook/stripe"
PUBLISHABLE_API_KEY=$stagingStripePublicApiKey
SECRET_API_KEY=$stagingStripeSecretApiKey
elif [[ "$ENVIRONMENT" == "prod" ]]; then
BACKEND_API_WEBHOOK_ENDPOINT="https://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>/webhook/stripe"
PUBLISHABLE_API_KEY=$productionStripePublicApiKey
SECRET_API_KEY=$productionStripeSecretApiKey
fi

TOKEN=$(echo $SECRET_API_KEY | base64)
AUTH_HEADER="Authorization: Basic ${TOKEN}"

## Create Product
PRODUCT_ID=$(curl -XPOST \
--url https://api.stripe.com/v1/products \
--header "${AUTH_HEADER}" \
-d "name"="$PROJECT_NAME" | jq -r ".id")

curl https://api.stripe.com/v1/prices \
--header "${AUTH_HEADER}" \
-d "product"="$PRODUCT_ID" \
-d "unit_amount"=5499 \
-d "currency"="CAD" \
-d "recurring[interval]=month" \
-d "nickname"="Monthly Plan"

curl https://api.stripe.com/v1/prices \
--header "${AUTH_HEADER}" \
-d "product"="$PRODUCT_ID" \
-d "unit_amount"=299 \
-d "currency"="CAD" \
-d "recurring[interval]=day" \
-d "nickname"="Daily Plan"

curl https://api.stripe.com/v1/prices \
--header "${AUTH_HEADER}" \
-d "product"="$PRODUCT_ID" \
-d "unit_amount"=50000 \
-d "currency"="CAD" \
-d "recurring[interval]=year" \
-d "nickname"="Annual Plan"

# Create webhook on stripe platform
# See link for available webhooks: https://stripe.com/docs/api/webhook_endpoints/create?lang=curl#create_webhook_endpoint-enabled_events
curl https://api.stripe.com/v1/webhook_endpoints \
--header "${AUTH_HEADER}" \
-d url="${BACKEND_API_WEBHOOK_ENDPOINT}" \
-d "enabled_events[]"="charge.failed" \
-d "enabled_events[]"="charge.succeeded" \
-d "enabled_events[]"="customer.created" \
-d "enabled_events[]"="subscription_schedule.created"
28 changes: 15 additions & 13 deletions templates/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@ const dotenv = require("dotenv");
const express = require("express");
const morgan = require("morgan");
const dbDatasource = require("./db");
<%if eq (index .Params `fileUploads`) "yes" %>const fileRoutes = require("./app/file");<% end %>
<%if eq (index .Params `userAuth`) "yes" %>const authRoutes = require("./app/auth");
const { authMiddleware } = require("./middleware/auth");<% end %>
const statusRoutes = require("./app/status");
<%if eq (index .Params `fileUploads`) "yes" %>const fileRoutes = require("./app/file");
<%- end %>
<%- if eq (index .Params `userAuth`) "yes" %>const authRoutes = require("./app/auth");
const { authMiddleware } = require("./middleware/auth");
<%- end %>
<%- if eq (index .Params `billingEnabled`) "yes" %>const billingRoutes = require("./app/billing");
<%- end %>
const publicRoutes = require("./app/public");

dotenv.config();
const app = express();
app.use(morgan("combined"));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

<%if eq (index .Params `userAuth`) "yes" %>app.use(authMiddleware);
app.use("/auth", authRoutes);<% end %>

<%if eq (index .Params `fileUploads`) "yes" %>app.use("/file", fileRoutes);<% end %>
// these are placed before the auth middleware, so will not 401 upon non-authenticated requests
app.use("/", publicRoutes);

app.use("/status", statusRoutes);
<% if eq (index .Params `userAuth`) "yes" %>app.use(authMiddleware);
app.use("/auth", authRoutes);<% end %>
<% if eq (index .Params `fileUploads`) "yes" %>app.use("/file", fileRoutes);<% end %>
<% if eq (index .Params `billingEnabled`) "yes" %>app.use("/billing", billingRoutes);<% end %>

const port = process.env.SERVER_PORT;
if (!port) {
port = 3000;
}
const port = process.env.SERVER_PORT || 8080;

const main = async () => {
// remove this block for development, just for verifying DB
Expand Down
Loading