Skip to content

Instantly share code, notes, and snippets.

@eSlider
Last active December 9, 2022 14:56
Show Gist options
  • Save eSlider/691a6c4a3135b548fc384e6b3b9bb9ea to your computer and use it in GitHub Desktop.
Save eSlider/691a6c4a3135b548fc384e6b3b9bb9ea to your computer and use it in GitHub Desktop.
Workflow-Example

Service for Author and Reader Workflows

This AWS Lambda service provides a streamlined workflow for authors and readers to interact with each other. It allows authors to sign up, configure their reader magnet, and integrate their email tool. It also allows readers to access the author's book landing page, confirm their email address, and access the exclusive content.

Author Workflow

  1. Author Sign-up: Authors come to the website to sign-up for the service and to configure/administer their reader magnet. The author account sign-up flow should simply be email and password. Admin has the ability to approve/reject/freeze accounts in order to save capacity for the 100 authors (in case a bunch of random people sign-up).

  2. Author’s Book Landing Page Configuration: After an author is signed up, they have a landing page configuration capability that provides the inputs to their landing page that readers see for each of their books that contain an email magnet. Each book landing page should have the ability to upload a cover image and a few paragraphs of text. For each successfully configured book, a link is generated so the author can place that link at the end of their book.

  3. Author’s Email Tool Integration: Author’s can select what email tool they utilize and login to their email provider to oauth/connect their email service. Example integrations include: Mailchimp, ConstantContact, MailerLite, ConvertKit, AWeber, etc. If an author doesn’t configure a tool, they have the option to download a csv of emails provided. Additionally, authors will also have the ability to select whether or not they want to ensure emails are confirmed before providing the reader landing page and this is not selected by default.

  4. Author’s Reader Landing Page: Author’s can upload their exclusive book or chapter content. That book content is delivered to an Amazon S3 bucket.

  5. Author Statistics Portal: This portal shows high level statistics including: how many readers have clicked the link at the end of the book and loaded their Author Landing Page per configured book, how many readers have signed up for their newsletter, and how many readers clicked the link to access the exclusive content.

Reader Workflow

  1. Author’s book landing page: This is the actual page that readers can click and come to from the email magnet. The page should display on mobile and web and have a strong call to action to sign-up for the newsletter.

  2. Email Confirmation: Once a reader submits their email on the landing page, if the author has selected the option that emails are confirmed, then a reader must look in their inbox and confirm the proper email address before progressing to the reader landing page.

  3. Reader Landing Page: Once a reader signs up for the newsletter, a “Thank you for signing up” page displays with a link that directs the reader to download a book.

Tasks

Author Workflow

  1. Author Sign-up:

    • Create an AWS Lambda function to handle the author sign-up process.
    • The function should accept an email and password as input and validate the credentials.
    • If the credentials are valid, the function should create an author account and return a success message.
    • If the credentials are invalid, the function should return an error message.
    • The function should also have the ability to approve/reject/freeze accounts in order to save capacity for the 100 authors.
  2. Author’s Book Landing Page Configuration:

    • Create an AWS Lambda function to handle the author’s book landing page configuration.
    • The function should accept inputs such as a cover image and a few paragraphs of text.
    • The function should generate a link for each successfully configured book.
    • The function should return the generated link to the author.
  3. Author’s Email Tool Integration:

    • Create an AWS Lambda function to handle the author’s email tool integration.
    • The function should accept the author’s email provider as input and validate the credentials.
    • If the credentials are valid, the function should connect the email service and return a success message.
    • If the credentials are invalid, the function should return an error message.
    • The function should also have the ability to select whether or not the author wants to ensure emails are confirmed before providing the reader landing page.
  4. Author’s Reader Landing Page:

    • Create an AWS Lambda function to handle the author’s reader landing page.
    • The function should accept the exclusive book or chapter content as input.
    • The function should upload the content to an Amazon S3 bucket.
    • The function should return a success message when the upload is complete.
  5. Author Statistics Portal:

    • Create an AWS Lambda function to handle the author statistics portal.
    • The function should query the database for the number of readers who have clicked the link at the end of the book and loaded their Author Landing Page per configured book, the number of readers who have signed up for their newsletter, and the number of readers who clicked the link to access the exclusive content.
    • The function should return the statistics to the author.

Reader Workflow

  1. Author’s book landing page:

    • Create an AWS Lambda function to handle the author’s book landing page.
    • The function should accept the reader’s email address as input and validate the credentials.
    • If the credentials are valid, the function should display the landing page with a strong call to action to sign-up for the newsletter.
    • If the credentials are invalid, the function should return an error message.
  2. Email Confirmation:

    • Create an AWS Lambda function to handle the email confirmation process.
    • The function should check if the author has selected the option that emails are confirmed.
    • If the option is selected, the function should send a confirmation email to the reader and wait for the reader to confirm the email address.
    • If the option is not selected, the function should proceed to the reader landing page.
  3. Reader Landing Page:

    • Create an AWS Lambda function to handle the reader landing page.
    • The function should display a “Thank you for signing up” page with a link that directs the reader to download a book.

Service prototype code

Author Workflow

Author Sign-up

import { APIGatewayEvent, Context, Callback } from 'aws-lambda';

export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const { email, password } = event.body;

  // Validate email and password
  if (!email || !password) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'Email and password are required'
      })
    });
  }

  // Check if user already exists
  const userExists = checkIfUserExists(email);
  if (userExists) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'User already exists'
      })
    });
  }

  // Create user
  const user = createUser(email, password);

  // Return success
  return callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: 'User created successfully',
      user
    })
  });
};

Author's Book Landing Page Configuration

import { APIGatewayEvent, Context, Callback } from 'aws-lambda';

export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const { bookId, coverImage, text } = event.body;

  // Validate bookId, coverImage, and text
  if (!bookId || !coverImage || !text) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'BookId, coverImage, and text are required'
      })
    });
  }

  // Check if book already exists
  const bookExists = checkIfBookExists(bookId);
  if (bookExists) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'Book already exists'
      })
    });
  }

  // Create book
  const book = createBook(bookId, coverImage, text);

  // Generate link
  const link = generateLink(bookId);

  // Return success
  return callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Book created successfully',
      book,
      link
    })
  });
};

Author's Email Tool Integration

import { APIGatewayEvent, Context, Callback } from 'aws-lambda';

export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const { emailTool, emailAddress, confirmEmails } = event.body;

  // Validate emailTool, emailAddress, and confirmEmails
  if (!emailTool || !emailAddress || !confirmEmails) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'EmailTool, emailAddress, and confirmEmails are required'
      })
    });
  }

  // Check if emailTool is supported
  const isSupported = checkIfEmailToolIsSupported(emailTool);
  if (!isSupported) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'EmailTool is not supported'
      })
    });
  }

  // Connect emailTool
  const connection = connectEmailTool(emailTool, emailAddress);

  // Generate csv
  const csv = generateCsv(emailAddress);

  // Return success
  return callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: 'EmailTool connected successfully',
      connection,
      csv
    })
  });
};

Author's Reader Landing Page

import { APIGatewayEvent, Context, Callback } from 'aws-lambda';

export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const { bookId, content } = event.body;

  // Validate bookId and content
  if (!bookId || !content) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'BookId and content are required'
      })
    });
  }

  // Upload content to S3
  const s3Url = uploadContentToS3(bookId, content);

  // Return success
  return callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Content uploaded successfully',
      s3Url
    })
  });
};

Author Statistics Portal

import { APIGatewayEvent, Context, Callback } from 'aws-lambda';

export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const { bookId } = event.body;

  // Validate bookId
  if (!bookId) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'BookId is required'
      })
    });
  }

  // Get statistics
  const statistics = getStatistics(bookId);

  // Return success
  return callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Statistics retrieved successfully',
      statistics
    })
  });
};

Reader Workflow

Author's Book Landing Page

import { APIGatewayEvent, Context, Callback } from 'aws-lambda';

export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const { bookId } = event.body;

  // Validate bookId
  if (!bookId) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'BookId is required'
      })
    });
  }

  // Get book
  const book = getBook(bookId);

  // Return success
  return callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Book retrieved successfully',
      book
    })
  });
};

Email Confirmation

import { APIGatewayEvent, Context, Callback } from 'aws-lambda';

export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const { emailAddress, confirmEmails } = event.body;

  // Validate emailAddress and confirmEmails
  if (!emailAddress || !confirmEmails) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'EmailAddress and confirmEmails are required'
      })
    });
  }

  // Check if email is confirmed
  const isConfirmed = checkIfEmailIsConfirmed(emailAddress);
  if (!isConfirmed && confirmEmails) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'Email is not confirmed'
      })
    });
  }

  // Return success
  return callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Email confirmed successfully'
    })
  });
};

Reader Landing Page

import { APIGatewayEvent, Context, Callback } from 'aws-lambda';

export const handler = (event: APIGatewayEvent, context: Context, callback: Callback) => {
  const { emailAddress } = event.body;

  // Validate emailAddress
  if (!emailAddress) {
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        message: 'EmailAddress is required'
      })
    });
  }

  // Sign up for newsletter
  const signup = signupForNewsletter(emailAddress);

  // Generate link
  const link = generateLink(emailAddress);

  // Return success
  return callback(null, {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Signup successful',
      signup,
      link
    })
  });
};

Frontend

Using Vue and Vuetify frameworks

<template>
  <v-app>
    <v-content>
      <v-container>
        <!-- Author Sign-up -->
        <v-row>
          <v-col>
            <v-form>
              <v-text-field label="Email" v-model="email" />
              <v-text-field label="Password" v-model="password" type="password" />
              <v-btn @click="signUp()">Sign Up</v-btn>
            </v-form>
          </v-col>
        </v-row>

        <!-- Author's Book Landing Page Configuration -->
        <v-row>
          <v-col>
            <v-form>
              <v-text-field label="Cover Image" v-model="coverImage" />
              <v-textarea label="Text" v-model="text" />
              <v-btn @click="generateLink()">Generate Link</v-btn>
            </v-form>
          </v-col>
        </v-row>

        <!-- Author's Email Tool Integration -->
        <v-row>
          <v-col>
            <v-form>
              <v-select label="Email Tool" v-model="emailTool" :items="emailTools" />
              <v-btn @click="connectEmailTool()">Connect Email Tool</v-btn>
              <v-btn @click="downloadCSV()">Download CSV</v-btn>
            </v-form>
          </v-col>
        </v-row>

        <!-- Author's Reader Landing Page -->
        <v-row>
          <v-col>
            <v-form>
              <v-text-field label="Book Content" v-model="bookContent" />
              <v-btn @click="uploadBookContent()">Upload Book Content</v-btn>
            </v-form>
          </v-col>
        </v-row>

        <!-- Author Statistics Portal -->
        <v-row>
          <v-col>
            <v-card>
              <v-card-title>Statistics</v-card-title>
              <v-card-text>
                <p>Number of readers who clicked the link at the end of the book and loaded their Author Landing Page per configured book: {{ readersLinked }}</p>
                <p>Number of readers who signed up for their newsletter: {{ readersSignedUp }}</p>
                <p>Number of readers who clicked the link to access the exclusive content: {{ readersContent }}</p>
              </v-card-text>
            </v-card>
          </v-col>
        </v-row>

        <!-- Reader Workflow -->
        <v-row>
          <v-col>
            <v-form>
              <v-text-field label="Email" v-model="readerEmail" />
              <v-btn @click="submitReaderEmail()">Submit Email</v-btn>
            </v-form>
          </v-col>
        </v-row>

        <!-- Reader Landing Page -->
        <v-row>
          <v-col>
            <v-card>
              <v-card-title>Thank you for signing up!</v-card-title>
              <v-card-text>
                <p>Click the link below to download your book.</p>
                <v-btn @click="downloadBook()">Download Book</v-btn>
              </v-card-text>
            </v-card>
          </v-col>
        </v-row>
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
import { mapState } from 'vuex';

export default {
  data() {
    return {
      email: '',
      password: '',
      coverImage: '',
      text: '',
      emailTool: '',
      emailTools: ['Mailchimp', 'ConstantContact', 'MailerLite', 'ConvertKit', 'AWeber'],
      bookContent: '',
      readerEmail: ''
    };
  },
  computed: {
    ...mapState(['readersLinked', 'readersSignedUp', 'readersContent'])
  },
  methods: {
    signUp() {
      // sign up logic
    },
    generateLink() {
      // generate link logic
    },
    connectEmailTool() {
      // connect email tool logic
    },
    downloadCSV() {
      // download csv logic
    },
    uploadBookContent() {
      // upload book content logic
    },
    submitReaderEmail() {
      // submit reader email logic
    },
    downloadBook() {
      // download book logic
    }
  }
};
</script>

AWS Terraform infrastructure

Provider

provider "aws" {
  region = "us-east-1"
}

IAM

Create an IAM user for the author to sign up and configure their reader magnet.

resource "aws_iam_user" "author" {
  name = "author"
}

resource "aws_iam_access_key" "author" {
  user = "${aws_iam_user.author.name}"
}

S3

Create an S3 bucket for the author to upload their exclusive book or chapter content.

resource "aws_s3_bucket" "author_content" {
  bucket = "author-content"
  acl    = "private"
}

Lambda

Create a Lambda function to process the reader's email address and send a confirmation email if the author has selected the option that emails are confirmed.

resource "aws_lambda_function" "email_confirmation" {
  filename         = "email_confirmation.zip"
  function_name    = "email_confirmation"
  role             = "${aws_iam_role.lambda_role.arn}"
  handler          = "index.handler"
  source_code_hash = "${base64sha256(file("email_confirmation.zip"))}"
  runtime          = "nodejs12.x"
}

resource "aws_iam_role" "lambda_role" {
  name = "lambda_role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "lambda_policy" {
  name = "lambda_policy"
  role = "${aws_iam_role.lambda_role.id}"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::author-content/*"
    }
  ]
}
EOF
}

API Gateway

Create an API Gateway to handle the requests from the reader's landing page.

resource "aws_api_gateway_rest_api" "reader_landing_page" {
  name = "reader_landing_page"
}

resource "aws_api_gateway_resource" "reader_landing_page" {
  rest_api_id = "${aws_api_gateway_rest_api.reader_landing_page.id}"
  parent_id   = "${aws_api_gateway_rest_api.reader_landing_page.root_resource_id}"
  path_part   = "reader_landing_page"
}

resource "aws_api_gateway_method" "reader_landing_page" {
  rest_api_id   = "${aws_api_gateway_rest_api.reader_landing_page.id}"
  resource_id   = "${aws_api_gateway_resource.reader_landing_page.id}"
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "reader_landing_page" {
  rest_api_id             = "${aws_api_gateway_rest_api.reader_landing_page.id}"
  resource_id             = "${aws_api_gateway_resource.reader_landing_page.id}"
  http_method             = "${aws_api_gateway_method.reader_landing_page.http_method}"
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = "${aws_lambda_function.email_confirmation.invoke_arn}"
}

resource "aws_api_gateway_deployment" "reader_landing_page" {
  depends_on = ["aws_api_gateway_integration.reader_landing_page"]

  rest_api_id = "${aws_api_gateway_rest_api.reader_landing_page.id}"
  stage_name  = "prod"
}

CI/CD

GitLab CI Configuration

.gitlab-ci.yml

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - npm install
    - npm run build
  artifacts:
    paths:
      - dist/

test:
  stage: test
  script:
    - npm run test

deploy:
  stage: deploy
  script:
    - aws deploy --type typescript --lambda-service
  environment:
    name: production
    url: https://example.com
  only:
    - master

.deploy_job:
  stage: deploy
  script:
    - aws deploy --type typescript --lambda-service
  environment:
    name: production
    url: https://example.com
  when: manual
  allow_failure: true
  only:
    - master

Github Actions Workflows

name: CI

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup Node
      uses: actions/setup-node@v1
      with:
        node-version: '12.x'
    - name: Install Dependencies
      run: npm install
    - name: Build
      run: npm run build
    - name: Test
      run: npm run test
    - name: Deploy
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1
    - name: Deploy to AWS
      run: npm run deploy
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        AWS_REGION: us-east-1

# Return code should be 0 if success
    - name: Check Return Code
      run: exit 0
      
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment