├── app.sh ├── start ├── stop ├── .node-version ├── docs ├── test.md └── _config.yml ├── rebuild ├── public ├── Campaigns │ ├── test.md │ ├── Clean Futures Act.png │ ├── Green New Deal for New York Act.png │ ├── Energy Efficiency, Equity and Jobs.png │ ├── Proposed Actions on EJ Communities.png │ ├── Climate and Community Investment Act.png │ └── Climate Leadership and Community Protection Act.png ├── favicon.ico ├── favicon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── browserconfig.xml ├── site.webmanifest └── index.html ├── .docker ├── .dockerignore ├── Dockerfile └── docker-compose.yml ├── roiScript ├── issue_timestamp.log ├── log-branch.mjs ├── issue-metrics.mjs ├── package.json ├── send-time.mjs └── send-metrics.mjs ├── Brewfile ├── jsconfig.json ├── .husky └── pre-commit ├── babel.config.js ├── script ├── stop.sh ├── rebuild.sh ├── start.sh ├── send-letters.js ├── bootstrap ├── drop-db.js ├── stringify.js └── create-db.js ├── .prettierignore ├── Renewable Capitol Act.png ├── run-metrics.sh ├── src ├── assets │ ├── images │ │ ├── mailbox.png │ │ ├── amplifysumm.png │ │ ├── cardimage.jpeg │ │ ├── shellmound.jpeg │ │ ├── StepsGraphic.png │ │ ├── amplify-web-3.png │ │ ├── Ohlone-village1.jpeg │ │ ├── Large Hero Banner.png │ │ ├── SOGOREA-TE-LAND-TRUS.jpeg │ │ └── Screen Shot 2022-11-22 at 12.57.10 AM.png │ ├── logo │ │ ├── Amplify-Email.png │ │ ├── Amplify-Website-Vertical-RGB.jpg │ │ ├── Amplify-Website-Vertical-RGB.png │ │ ├── Amplify-Newsletter-Vertical-RGB.jpg │ │ ├── Amplify-Newsletter-Vertical-RGB.png │ │ ├── Amplify-Website-Horizontal-RGB.jpg │ │ ├── Amplify-Website-Horizontal-RGB.png │ │ ├── Amplify-Newsletter-Horizontal-RGB.jpg │ │ └── Amplify-Newsletter-Horizontal-RGB.png │ └── scm │ │ ├── images │ │ ├── campaign-img-1.webp │ │ ├── campaign-img-2.webp │ │ ├── campaign-img-3.webp │ │ ├── campaign-logo.webp │ │ └── campaign-background.webp │ │ └── text │ │ └── text.json ├── views │ ├── About.vue │ ├── 1-IN-THIS-DIR.md │ ├── 0-DO-NOT-NAME-FILE.md │ ├── 2-WITH-LOWER-CASE.md │ ├── CompletePage.vue │ ├── Home.vue │ └── Campaign.vue ├── .eslintrc.json ├── components │ ├── 1-IN-THIS-DIR.md │ ├── 0-DO-NOT-NAME-FILE.md │ ├── 2-WITH-LOWER-CASE.md │ ├── AuthNav.vue │ ├── AuthenticationButton.vue │ ├── LogoutButton.vue │ ├── SignupButton.vue │ ├── LoginButton.vue │ ├── AppHeader.vue │ ├── HomeHero.vue │ ├── CampaignCard.vue │ ├── CauseCarousel.vue │ ├── AppFooter.vue │ ├── CampaignCards.vue │ ├── RepresentativeCard.vue │ ├── ActionComplete.vue │ ├── CampaignBlurb.vue │ └── CampaignHero.vue ├── router │ ├── scroll-behavior.js │ └── index.js ├── plugins │ └── vuetify.js ├── styles │ └── global.less ├── configs │ └── theme.js ├── utilities │ └── vue_logger.js ├── App.vue ├── registerServiceWorker.js ├── main.js └── auth │ └── auth0-plugin.js ├── static.json ├── auth_config.json ├── .prettierrc ├── .github ├── CODEOWNERS ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── new-feature.md │ ├── backend-onboarding.md │ ├── frontend-onboarding.md │ ├── batch-templade.md │ ├── bug.md │ └── design issue.md ├── workflows │ ├── welcome_message.yml │ ├── docker-build-artifact.yml │ ├── lint.yml │ ├── check-formatting.yml │ ├── programequity_slack.yml │ ├── get-originaltime.yml │ ├── build.yml │ ├── unit-tests.yml │ ├── codeql-analysis.yml │ ├── labeler.yml │ ├── finish-hackpod.yml │ ├── emojiPR.yml │ ├── integration-tests.yaml │ ├── issue-metrics.yml │ └── scorecards-analysis.yml └── mention-to-slack.yml ├── server ├── db │ ├── _migration.stub.js │ ├── _seed.stub.js │ ├── migrations │ │ ├── 20241013134620_add_tracking_to_letters.js │ │ ├── 20240528021821_add_letter_merge_vars.js │ │ ├── 20240528133030_adjust_letter_data_types.js │ │ ├── 20220425034702_remove-campaigns-letters_counter.js │ │ ├── 20210729063333_create-variants-table.js │ │ ├── 20220227142912_add-campaign-fields.js │ │ ├── 20230309195953_update-campaign-model.js │ │ ├── 20210514142754_create-volunteeers-table.js │ │ ├── 20240519161955_campaign_table_updates.js │ │ ├── 20210729053119_create-transactions-table.js │ │ ├── 20231204193051_create-admins-table.js │ │ ├── 20230301220841_create-error_logs-table.js │ │ ├── 20220420234746_letter_versions-office_division-constraints.js │ │ ├── 20210514201314_create-sent-letters-table.js │ │ ├── 20211230165819_harden-volunteers-table.js │ │ ├── 20240817143934_create_letter_templates_table.js │ │ ├── 20211230165827_harden-letter_versions-table.js │ │ ├── 20210513215151_create-campaigns-table.js │ │ ├── 20211224223204_match-production-letter_versions-table.js │ │ ├── 20220413154827_rename-volunteers-table-to-constituents.js │ │ ├── 20210514145630_create-letter-versions-table.js │ │ ├── 20231022014507_update_constituent_transaction_letters_tables.js │ │ ├── 20211230165803_harden-campaigns-table.js │ │ └── 20211222210238_match-production-campaigns-table.js │ ├── util.js │ ├── index.js │ ├── models │ │ ├── letter-template.js │ │ ├── _base.js │ │ ├── admin.js │ │ ├── error_log.js │ │ ├── transaction.js │ │ ├── campaign.js │ │ ├── constituent.js │ │ ├── letter.js │ │ └── letter-version.js │ └── seeds │ │ └── development │ │ ├── 05-seed-transactions-table.js │ │ ├── 03-seed-admins-table.js │ │ └── 04-seed-constituents-table.js ├── server.js ├── routes │ └── api │ │ ├── twilio.js │ │ ├── v1 │ │ ├── v1.js │ │ ├── campaigns │ │ │ └── index.js │ │ └── letter-templates │ │ │ └── index.js │ │ ├── letter_versions.js │ │ ├── authentication.js │ │ ├── event_logger.js │ │ └── campaigns.js ├── auth │ ├── messages │ │ └── messages.service.js │ ├── check-jwt.js │ └── config │ │ └── env.dev.js ├── lib │ ├── encrypt.js │ ├── handlebars.js │ ├── sendgrid.js │ ├── stripe.js │ └── lob.js ├── __tests__ │ ├── integration │ │ ├── checkout.test.js │ │ ├── server │ │ │ └── routes │ │ │ │ └── api │ │ │ │ └── v1 │ │ │ │ └── campaigns │ │ │ │ └── campaign_put_endpoint.test.js │ │ └── axios-cache-interceptor.test.js │ ├── unit │ │ └── event_logger.test.js │ └── fixtures │ │ └── stripe │ │ ├── payment-failure.json │ │ └── payment-success.json ├── app.js ├── utilities │ └── winston_logger.js └── api.js ├── .devcontainer ├── post-create-command.sh ├── on-create-command.sh ├── Dockerfile ├── first-run-notice.txt ├── docker-compose.yml └── devcontainer.json ├── .eslintrc.json ├── vue.config.js ├── LICENSE ├── SECURITY.md ├── CHECKOUT.md ├── .env ├── shared └── presenters │ └── payment-presenter.js ├── AmplifyApp.md ├── knexfile.js ├── .gitignore └── README.md /app.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /start: -------------------------------------------------------------------------------- 1 | ./script/start.sh -------------------------------------------------------------------------------- /stop: -------------------------------------------------------------------------------- 1 | ./script/stop.sh -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.13.1 2 | -------------------------------------------------------------------------------- /docs/test.md: -------------------------------------------------------------------------------- 1 | This is a test. -------------------------------------------------------------------------------- /rebuild: -------------------------------------------------------------------------------- 1 | ./script/rebuild.sh -------------------------------------------------------------------------------- /public/Campaigns/test.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.docker/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /roiScript/issue_timestamp.log: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "nodenv" 2 | brew "postgresql" 3 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "vueCompilerOptions": { 3 | "target": 2 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run precommit 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | } 4 | -------------------------------------------------------------------------------- /script/stop.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | docker compose -f .docker/docker-compose.yml down 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/favicon.png -------------------------------------------------------------------------------- /script/rebuild.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | docker compose -f .docker/docker-compose.yml build amplify -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore build artifacts 2 | /dist/ 3 | /.vscode/ 4 | # Ignore all Markdown files 5 | *.md 6 | -------------------------------------------------------------------------------- /Renewable Capitol Act.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/Renewable Capitol Act.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /run-metrics.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | /opt/hostedtoolcache/node/14.21.3/x64/bin/node /github/workspace/issue-metrics.js 3 | -------------------------------------------------------------------------------- /src/assets/images/mailbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/mailbox.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/android-chrome-256x256.png -------------------------------------------------------------------------------- /src/assets/images/amplifysumm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/amplifysumm.png -------------------------------------------------------------------------------- /src/assets/images/cardimage.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/cardimage.jpeg -------------------------------------------------------------------------------- /src/assets/images/shellmound.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/shellmound.jpeg -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Email.png -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "dist", 3 | "clean_urls": true, 4 | "routes": { 5 | "/**": "index.html" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "prettier", "plugin:vue/vue3-recommended"], 3 | "rules": {} 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/images/StepsGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/StepsGraphic.png -------------------------------------------------------------------------------- /src/assets/images/amplify-web-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/amplify-web-3.png -------------------------------------------------------------------------------- /public/Campaigns/Clean Futures Act.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/Campaigns/Clean Futures Act.png -------------------------------------------------------------------------------- /src/assets/images/Ohlone-village1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/Ohlone-village1.jpeg -------------------------------------------------------------------------------- /src/assets/images/Large Hero Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/Large Hero Banner.png -------------------------------------------------------------------------------- /src/assets/scm/images/campaign-img-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/scm/images/campaign-img-1.webp -------------------------------------------------------------------------------- /src/assets/scm/images/campaign-img-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/scm/images/campaign-img-2.webp -------------------------------------------------------------------------------- /src/assets/scm/images/campaign-img-3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/scm/images/campaign-img-3.webp -------------------------------------------------------------------------------- /src/assets/scm/images/campaign-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/scm/images/campaign-logo.webp -------------------------------------------------------------------------------- /src/assets/images/SOGOREA-TE-LAND-TRUS.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/SOGOREA-TE-LAND-TRUS.jpeg -------------------------------------------------------------------------------- /src/assets/scm/images/campaign-background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/scm/images/campaign-background.webp -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Website-Vertical-RGB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Website-Vertical-RGB.jpg -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Website-Vertical-RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Website-Vertical-RGB.png -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Newsletter-Vertical-RGB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Newsletter-Vertical-RGB.jpg -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Newsletter-Vertical-RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Newsletter-Vertical-RGB.png -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Website-Horizontal-RGB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Website-Horizontal-RGB.jpg -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Website-Horizontal-RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Website-Horizontal-RGB.png -------------------------------------------------------------------------------- /public/Campaigns/Green New Deal for New York Act.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/Campaigns/Green New Deal for New York Act.png -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Newsletter-Horizontal-RGB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Newsletter-Horizontal-RGB.jpg -------------------------------------------------------------------------------- /src/assets/logo/Amplify-Newsletter-Horizontal-RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/logo/Amplify-Newsletter-Horizontal-RGB.png -------------------------------------------------------------------------------- /auth_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "dev-37yoa2h5.us.auth0.com", 3 | "clientId": "INfcSwGwX68uZlWy2FlIh3KbJiFKDjw7", 4 | "audience": "", 5 | "serverUrl": "" 6 | } 7 | -------------------------------------------------------------------------------- /public/Campaigns/Energy Efficiency, Equity and Jobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/Campaigns/Energy Efficiency, Equity and Jobs.png -------------------------------------------------------------------------------- /public/Campaigns/Proposed Actions on EJ Communities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/Campaigns/Proposed Actions on EJ Communities.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "semi": false, 6 | "singleQuote": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /public/Campaigns/Climate and Community Investment Act.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/Campaigns/Climate and Community Investment Act.png -------------------------------------------------------------------------------- /script/start.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | docker compose -f .docker/docker-compose.yml up -d 4 | 5 | echo "Entering container..." 6 | 7 | docker exec -it amplify_app /bin/sh 8 | -------------------------------------------------------------------------------- /src/assets/images/Screen Shot 2022-11-22 at 12.57.10 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/src/assets/images/Screen Shot 2022-11-22 at 12.57.10 AM.png -------------------------------------------------------------------------------- /public/Campaigns/Climate Leadership and Community Protection Act.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSourceFellows/amplify/HEAD/public/Campaigns/Climate Leadership and Community Protection Act.png -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # All PRs will require one dev team approval 2 | * @ProgramEquity/dev-leads 3 | 4 | # Design & Devs should be able to approve Markdown changes 5 | *.md @ProgramEquity/dev-leads @ProgramEquity/design 6 | -------------------------------------------------------------------------------- /src/views/1-IN-THIS-DIR.md: -------------------------------------------------------------------------------- 1 | Do not name files in this directory with lowercase names 2 | 3 | For example: 4 | 5 | myFile.vue <~~~~~~ BAD BAD Not good 6 | 7 | MyFile.vue <~~~~~~ Alrighty then, now we're talking, good job 8 | -------------------------------------------------------------------------------- /server/db/_migration.stub.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(knex) { 3 | await knex.schema.doSomethingForReal() 4 | }, 5 | 6 | async down(knex) { 7 | await knex.schema.doSomethingForReal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/1-IN-THIS-DIR.md: -------------------------------------------------------------------------------- 1 | Do not name files in this directory with lowercase names 2 | 3 | For example: 4 | 5 | myFile.vue <~~~~~~ BAD BAD Not good 6 | 7 | MyFile.vue <~~~~~~ Alrighty then, now we're talking, good job 8 | -------------------------------------------------------------------------------- /src/views/0-DO-NOT-NAME-FILE.md: -------------------------------------------------------------------------------- 1 | Do not name files in this directory with lowercase names 2 | 3 | For example: 4 | 5 | myFile.vue <~~~~~~ BAD BAD Not good 6 | 7 | MyFile.vue <~~~~~~ Alrighty then, now we're talking, good job 8 | -------------------------------------------------------------------------------- /src/views/2-WITH-LOWER-CASE.md: -------------------------------------------------------------------------------- 1 | Do not name files in this directory with lowercase names 2 | 3 | For example: 4 | 5 | myFile.vue <~~~~~~ BAD BAD Not good 6 | 7 | MyFile.vue <~~~~~~ Alrighty then, now we're talking, good job 8 | -------------------------------------------------------------------------------- /.devcontainer/post-create-command.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Install all dependencies 6 | npm install 7 | 8 | # Setup the database 9 | npm run db:create 10 | npm run db:migrate 11 | npm run db:seed 12 | -------------------------------------------------------------------------------- /src/components/0-DO-NOT-NAME-FILE.md: -------------------------------------------------------------------------------- 1 | Do not name files in this directory with lowercase names 2 | 3 | For example: 4 | 5 | myFile.vue <~~~~~~ BAD BAD Not good 6 | 7 | MyFile.vue <~~~~~~ Alrighty then, now we're talking, good job 8 | -------------------------------------------------------------------------------- /src/components/2-WITH-LOWER-CASE.md: -------------------------------------------------------------------------------- 1 | Do not name files in this directory with lowercase names 2 | 3 | For example: 4 | 5 | myFile.vue <~~~~~~ BAD BAD Not good 6 | 7 | MyFile.vue <~~~~~~ Alrighty then, now we're talking, good job 8 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const app = require('./app') 2 | const port = parseInt(process.env.PORT, 10) || 8080 3 | 4 | const server = app.listen(port, () => { 5 | console.log(`App started at http://localhost:${port}/`) 6 | }) 7 | module.exports = server 8 | -------------------------------------------------------------------------------- /src/router/scroll-behavior.js: -------------------------------------------------------------------------------- 1 | const scrollBehavior = (to, from, savedPosition) => { 2 | if (savedPosition) { 3 | return savedPosition 4 | } else { 5 | return { x: 0, y: 0 } 6 | } 7 | } 8 | 9 | export default scrollBehavior 10 | -------------------------------------------------------------------------------- /.devcontainer/on-create-command.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # Copy our welcome message 6 | if [ -f "./.devcontainer/first-run-notice.txt" ]; then 7 | sudo cp --force ./.devcontainer/first-run-notice.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt 8 | fi 9 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Update 'VARIANT' to pick a default version of Node.js: 16, 14, 12. 2 | # Append -bullseye to pin to local arm64/Apple Silicon. 3 | # Append -buster to pin to Debian. 4 | ARG VARIANT=16-buster 5 | 6 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 7 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.devcontainer/first-run-notice.txt: -------------------------------------------------------------------------------- 1 | 👋 Welcome to developing ProgramEquity/amplify in GitHub Codespaces! 2 | 3 | 👢 Your environment is all set up. 4 | 5 | 🚀 To get started, start the server by running `npm run dev`. 6 | 7 | 🧪 Verify the app at http://localhost:8080/ 8 | 9 | ❓ Having troubles? Visit #devs in Slack for support! 10 | -------------------------------------------------------------------------------- /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | 3 | ARG workdir=/src/amplify 4 | WORKDIR ${workdir} 5 | 6 | # Adding package.json first will cache our dependencies so 7 | # that they do not have to be re-installed when the image rebuilds 8 | ADD package.json ${workdir} 9 | 10 | RUN cd ${workdir} 11 | RUN npm i 12 | 13 | EXPOSE 8080 14 | -------------------------------------------------------------------------------- /src/components/AuthNav.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": ["eslint:recommended", "prettier"], 9 | "parserOptions": { 10 | "ecmaVersion": 13, 11 | "parser": "@babel/eslint-parser" 12 | }, 13 | "rules": {}, 14 | "ignorePatterns": ["/dist/**/*.js"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 20 # default is 5 8 | 9 | - package-ecosystem: 'github-actions' 10 | directory: '/' 11 | schedule: 12 | interval: monthly 13 | open-pull-requests-limit: 10 # default is 5 14 | -------------------------------------------------------------------------------- /server/db/_seed.stub.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async seed(knex) { 3 | // Deletes ALL existing entries 4 | await knex('table_name').del() 5 | 6 | // Inserts seed entries 7 | await knex('table_name').insert([ 8 | { id: 1, colName: 'rowValue1' }, 9 | { id: 2, colName: 'rowValue2' }, 10 | { id: 3, colName: 'rowValue3' } 11 | ]) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/db/migrations/20241013134620_add_tracking_to_letters.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(knex) { 3 | await knex.schema.alterTable('letters', (table) => { 4 | table.uuid('tracking_number') 5 | }) 6 | }, 7 | 8 | async down(knex) { 9 | await knex.schema.alterTable('letters', (table) => { 10 | table.dropColumn('tracking_number') 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/db/migrations/20240528021821_add_letter_merge_vars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(knex) { 3 | await knex.schema.alterTable('letters', (table) => { 4 | table.jsonb('merge_variables').nullable() 5 | }) 6 | }, 7 | 8 | async down(knex) { 9 | await knex.schema.alterTable('letters', (table) => { 10 | table.dropColumn('merge_variables') 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Feature 3 | about: For singular features 4 | title: "" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | Some description 12 | 13 | ### Spec 14 | What it do. Acceptance criteria and all that. 15 | 16 | ### Copilot Prompts 17 | A few statements or questions for Copilot 18 | 19 | ### Target Date: 📆 20 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | // src/plugins/vuetify.js 2 | 3 | import Vue from 'vue' 4 | import Vuetify from 'vuetify' 5 | import 'vuetify/dist/vuetify.min.css' 6 | import { lightTheme } from '@/configs/theme.js' 7 | 8 | Vue.use(Vuetify) 9 | 10 | const vuetify = new Vuetify({ 11 | theme: { 12 | themes: { light: lightTheme }, 13 | options: { customProperties: true } 14 | } 15 | }) 16 | 17 | export default vuetify 18 | -------------------------------------------------------------------------------- /src/components/AuthenticationButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/styles/global.less: -------------------------------------------------------------------------------- 1 | // Theme Names 2 | @primary: var(--v-primary-base); 3 | @primary-alt: var(--v-primary-alt-base); 4 | @secondary: var(--v-secondary-base); 5 | @secondary-alt: var(--v-secondary-alt-base); 6 | @tertiary: var(--v-tertiary-base); 7 | @tertiary-alt: var(--v-tertiary-alt-base); 8 | @dark: var(--v-dark-base); 9 | @med-gray: var(--v-med-gray-base); 10 | @light-gray: var(--v-light-gray-base); 11 | @white: var(--v-white-base); 12 | -------------------------------------------------------------------------------- /src/views/CompletePage.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /src/components/LogoutButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /server/routes/api/twilio.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const express = require('express') 4 | const { createClient } = require('../../db') 5 | const router = express.Router() 6 | const db = createClient() 7 | // // need to `npm install --save twilio` first 8 | const twilio = require('twilio') 9 | const accountSid = process.env.TWILIO_ACCOUNT_SID 10 | const authToken = process.env.TWILIO_AUTH_TOKEN 11 | 12 | module.exports = router 13 | -------------------------------------------------------------------------------- /src/components/SignupButton.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /server/auth/messages/messages.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Service Methods 3 | */ 4 | 5 | const getPublicMessage = () => { 6 | return { 7 | message: "The API doesn't require an access token to share this message." 8 | } 9 | } 10 | 11 | const getProtectedMessage = () => { 12 | return { 13 | message: 'The API successfully validated your access token.' 14 | } 15 | } 16 | 17 | module.exports = { 18 | getPublicMessage, 19 | getProtectedMessage 20 | } 21 | -------------------------------------------------------------------------------- /src/configs/theme.js: -------------------------------------------------------------------------------- 1 | // Config for Vuetify color themes 2 | // You can define colors here for branding overrides. 3 | 4 | const lightTheme = { 5 | primary: '#083d77', 6 | 'primary-alt': '#38618c', 7 | secondary: '#fe5e41', 8 | 'secondary-alt': '#fcd7ad', 9 | tertiary: '#F3B700', 10 | 'tertiary-alt': '#080085', 11 | dark: '#131313', 12 | 'med-gray': '#bbbbbb', 13 | 'light-gray': '#eeeeee', 14 | white: '#ffffff' 15 | } 16 | 17 | export { lightTheme } 18 | -------------------------------------------------------------------------------- /server/lib/encrypt.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | 3 | // Encrypt password 4 | async function encryptPassword(password) { 5 | const hash = await bcrypt.hash(password, 10) 6 | return hash 7 | } 8 | 9 | // Check if password is valid 10 | async function validatePassword(inputPassword, storedPassword) { 11 | const isMatch = await bcrypt.compare(inputPassword, storedPassword) 12 | return isMatch 13 | } 14 | 15 | module.exports = { encryptPassword, validatePassword } 16 | -------------------------------------------------------------------------------- /roiScript/log-branch.mjs: -------------------------------------------------------------------------------- 1 | import github from '@actions/github' 2 | 3 | const issueNumber = process.env.ISSUE_NUMBER 4 | const comment = `Branch: [issue-${issueNumber}](https://github.com/ProgramEquity/amplify/tree/issue-${issueNumber})` 5 | 6 | const octokit = github.getOctokit(process.env.GH_TOKEN) 7 | 8 | await octokit.rest.issues.createComment({ 9 | owner: github.context.repo.owner, 10 | repo: github.context.repo.repo, 11 | issue_number: issueNumber, 12 | body: comment 13 | }) 14 | -------------------------------------------------------------------------------- /server/db/migrations/20240528133030_adjust_letter_data_types.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(knex) { 3 | await knex.schema.alterTable('letters', (table) => { 4 | table.dropColumn('letter_template') 5 | }) 6 | 7 | await knex.schema.alterTable('letters', (table) => { 8 | table.text('letter_template') 9 | }) 10 | }, 11 | 12 | async down(knex) { 13 | knex.schema.alterTable('letters', (table) => { 14 | table.dropColumn('letter_template') 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/db/util.js: -------------------------------------------------------------------------------- 1 | function getEnv() { 2 | let targetEnv = process.env.NODE_ENV || 'development' 3 | // Prefer `--env test` and `--env=test` command line arguments if provided 4 | process.argv.forEach((val, i) => { 5 | if (val === '--env' && process.argv[i + 1]) { 6 | targetEnv = process.argv[i + 1] 7 | } else if (val.startsWith('--env=') && val.length > 6) { 8 | targetEnv = val.slice(6) 9 | } 10 | }) 11 | return targetEnv 12 | } 13 | 14 | module.exports = { 15 | getEnv 16 | } 17 | -------------------------------------------------------------------------------- /server/routes/api/v1/v1.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const campaigns = require('./campaigns') // If the routes file is named index.js, we can omit it when we do the import instead of writing './campaigns/index'. 3 | const letterTemplates = require('./letter-templates') 4 | 5 | const v1Router = express.Router() 6 | 7 | // Create the final piece of the url /campaigns 8 | v1Router.use('/campaigns', campaigns) 9 | v1Router.use('/letter_templates', letterTemplates) 10 | 11 | module.exports = v1Router 12 | -------------------------------------------------------------------------------- /roiScript/issue-metrics.mjs: -------------------------------------------------------------------------------- 1 | import github from '@actions/github' 2 | 3 | const pullNumber = process.env.PR_NUMBER 4 | const timeDiff = process.env.TIME_DIFF ? process.env.TIME_DIFF : 1 5 | const comment = `Time from assignment to PR for #${pullNumber}: ${timeDiff} seconds` 6 | 7 | const octokit = github.getOctokit(process.env.GH_TOKEN) 8 | 9 | await octokit.rest.issues.createComment({ 10 | owner: github.context.repo.owner, 11 | repo: github.context.repo.repo, 12 | issue_number: pullNumber, 13 | body: comment 14 | }) 15 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | outputDir: path.resolve(__dirname, './dist'), 5 | 6 | devServer: { 7 | proxy: { 8 | '/api': { 9 | target: 'http://localhost:5000', 10 | changeOrigin: true 11 | } 12 | } 13 | }, 14 | 15 | pluginOptions: { 16 | 'style-resources-loader': { 17 | preProcessor: 'less', 18 | patterns: [path.resolve(__dirname, './src/styles/global.less')] 19 | } 20 | }, 21 | 22 | transpileDependencies: ['vuetify'] 23 | } 24 | -------------------------------------------------------------------------------- /server/auth/check-jwt.js: -------------------------------------------------------------------------------- 1 | const { expressjwt: jwt } = require('express-jwt') 2 | const jwksRsa = require('jwks-rsa') 3 | const { domain, audience } = require('./config/env.dev') 4 | 5 | const checkJwt = jwt({ 6 | secret: jwksRsa.expressJwtSecret({ 7 | cache: true, 8 | rateLimit: true, 9 | jwksRequestsPerMinute: 5, 10 | jwksUri: `https://${domain}/.well-known/jwks.json` 11 | }), 12 | 13 | audience: audience, 14 | issuer: `https://${domain}/`, 15 | algorithms: ['RS256'] 16 | }) 17 | 18 | module.exports = { 19 | checkJwt 20 | } 21 | -------------------------------------------------------------------------------- /server/db/migrations/20220425034702_remove-campaigns-letters_counter.js: -------------------------------------------------------------------------------- 1 | const tableName = 'campaigns' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Alter the table 6 | await knex.schema.alterTable(tableName, function (table) { 7 | // Drop columns 8 | table.dropColumn('letters_counter') 9 | }) 10 | }, 11 | 12 | async down(knex) { 13 | // Alter the table 14 | await knex.schema.alterTable(tableName, function (table) { 15 | // Add columns 16 | table.integer('letters_counter').notNullable() 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utilities/vue_logger.js: -------------------------------------------------------------------------------- 1 | import VueLogger from 'vue-logger-plugin' 2 | import axios from 'axios' 3 | 4 | // after hook to send log messages to api endpoint 5 | const ServerLogHook = { 6 | run: (event) => { 7 | axios.post('/api/event_logger/log', { 8 | severity: event.level, 9 | data: event.argumentArray 10 | }) 11 | } 12 | } 13 | 14 | // define options 15 | const options = { 16 | enabled: true, 17 | level: 'debug', 18 | afterHooks: [ServerLogHook] 19 | } 20 | 21 | // export logger with applied options 22 | export default new VueLogger(options) 23 | -------------------------------------------------------------------------------- /server/lib/handlebars.js: -------------------------------------------------------------------------------- 1 | // Wrapper for Handlebars template engine 2 | const handlebars = require('handlebars') 3 | 4 | class HandlebarsError extends Error { 5 | constructor(message) { 6 | super(message) 7 | this.name = 'HandlebarsError' 8 | } 9 | } 10 | 11 | class Handlebars { 12 | static render(mergeVariables, html) { 13 | try { 14 | const template = handlebars.compile(html) 15 | 16 | return template(mergeVariables) 17 | } catch (err) { 18 | throw new HandlebarsError(err.message) 19 | } 20 | } 21 | } 22 | 23 | module.exports = Handlebars 24 | -------------------------------------------------------------------------------- /server/routes/api/letter_versions.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const LetterVersion = require('../../db/models/letter-version') 4 | 5 | router.get('/:campaignId', async (req, res) => { 6 | const campaignId = req.params.campaignId 7 | try { 8 | const letterVersions = await LetterVersion.query().where( 9 | 'campaign_id', 10 | campaignId 11 | ) 12 | res.send(letterVersions) 13 | } catch (error) { 14 | console.log(error) 15 | res.status(500).send({ error: 'Whoops' }) 16 | } 17 | }) 18 | 19 | module.exports = router 20 | -------------------------------------------------------------------------------- /server/db/index.js: -------------------------------------------------------------------------------- 1 | const knex = require('knex') 2 | const { getEnv } = require('./util') 3 | const knexfile = require('../../knexfile') 4 | 5 | function createClient(config) { 6 | return knex(config || getConfig()) 7 | } 8 | 9 | function getConfig(env) { 10 | const targetEnv = env || getEnv() 11 | const config = knexfile[targetEnv] 12 | 13 | if (!config) { 14 | throw new Error( 15 | `No config found in "knexfile.js" for environment "${targetEnv}"` 16 | ) 17 | } 18 | 19 | return config 20 | } 21 | 22 | module.exports = { 23 | createClient, 24 | getConfig, 25 | getEnv 26 | } 27 | -------------------------------------------------------------------------------- /server/db/migrations/20210729063333_create-variants-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'variants' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | await knex.schema.createTable(tableName, (table) => { 6 | table.increments() 7 | table.float('price') 8 | table.string('sku') 9 | table.integer('quantity').defaultTo(1) 10 | table.string('description') 11 | table.integer('campaign_id').notNullable() 12 | table.timestamps() 13 | 14 | table.foreign('campaign_id').references('campaigns.id') 15 | }) 16 | }, 17 | 18 | async down(knex) { 19 | await knex.schema.dropTable(tableName) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/db/models/letter-template.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base') 2 | 3 | class LetterTemplate extends BaseModel { 4 | static get tableName() { 5 | return 'letter_templates' 6 | } 7 | 8 | static get jsonSchema() { 9 | return { 10 | type: 'object', 11 | required: ['lob_template_id', 'sendgrid_template_id', 'merge_variables'], 12 | 13 | properties: { 14 | subject: { type: 'string', minLength: 1, maxLength: 255 }, 15 | name: { type: 'string', minLength: 1, maxLength: 255 }, 16 | merge_variables: { type: 'object' } 17 | } 18 | } 19 | } 20 | } 21 | 22 | module.exports = LetterTemplate 23 | -------------------------------------------------------------------------------- /server/db/migrations/20220227142912_add-campaign-fields.js: -------------------------------------------------------------------------------- 1 | const tableName = 'campaigns' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | await knex.schema.alterTable(tableName, function (table) { 6 | // Add nullable columns 7 | table.text('logo_url') 8 | table.integer('letters_goal').unsigned() 9 | table.integer('donation_goal').unsigned() 10 | }) 11 | }, 12 | 13 | async down(knex) { 14 | await knex.schema.alterTable(tableName, function (table) { 15 | // Drop columns 16 | table.dropColumn('donation_goal') 17 | table.dropColumn('letters_goal') 18 | table.dropColumn('logo_url') 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/__tests__/integration/checkout.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | require('dotenv').config() 4 | const request = require('supertest') 5 | const express = require('express') 6 | const apiRouter = require('../../api') 7 | 8 | // Create a test application 9 | const app = express() 10 | app.use('/api', apiRouter) 11 | 12 | afterEach(() => { 13 | jest.clearAllMocks() 14 | }) 15 | 16 | describe('GET /api/...', () => { 17 | test.skip('fake test', async () => { 18 | expect(true).toBe(true) 19 | }) 20 | }) 21 | 22 | afterAll(async () => { 23 | await new Promise((resolve) => setTimeout(() => resolve(), 500)) // avoid jest open handle error 24 | }) 25 | -------------------------------------------------------------------------------- /roiScript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "amplify-script", 4 | "version": "1.0.0", 5 | "description": "The script for amplify", 6 | "author": "ProgramEquity", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/ProgramEquity/amplify.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/ProgramEquity/amplify/issues" 14 | }, 15 | "homepage": "https://www.programequity.com/", 16 | "main": "server/server.js", 17 | "scripts": {}, 18 | "engines": { 19 | "node": "16.x" 20 | }, 21 | "dependencies": { 22 | "@actions/github": "^6.0.0", 23 | "@notionhq/client": "^2.2.14" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/db/migrations/20230309195953_update-campaign-model.js: -------------------------------------------------------------------------------- 1 | const tableName = 'campaigns' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | await knex.schema.alterTable(tableName, (table) => { 6 | // Add nullable columns 7 | table.string('campaign_tagline').nullable() 8 | table.string('campaign_text').nullable() 9 | table.string('supplemental_text').nullable() 10 | }) 11 | }, 12 | 13 | async down(knex) { 14 | await knex.schema.alterTable(tableName, (table) => { 15 | // Drop columns 16 | table.dropColumn('supplemental_text') 17 | table.dropColumn('campaign_text') 18 | table.dropColumn('campaign_tagline') 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/db/seeds/development/05-seed-transactions-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async seed(knex) { 3 | await knex('transactions').del() 4 | 5 | await knex('transactions').insert([ 6 | { 7 | stripe_transaction_id: 'pi_test-success-1234', 8 | amount: 10000, 9 | currency: 'usd', 10 | status: null, 11 | payment_method: 'credit_card', 12 | constituent_id: 1 13 | }, 14 | { 15 | stripe_transaction_id: 'pi_test-failed-5678', 16 | amount: 5000, 17 | currency: 'usd', 18 | status: null, 19 | payment_method: 'credit_card', 20 | constituent_id: 2 21 | } 22 | ]) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/db/seeds/development/03-seed-admins-table.js: -------------------------------------------------------------------------------- 1 | const { encryptPassword } = require('../../../lib/encrypt') 2 | 3 | module.exports = { 4 | async seed(knex) { 5 | // Deletes all existing entries 6 | await knex('admins').del() 7 | 8 | const password = await encryptPassword('password') 9 | 10 | // Insert seed entries 11 | await knex('admins').insert([ 12 | { 13 | first_name: 'John', 14 | last_name: 'Doe', 15 | email: 'johndoe@gmail.com', 16 | password 17 | }, 18 | { 19 | first_name: 'Jane', 20 | last_name: 'Doe', 21 | email: 'janedoe@gmail.com', 22 | password 23 | } 24 | ]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/db/migrations/20210514142754_create-volunteeers-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'volunteers' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Create the table 6 | await knex.schema.createTable(tableName, (table) => { 7 | // Auto-incrementing non-nullable unsigned integer primary key "id" field 8 | table.increments() 9 | 10 | // Simple fields 11 | table.string('name').notNullable() 12 | table.string('email').notNullable() 13 | table.text('physical_address').notNullable() 14 | 15 | // Unique indexes 16 | table.unique(['email']) 17 | }) 18 | }, 19 | 20 | async down(knex) { 21 | // Drop the table 22 | await knex.schema.dropTable(tableName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/db/migrations/20240519161955_campaign_table_updates.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(knex) { 3 | await knex.schema.alterTable('campaigns', (table) => { 4 | table.jsonb('representatives').nullable() 5 | table.jsonb('assets').nullable() 6 | table.dropColumn('campaign_text') 7 | table.dropColumn('supplemental_text') 8 | }) 9 | 10 | await knex.schema.alterTable('campaigns', (table) => { 11 | table.text('campaign_text') 12 | table.text('supplemental_text') 13 | }) 14 | }, 15 | 16 | async down(knex) { 17 | await knex.schema.alterTable('campaigns', (table) => { 18 | table.dropColumn('representatives') 19 | table.dropColumn('assets') 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/db/seeds/development/04-seed-constituents-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async seed(knex) { 3 | await knex('constituents').del() 4 | 5 | await knex('constituents').insert([ 6 | { 7 | email: 'tester@gmail.com', 8 | first_name: 'Baby', 9 | last_name: 'Cakes', 10 | address_line_1: '123 Fake St', 11 | city: 'Chicago', 12 | state: 'IL', 13 | zip: '60618' 14 | }, 15 | { 16 | email: 'seadog@gmail.com', 17 | first_name: 'Fish', 18 | last_name: 'Cakes', 19 | address_line_1: '420 Lakeview Terrace', 20 | city: 'Grand Rapids', 21 | state: 'MI', 22 | zip: '49501' 23 | } 24 | ]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/routes/api/authentication.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const express = require('express') 3 | const router = express.Router() 4 | 5 | const { 6 | getPublicMessage, 7 | getProtectedMessage 8 | } = require('../../auth/messages/messages.service') 9 | const { checkJwt } = require('../../auth/check-jwt') 10 | 11 | router.get('/isAuthenticated', checkJwt, (req, res) => { 12 | res.send(true) 13 | }) 14 | 15 | router.get('/public-message', (req, res) => { 16 | const message = getPublicMessage() 17 | res.status(200).send(message) 18 | }) 19 | 20 | router.get('/protected-message', checkJwt, (req, res) => { 21 | const message = getProtectedMessage() 22 | res.status(200).send(message) 23 | }) 24 | 25 | module.exports = router 26 | -------------------------------------------------------------------------------- /server/db/models/_base.js: -------------------------------------------------------------------------------- 1 | const { Model, snakeCaseMappers } = require('objection') 2 | const { createClient } = require('../') 3 | 4 | class BaseModel extends Model { 5 | static get modelPaths() { 6 | return [__dirname] 7 | } 8 | 9 | // Maps column_name in postgres to columnName in the app. 10 | // Queries must still use snake_case if sql is passed into a query method chain 11 | // For ex. Constituent.query().where('first_name', 'Jennifer') 12 | static get columnNameMappers() { 13 | return snakeCaseMappers() 14 | } 15 | } 16 | 17 | // One-time configuration to link all Objection models derived from this class with Knex 18 | BaseModel.knex(createClient()) 19 | 20 | // Export the base class 21 | module.exports = BaseModel 22 | -------------------------------------------------------------------------------- /script/send-letters.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const letterData = require('./catchup.js') 3 | 4 | ;(async () => { 5 | const data = JSON.parse(letterData) 6 | 7 | for (const transaction of data) { 8 | try { 9 | const response = await axios.post( 10 | 'https://amplify-hooks-0194518485a8.herokuapp.com/api/checkout/process-transaction', 11 | { 12 | data: { 13 | object: { 14 | id: transaction.stripe_id 15 | } 16 | }, 17 | type: 'payment_intent.succeeded' 18 | } 19 | ) 20 | 21 | if (response.statusCode == 201) console.log(transaction.stripe_id) 22 | } catch (error) { 23 | console.error(error) 24 | } 25 | } 26 | })() 27 | -------------------------------------------------------------------------------- /src/components/LoginButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /.github/workflows/welcome_message.yml: -------------------------------------------------------------------------------- 1 | name: 'Welcome New Contributors' 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | pull_request_target: 7 | types: [opened] 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | jobs: 12 | welcome-new-contributor: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 2 15 | steps: 16 | - name: 'Greet the contributor' 17 | uses: garg3133/welcome-new-contributors@a38583ed8282e23d63d7bf919ca2d9fb95300ca6 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | issue-message: 'Hello there, thanks for opening your first issue. We welcome you to the community!' 21 | pr-message: 'Hello there, thanks for opening your first Pull Request. Someone will review it soon.' 22 | -------------------------------------------------------------------------------- /server/routes/api/event_logger.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const logger = require('../../utilities/winston_logger') 4 | const ErrorLog = require('../../db/models/error_log') 5 | 6 | router.post('/log', async (req, res) => { 7 | const { severity, data } = req.body 8 | // send log messages to Winston 9 | logger.info(JSON.stringify({ severity, data })) 10 | res.sendStatus(200) 11 | 12 | // save log messages to database 13 | try { 14 | await ErrorLog.query().insert({ 15 | level: severity, 16 | data: JSON.stringify(data) 17 | }) 18 | } catch (error) { 19 | res 20 | .status(500) 21 | .send({ error: 'an error happened with the ErrorLog query insert' }) 22 | } 23 | }) 24 | 25 | module.exports = router 26 | -------------------------------------------------------------------------------- /server/routes/api/campaigns.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const Campaign = require('../../db/models/campaign') 4 | 5 | router.get('/:id', async (req, res) => { 6 | const id = req.params.id 7 | try { 8 | const campaign = await Campaign.query().findById(id) 9 | // console.log(campaign) 10 | res.send(campaign) 11 | } catch (error) { 12 | console.log(error) 13 | res.status(500).send({ error: 'Whoops' }) 14 | } 15 | }) 16 | 17 | router.get('/', async (req, res) => { 18 | try { 19 | const campaigns = await Campaign.query() 20 | // console.log(campaigns) 21 | res.send(campaigns) 22 | } catch (error) { 23 | console.log(error) 24 | res.status(500).send({ error: 'Whoops' }) 25 | } 26 | }) 27 | 28 | module.exports = router 29 | -------------------------------------------------------------------------------- /server/db/migrations/20210729053119_create-transactions-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'transactions' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | await knex.schema.createTable(tableName, (table) => { 6 | table.increments() 7 | table.string('stripe_transaction_id').notNullable() 8 | table.float('amount').notNullable() 9 | table.string('currency').notNullable() 10 | table.string('email').notNullable() 11 | table.string('status') 12 | table.string('payment_method').notNullable() 13 | table.string('payment_method_type').notNullable() 14 | table.integer('stripe_payment_created') 15 | table.string('stripe_client_secret') 16 | table.timestamps() 17 | }) 18 | }, 19 | 20 | async down(knex) { 21 | await knex.schema.dropTable(tableName) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | const history = require('connect-history-api-fallback') 4 | const apiRouter = require('./api') 5 | 6 | const app = express() 7 | 8 | // Enable if you're behind a reverse proxy (Heroku, Bluemix, AWS ELB or API Gateway, Nginx, etc) 9 | // see https://expressjs.com/en/guide/behind-proxies.html 10 | app.set('trust proxy', true) 11 | 12 | // Load the API router 13 | app.use('/api', apiRouter) 14 | 15 | // Fix for hash URLs in Vue 16 | // IMPORTANT: MUST be added before the `express.static` middleware for the `dist/` directory 17 | app.use(history()) 18 | 19 | // Routes generated by Vue CLI 20 | const staticPath = path.resolve(__dirname, '../dist') 21 | app.use(express.static(staticPath, { maxAge: '1d', etag: false })) 22 | 23 | module.exports = app 24 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 32 | -------------------------------------------------------------------------------- /server/routes/api/v1/campaigns/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const Campaign = require('../../../../db/models/campaign') 3 | 4 | const router = express.Router() 5 | 6 | // PUT request to update a campaign 7 | router.put('/:id', async (req, res) => { 8 | const campaignId = req.params.id 9 | 10 | if (!campaignId) return res.status(404).end() 11 | 12 | try { 13 | const updatedCampaign = await Campaign.query().patchAndFetchById( 14 | campaignId, 15 | req.body.campaign 16 | ) 17 | if (!updatedCampaign) 18 | return res.status(404).json({ msg: 'Campaign not found' }).end() 19 | 20 | return res.status(200).json({ campaign: updatedCampaign }).end() 21 | } catch (error) { 22 | console.error(error.message) 23 | 24 | return res.status(500).json({ msg: error.message }).end() 25 | } 26 | }) 27 | 28 | module.exports = router 29 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 45 | -------------------------------------------------------------------------------- /server/db/migrations/20231204193051_create-admins-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'admins' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | await knex.schema.createTable(tableName, (table) => { 6 | // Auto-incrementing non-nullable unsigned integer primary key "id" field 7 | table.increments() 8 | 9 | // Simple fields 10 | table.string('first_name').notNullable() 11 | table.string('last_name').notNullable() 12 | table.string('email').notNullable() 13 | table.text('password').notNullable() 14 | table.boolean('active').defaultTo(true) 15 | // Creates an 'created_at' field and 'updated_at' field 16 | table.timestamps(false, true) 17 | 18 | // Unique index 19 | table.unique(['email']) 20 | }) 21 | }, 22 | 23 | async down(knex) { 24 | // Drop the table 25 | await knex.schema.dropTable(tableName) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/mention-to-slack.yml: -------------------------------------------------------------------------------- 1 | # For GitHub user: 2 | # github_username: "slack_member_id" 3 | # For GitHub team: 4 | # github_teamname: "slack_member_id" 5 | 6 | # Team 7 | manishapriya94: 'U014PTUCF45' 8 | JamesMGreene: 'U020NHEBUPM' 9 | DietBepis1: 'U03K6HNBNNP' 10 | thyeggman: 'U03JUQDB8CB' 11 | paramsiddharth: 'U03J5KZVDM3' 12 | eprice555: 'U03UZQDA5DL' 13 | wallace: 'U02M9M81WUS' 14 | gdomingu: 'U03JVS4G334' 15 | ipc103: 'U046UL9BS9M' 16 | therzka: 'U03V8PPKR6H' 17 | joeyouss: 'U043YKQND98' 18 | 19 | # Fellows 20 | tdo95: 'U0457B11TLY' 21 | ahn-nath: 'U043EMMAK9S' 22 | rcmtcristian: 'U044N2XMGSC' 23 | LoopFruits: 'U03A63J8U4V' 24 | ericvennemeyer: 'U04354QA2K0' 25 | sammychinedu2ky: 'U042T8UV7GF' 26 | JoseEspinosaMachado: 'U043XLVG5R9' 27 | Alex-is-Gonzalez: 'U04NASJJ5R6' 28 | rsensenig: 'U04LD85GHDH' 29 | SAUMILDHANKAR: 'U04MG19LM7G' 30 | beverand: 'U04M9E6CXHS' 31 | yoyoyojoe: 'U04LSCZPGF9' 32 | christina-ml: 'U04M0BU9GCE' 33 | -------------------------------------------------------------------------------- /.github/workflows/docker-build-artifact.yml: -------------------------------------------------------------------------------- 1 | name: Format with Prettier 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, synchronize] 7 | 8 | jobs: 9 | prettier: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout (full) 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Use Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '18' 21 | 22 | - name: Install dependencies 23 | run: | 24 | npm ci || npm install 25 | npm install --no-save prettier 26 | 27 | - name: Run Prettier (format) 28 | run: npx prettier --write "**/*.{js,jsx,ts,tsx,json,md}" 29 | 30 | - name: Commit formatted changes 31 | uses: EndBug/add-and-commit@v9 32 | with: 33 | message: "chore: format code with Prettier" 34 | add: "." -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint code 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | 7 | permissions: 8 | contents: read 9 | 10 | # This allows a subsequently queued workflow run to interrupt previous runs 11 | concurrency: 12 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | lint: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 2 19 | steps: 20 | - name: Check out repo 21 | uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b 22 | 23 | - name: Setup node 24 | uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b 25 | with: 26 | node-version-file: '.node-version' 27 | cache: npm 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Run linter 33 | run: npm run lint:check 34 | -------------------------------------------------------------------------------- /server/db/models/admin.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base') 2 | 3 | class Admin extends BaseModel { 4 | static get tableName() { 5 | return 'admins' 6 | } 7 | 8 | static timestamps() { 9 | return true 10 | } 11 | 12 | static get jsonSchema() { 13 | return { 14 | type: 'object', 15 | required: ['first_name', 'last_name', 'email', 'password'], 16 | 17 | properties: { 18 | id: { type: 'integer' }, 19 | first_name: { type: 'string', minLength: 1, maxLength: 255 }, 20 | last_name: { type: 'string', minLength: 1, maxLength: 255 }, 21 | email: { type: 'string', minLength: 6, maxLength: 127 }, 22 | password: { type: 'string' }, 23 | active: { type: 'boolean' }, 24 | created_at: { type: 'string', minLength: 1, maxLength: 50 }, 25 | updated_at: { type: 'string', minLength: 1, maxLength: 50 } 26 | } 27 | } 28 | } 29 | } 30 | 31 | module.exports = Admin 32 | -------------------------------------------------------------------------------- /.github/workflows/check-formatting.yml: -------------------------------------------------------------------------------- 1 | name: Check formatting 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | 7 | permissions: 8 | contents: read 9 | 10 | # This allows a subsequently queued workflow run to interrupt previous runs 11 | concurrency: 12 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | check: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 2 19 | steps: 20 | - name: Check out repo 21 | uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b 22 | 23 | - name: Setup node 24 | uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b 25 | with: 26 | node-version-file: '.node-version' 27 | cache: npm 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Check code style 33 | run: npm run format:check 34 | -------------------------------------------------------------------------------- /src/views/Campaign.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | 44 | -------------------------------------------------------------------------------- /.github/workflows/programequity_slack.yml: -------------------------------------------------------------------------------- 1 | name: Notify mentioned users via Slack 2 | 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | issue_comment: 7 | types: [created, edited] 8 | pull_request_target: 9 | types: [opened, edited, review_requested] 10 | pull_request_review: 11 | types: [submitted] 12 | pull_request_review_comment: 13 | types: [created, edited] 14 | 15 | permissions: 16 | issues: read 17 | pull-requests: read 18 | 19 | jobs: 20 | mention-to-slack: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Run 24 | uses: abeyuya/actions-mention-to-slack@v2 25 | with: 26 | repo-token: ${{ secrets.GITHUB_TOKEN }} 27 | # Send messages to the 'devs' channel in the ProgramEquity Slack workspace 28 | slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} 29 | icon-url: https://img.icons8.com/color/256/000000/github-2.png 30 | bot-name: 'Mentioned on GitHub: ${{ github.event_name }}' 31 | run-id: ${{ github.run_id }} 32 | -------------------------------------------------------------------------------- /server/db/models/error_log.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base') 2 | 3 | class ErrorLog extends BaseModel { 4 | static get tableName() { 5 | return 'error_logs' 6 | } 7 | 8 | static timestamps = true 9 | 10 | // Optional JSON schema. This is not the database schema! Nothing is generated 11 | // based on this. This is only used for validation. Whenever a model instance 12 | // is created, it is checked against this schema. http://json-schema.org/ 13 | static get jsonSchema() { 14 | return { 15 | type: 'object', 16 | required: ['level', 'data'], 17 | 18 | properties: { 19 | id: { type: 'integer' }, 20 | level: { 21 | type: 'string', 22 | enum: ['log', 'error', 'warn', 'info', 'debug'] 23 | }, 24 | data: { type: 'string', minLength: 1, maxLength: 255 }, 25 | created_at: { type: 'string', minLength: 1, maxLength: 50 }, 26 | updated_at: { type: 'string', minLength: 1, maxLength: 50 } 27 | } 28 | } 29 | } 30 | } 31 | 32 | module.exports = ErrorLog 33 | -------------------------------------------------------------------------------- /server/auth/config/env.dev.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | 3 | dotenv.config() 4 | 5 | const audience = process.env.AUTH0_AUDIENCE 6 | const domain = process.env.AUTH0_DOMAIN 7 | const serverPort = process.env.SERVER_PORT 8 | const clientOriginUrl = process.env.CLIENT_ORIGIN_URL 9 | 10 | if (!audience) { 11 | throw new Error( 12 | '.env is missing the definition of an AUTH0_AUDIENCE environmental variable' 13 | ) 14 | } 15 | 16 | if (!domain) { 17 | throw new Error( 18 | '.env is missing the definition of an AUTH0_DOMAIN environmental variable' 19 | ) 20 | } 21 | 22 | if (!serverPort) { 23 | throw new Error( 24 | '.env is missing the definition of a API_PORT environmental variable' 25 | ) 26 | } 27 | 28 | if (!clientOriginUrl) { 29 | throw new Error( 30 | '.env is missing the definition of a APP_ORIGIN environmental variable' 31 | ) 32 | } 33 | 34 | const clientOrigins = ['http://localhost:4040'] 35 | 36 | module.exports = { 37 | audience, 38 | domain, 39 | serverPort, 40 | clientOriginUrl, 41 | clientOrigins 42 | } 43 | -------------------------------------------------------------------------------- /server/utilities/winston_logger.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { format, createLogger, transports } = require('winston') 3 | const { timestamp, combine, errors, json, colorize, printf } = format 4 | 5 | const logger = createLogger({ 6 | format: combine(timestamp(), errors({ stack: true }), json()), 7 | transports: [new transports.File({ filename: 'error.log' })] 8 | }) 9 | 10 | // create log format for when we're in development 11 | const logFormat = printf(({ level, message, timestamp, stack }) => { 12 | return `${timestamp} ${level}: ${stack || message}` 13 | }) 14 | 15 | // if we're not in production, then log to the console 16 | if (process.env.NODE_ENV !== 'production') { 17 | logger.add( 18 | new transports.Console({ 19 | format: combine( 20 | colorize(), 21 | timestamp({ 22 | format: 'YYYY-MM-DD HH:mm:ss' 23 | }), 24 | errors({ stack: true }), 25 | logFormat 26 | ), 27 | defaultMeta: { service: 'user-service' } 28 | }) 29 | ) 30 | } 31 | 32 | module.exports = logger 33 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from 'register-service-worker' 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready() { 8 | console.log( 9 | 'App is being served from cache by a service worker.\n' + 10 | 'For more details, visit https://goo.gl/AFskqB' 11 | ) 12 | }, 13 | registered() { 14 | console.log('Service worker has been registered.') 15 | }, 16 | cached() { 17 | console.log('Content has been cached for offline use.') 18 | }, 19 | updatefound() { 20 | console.log('New content is downloading.') 21 | }, 22 | updated() { 23 | console.log('New content is available; please refresh.') 24 | }, 25 | offline() { 26 | console.log( 27 | 'No internet connection found. App is running in offline mode.' 28 | ) 29 | }, 30 | error(error) { 31 | console.error('Error during service worker registration:', error) 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /server/db/migrations/20230301220841_create-error_logs-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'error_logs' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Create the table 6 | await knex.schema.createTable(tableName, (table) => { 7 | // Auto-incrementing non-nullable unsigned integer primary key "id" field 8 | table.increments() 9 | 10 | // Fields using native enum types 11 | table 12 | .enum('level', ['log', 'error', 'warn', 'info', 'debug'], { 13 | useNative: true, 14 | enumName: 'level_type' 15 | }) 16 | .notNullable() 17 | 18 | // Simple fields 19 | table.string('data').notNullable() 20 | table.timestamps(true, true) 21 | 22 | // Indexes 23 | table.index(['level']) 24 | 25 | // Unique indexes 26 | table.unique(['level']) 27 | }) 28 | }, 29 | 30 | async down(knex) { 31 | // Drop the table 32 | await knex.schema.dropTable(tableName) 33 | 34 | // Manually remove the native enum types 35 | await knex.raw(`DROP TYPE IF EXISTS level_type;`) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | command -v brew >/dev/null 2>&1 || { 6 | echo "==> Installing Homebrew..." 7 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 8 | } 9 | 10 | if [[ -z "${CODESPACES}" ]]; then 11 | echo "==> Adding Homebrew to the PATH..." 12 | echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/node/.profile 13 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 14 | fi 15 | 16 | echo "==> Updating Homebrew..." 17 | brew update 18 | 19 | brew bundle check >/dev/null 2>&1 || { 20 | echo "==> Installing Homebrew dependencies..." 21 | brew bundle 22 | } 23 | 24 | echo "==> Installing Node" 25 | nodenv install -s 26 | 27 | echo "==> Starting PostgreSQL service..." 28 | brew services restart postgres 29 | 30 | echo "==> Installing Node dependencies..." 31 | npm install 32 | 33 | echo "==> Creating and setting up databases..." 34 | npm run db:create 35 | 36 | echo "" 37 | echo '‼️ Create a ".env" file based on the ".env.example" file, then enter all of your environment variables.' 38 | -------------------------------------------------------------------------------- /.github/workflows/get-originaltime.yml: -------------------------------------------------------------------------------- 1 | name: Detect Original Time Label 2 | 3 | on: 4 | issues: 5 | types: [labeled] 6 | 7 | jobs: 8 | run_on_label_assignment: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout Repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 16 19 | 20 | # TODO: fix the unsupported engine error 21 | - name: Install 22 | run: npm install # because of amplify-script@1.0.0 only able to install node: '16.x' 23 | working-directory: ./roiScript 24 | 25 | - name: Send time 26 | if: contains(join(github.event.issue.labels.*.name, ','), 'originaltime-') 27 | env: 28 | LABELS: ${{ toJson(github.event.issue.labels) }} 29 | NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} 30 | NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }} 31 | ISSUE_NUMBER: ${{ github.event.issue.number }} 32 | GH_HANDLE: ${{ github.actor }} 33 | run: node ./roiScript/send-time.mjs 34 | -------------------------------------------------------------------------------- /server/db/migrations/20220420234746_letter_versions-office_division-constraints.js: -------------------------------------------------------------------------------- 1 | const tableName = 'letter_versions' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Alter the table 6 | await knex.schema.alterTable(tableName, (table) => { 7 | // Fields using native enum types 8 | table 9 | .enum( 10 | 'office_division', 11 | ['Federal', 'State', 'County', 'Municipality'], 12 | { useNative: true, enumName: 'political_division' } 13 | ) 14 | .notNullable() 15 | .defaultTo('Federal') 16 | .alter() 17 | 18 | // More simple fields 19 | // Limit "state" to just 2 characters 20 | table.string('state', 2).alter() 21 | }) 22 | }, 23 | 24 | async down(knex) { 25 | // Alter the table 26 | await knex.schema.alterTable(tableName, (table) => { 27 | // Simple fields 28 | table.string('state').alter() 29 | table.string('office_division').alter() 30 | }) 31 | 32 | // Manually remove the native enum types 33 | await knex.raw(`DROP TYPE IF EXISTS political_division;`) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ProgramEquity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Amplify is currently only used as a centrally hosted site, so the only supported version is the latest code in our `main` branch. 6 | 7 | ## Security Process 8 | 9 | If you have found a security vulnerability in Amplify, please send an email to our team: team@programequity.com 10 | 11 | Include all of the following details in your description of the vulnerability: 12 | - Platform used. _For example: Ubuntu 20.04.2 LTS (x86\_64)_ 13 | - Exact version of Amplify that you tested. _For example: commit [94d053f0234da0f418a3a683b590169c92a9683d](https://github.com/ProgramEquity/amplify/commit/94d053f0234da0f418a3a683b590169c92a9683d)_ 14 | - The source location of the bug and/or any other information that you are able to provide about what the cause of the bug is. 15 | - Browser used, if relevant. _For example: Google Chrome 97.0.4692.99 (Official Build) (arm64)_ 16 | - Site/API URL, if relevant 17 | - Explanation of how to exploit the vulnerability 18 | 19 | To qualify as a security issue, the bug **must** be reproducible on the latest release of Amplify via a realistic attack vector. 20 | -------------------------------------------------------------------------------- /src/components/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 46 | 47 | -------------------------------------------------------------------------------- /server/db/models/transaction.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base') 2 | 3 | class Transaction extends BaseModel { 4 | static get tableName() { 5 | return 'transactions' 6 | } 7 | 8 | static get jsonSchema() { 9 | return { 10 | type: 'object', 11 | required: [ 12 | 'stripeTransactionId', 13 | 'constituentId', 14 | 'amount', 15 | 'currency', 16 | 'paymentMethod' 17 | ], 18 | properties: { 19 | stripe_transaction_id: { type: 'string' }, 20 | constituent_id: { type: 'number' }, 21 | amount: { type: 'number' }, 22 | currency: { type: 'string' }, 23 | payment_method: { type: 'string' }, 24 | status: { type: 'string' } 25 | } 26 | } 27 | } 28 | 29 | /* 30 | static get relationMappings() { 31 | const Constituent = require('./constituent') 32 | 33 | return { 34 | constituent: { 35 | relation: BaseModel.HasOneRelation, 36 | modelClass: Constituent, 37 | join: { 38 | // from 'transactions.constituent_id', 39 | to: 'constituents.id' 40 | } 41 | } 42 | } 43 | } 44 | */ 45 | } 46 | 47 | module.exports = Transaction 48 | -------------------------------------------------------------------------------- /server/lib/sendgrid.js: -------------------------------------------------------------------------------- 1 | // Wrapper for Sendgrid API 2 | 3 | require('dotenv').config() 4 | const sgMail = require('@sendgrid/mail') // For sending email 5 | const sgClient = require('@sendgrid/client') // For other api endpoints 6 | 7 | class SendgridError extends Error { 8 | constructor(message) { 9 | super(message) 10 | this.name = 'SendgridError' 11 | } 12 | } 13 | 14 | class Sendgrid { 15 | constructor() { 16 | this.apiKey = process.env.SENDGRID_API_KEY 17 | this.env = process.env.NODE_ENV 18 | 19 | this.sgMail = sgMail 20 | this.sgClient = sgClient 21 | this.sgMail.setApiKey(this.apiKey) 22 | this.sgClient.setApiKey(this.apiKey) 23 | } 24 | 25 | async template(id) { 26 | const params = { method: 'GET', url: `/v3/templates/${id}` } 27 | 28 | try { 29 | const response = await this.sgClient.request(params) 30 | 31 | console.log(response) 32 | return response 33 | } catch (err) { 34 | throw new SendgridError(err.message) 35 | } 36 | } 37 | 38 | async send(payload) { 39 | if (this.env != 'production') { 40 | return { response: 200, payload } 41 | } 42 | } 43 | } 44 | 45 | module.exports = { Sendgrid, SendgridError } 46 | -------------------------------------------------------------------------------- /.docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.24' 2 | 3 | services: 4 | amplify: 5 | container_name: 'amplify_app' 6 | build: 7 | context: ../ 8 | dockerfile: .docker/Dockerfile 9 | environment: 10 | - POSTGRES_HOST=amplify_db 11 | ports: 12 | - 8080:8080 13 | - 5000:5000 14 | volumes: 15 | - type: bind 16 | source: '../' 17 | target: '/src/amplify' 18 | command: sleep infinity 19 | networks: 20 | - amplify_dev 21 | depends_on: 22 | - amplify_db 23 | 24 | amplify_db: 25 | container_name: 'amplify_db' 26 | image: postgres:latest 27 | volumes: 28 | - amplify_pg:/var/lib/postgresql/data 29 | environment: 30 | - POSTGRES_USER=postgres 31 | - PGUSER=postgres # Needed for healthcheck command 32 | - POSTGRES_PASSWORD=postgres 33 | - POSTGRES_PORT=5432 34 | expose: 35 | - 5432 36 | ports: 37 | - 5432:5432 38 | networks: 39 | - amplify_dev 40 | healthcheck: 41 | test: ['CMD', 'pg_isready'] 42 | interval: 1m30s 43 | timeout: 30s 44 | retries: 5 45 | start_period: 30s 46 | 47 | volumes: 48 | amplify_pg: 49 | 50 | networks: 51 | amplify_dev: 52 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build application 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | 7 | permissions: 8 | contents: read 9 | 10 | # This allows a subsequently queued workflow run to interrupt previous runs 11 | concurrency: 12 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 5 19 | steps: 20 | - name: Check out repo 21 | uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b 22 | 23 | - name: Setup node 24 | uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b 25 | with: 26 | node-version-file: '.node-version' 27 | cache: npm 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Build application 33 | run: npm run build 34 | env: 35 | # Auth0 authentication parameters with nonsensical sample values 36 | SERVER_PORT: 8080 37 | CLIENT_ORIGIN_URL: http://localhost:8080 38 | AUTH0_AUDIENCE: your_Auth0_identifier_value 39 | AUTH0_DOMAIN: your_Auth0_domain 40 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | permissions: 6 | contents: read 7 | concurrency: 8 | group: >- 9 | ${{ github.workflow }} @ ${{ github.event.pull_request.head.label || 10 | github.head_ref || github.ref }} 11 | cancel-in-progress: true 12 | jobs: 13 | test: 14 | if: ${{ github.repository == 'ProgramEquity/amplify' }} 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 5 17 | steps: 18 | - name: Check out repo 19 | uses: actions/checkout@v3 20 | - name: Setup node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version-file: .node-version 24 | cache: npm 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Run tests 28 | run: npm test -- server/__tests__/unit/ 29 | env: 30 | CICERO_API_KEY: '${{ secrets.TEST_CICERO_API_KEY }}' 31 | LOB_API_KEY: '${{ secrets.TEST_LOB_API_KEY }}' 32 | STRIPE_SECRET_KEY: '${{ secrets.TEST_STRIPE_SECRET_KEY }}' 33 | SERVER_PORT: 8080 34 | CLIENT_ORIGIN_URL: 'http://localhost:8080' 35 | AUTH0_AUDIENCE: your_Auth0_identifier_value 36 | AUTH0_DOMAIN: your_Auth0_domain 37 | -------------------------------------------------------------------------------- /src/components/HomeHero.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /server/db/models/campaign.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base') 2 | 3 | class Campaign extends BaseModel { 4 | static get tableName() { 5 | return 'campaigns' 6 | } 7 | 8 | // Optional JSON schema. This is not the database schema! Nothing is generated 9 | // based on this. This is only used for validation. Whenever a model instance 10 | // is created, it is checked against this schema. http://json-schema.org/ 11 | static get jsonSchema() { 12 | return { 13 | type: 'object', 14 | required: ['organization', 'name', 'cause', 'type', 'page_url'], 15 | 16 | properties: { 17 | id: { type: 'integer' }, 18 | organization: { type: 'string', minLength: 1, maxLength: 255 }, 19 | name: { type: 'string', minLength: 1, maxLength: 255 }, 20 | cause: { 21 | type: 'string', 22 | enum: ['Civic Rights', 'Education', 'Climate Justice'] 23 | }, 24 | type: { type: 'string', enum: ['Starter', 'Accelerator', 'Grant'] }, 25 | page_url: { type: 'string', minLength: 1 }, 26 | campaign_tagline: { type: 'string' }, 27 | campaign_text: { type: 'string' }, 28 | supplemental_text: { type: 'string' }, 29 | letter_template_id: { type: 'integer' } 30 | } 31 | } 32 | } 33 | } 34 | 35 | module.exports = Campaign 36 | -------------------------------------------------------------------------------- /server/db/models/constituent.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base') 2 | 3 | class Constituent extends BaseModel { 4 | static get tableName() { 5 | return 'constituents' 6 | } 7 | 8 | static get jsonSchema() { 9 | return { 10 | type: 'object', 11 | required: [ 12 | 'email', 13 | 'firstName', 14 | 'lastName', 15 | 'addressLine_1', 16 | 'city', 17 | 'state', 18 | 'zip' 19 | ], 20 | properties: { 21 | email: { type: 'string', minLength: 1, maxLength: 255 }, 22 | first_name: { type: 'string', minLength: 1, maxLength: 255 }, 23 | last_name: { type: 'string', minLength: 1, maxLength: 255 }, 24 | address_line_1: { type: 'string', minLength: 1, maxLength: 255 }, 25 | address_line_2: { type: 'string', minLength: 1, maxLength: 255 }, 26 | city: { type: 'string', minLength: 1, maxLength: 255 }, 27 | state: { type: 'string', minLength: 1, maxLength: 255 }, 28 | zip: { type: 'string', minLength: 1, maxLength: 255 } 29 | } 30 | } 31 | } 32 | 33 | /* 34 | static get relationMappings() { 35 | // TODO: Add relation to transactions 36 | } 37 | */ 38 | 39 | fullName() { 40 | return `${this.first_name} ${this.last_name}` 41 | } 42 | } 43 | 44 | module.exports = Constituent 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | # The PR base branches below must be a subset of the push branches above 9 | branches: 10 | - main 11 | # Only execute on PRs if relevant files changed 12 | paths: 13 | - '**/*.js' 14 | - '.github/workflows/codeql-analysis.yml' 15 | schedule: 16 | - cron: '27 1 * * 0' 17 | 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | # This allows a subsequently queued workflow run to interrupt previous runs 24 | concurrency: 25 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 26 | cancel-in-progress: true 27 | 28 | jobs: 29 | analyze: 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b 35 | 36 | # Initializes the CodeQL tools for scanning. 37 | - name: Initialize CodeQL 38 | uses: github/codeql-action/init@f3feb00acb00f31a6f60280e6ace9ca31d91c76a 39 | with: 40 | languages: javascript 41 | 42 | - name: Perform CodeQL Analysis 43 | uses: github/codeql-action/analyze@f3feb00acb00f31a6f60280e6ace9ca31d91c76a 44 | -------------------------------------------------------------------------------- /script/drop-db.js: -------------------------------------------------------------------------------- 1 | const { createClient, getConfig, getEnv } = require('../server/db') 2 | 3 | // See https://www.postgresql.org/docs/13/errcodes-appendix.html 4 | const INVALID_CATALOG_NAME_ERROR = '3D000' 5 | 6 | destroyDatabase() 7 | 8 | async function destroyDatabase() { 9 | const targetEnv = getEnv() 10 | if (targetEnv === 'production') { 11 | throw new Error('This script should not be used in production!') 12 | } 13 | 14 | const config = getConfig(targetEnv) 15 | await dropDatabase(config) 16 | } 17 | 18 | async function dropDatabase(config) { 19 | const { database } = config.connection 20 | let db 21 | 22 | try { 23 | // Connect with system database selected 24 | db = createClient({ 25 | ...config, 26 | connection: { 27 | ...config.connection, 28 | database: 'postgres' 29 | } 30 | }) 31 | 32 | // Drop the database if it exists 33 | await db.raw(`DROP DATABASE ${database}`) 34 | console.log(`Dropped database "${database}"!`) 35 | } catch (error) { 36 | if (error.code === INVALID_CATALOG_NAME_ERROR) { 37 | console.warn(`Error dropping database "${database}": it does not exist!`) 38 | } else { 39 | console.error(`Error dropping database "${database}": ${error.message}`) 40 | throw error 41 | } 42 | } finally { 43 | // Disconnect 44 | await db.destroy() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: label new contributor 2 | on: 3 | issues: 4 | types: [opened] 5 | pull_request_target: 6 | types: [opened] 7 | 8 | jobs: 9 | labeler: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/github-script@v6 13 | with: 14 | script: | 15 | const eventType = context.eventName; 16 | // Get a list of all issues created by the PR opener 17 | // See: https://octokit.github.io/rest.js/#pagination 18 | const creator = context.payload.sender.login 19 | const opts = github.rest.issues.listForRepo.endpoint.merge({ 20 | ...context.issue, 21 | creator, 22 | state: 'all' 23 | }) 24 | const issues = await github.paginate(opts) 25 | 26 | for (const issue of issues) { 27 | if (issue.number === context.issue.number) { 28 | continue 29 | } 30 | 31 | if (issue.pull_request || issue.user.login === creator) { 32 | return // Creator is already a contributor. 33 | } } 34 | await github.rest.issues.addLabels({ 35 | issue_number: context.issue.number, 36 | owner: context.repo.owner, 37 | repo: context.repo.repo, 38 | labels:["new contributor"] 39 | }); 40 | -------------------------------------------------------------------------------- /server/db/migrations/20210514201314_create-sent-letters-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'sent_letters' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Create the table 6 | await knex.schema.createTable(tableName, (table) => { 7 | // Auto-incrementing non-nullable unsigned integer primary key "id" field 8 | table.increments() 9 | 10 | // Foreign key references 11 | table.integer('letter_version_id').unsigned().notNullable() 12 | table.foreign('letter_version_id').references('letter_versions.id') 13 | 14 | table.integer('volunteer_id').unsigned().notNullable() 15 | table.foreign('volunteer_id').references('volunteers.id') 16 | 17 | // The Lob API response ID, for tracking and management purposes 18 | table.string('request_id').notNullable() 19 | 20 | // Timestamp field 21 | table.timestamp('requested_at').notNullable().defaultTo(knex.fn.now()) 22 | 23 | // Simple fields 24 | table.string('rep_name').notNullable() 25 | table.text('rep_address').notNullable() 26 | 27 | // Indexes 28 | table.index(['letter_version_id']) 29 | table.index(['volunteer_id']) 30 | table.index(['rep_name']) 31 | 32 | // Unique indexes 33 | table.unique(['request_id']) 34 | }) 35 | }, 36 | 37 | async down(knex) { 38 | // Drop the table 39 | await knex.schema.dropTable(tableName) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/backend-onboarding.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: AppSec Onboarding 3 | about: An onboarding guide for new volunteers working on backend 4 | title: 'AppSec Onboarding' 5 | labels: ramping, needs buddy 6 | assignees: '@paramsiddharth' 7 | 8 | Hi and welcome to being one of the AdvoCats! Here is a self paced guide to get familiar with the projects stack as well as how to use available resources. 9 | 10 | Resources: 11 | - Wiki for API Endpoints 12 | - Project Specification 13 | - Invision Prototypes 14 | 15 | Week 1: Get familiar with this repo 16 | - [ ] Read Project Specification : understand app stucture 17 | - [ ] set up your repo: are you able to run npm? 18 | - [ ] pick an issue with label "widget" 19 | 20 | Week 2: Meet the team 21 | - [ ] Read Project Specifications: Vuetify 22 | - [ ] Drop into a pair hour 23 | - [ ] pick an issue with label "good first issue" 24 | 25 | 26 | Week 3: Become active 27 | - [ ] Read Project Specification: Data Structures 28 | - [ ] Write 2 questions that came up 29 | - [ ] Submit a pull request: caching input 30 | 31 | Week 4: Ramping up 32 | - [ ] Read Project Specifications: Features 33 | - [ ] Submit an issue for optimization or bug fix 34 | - [ ] Submit a pull request on any of these labels: letter append, payment verification 35 | 36 | Week 5: spread your wings 37 | - [ ] suggest subtask issues 38 | - [ ] solve a bug fix 39 | - [ ] try an issue with the label: 40 | -------------------------------------------------------------------------------- /server/db/migrations/20211230165819_harden-volunteers-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'volunteers' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Alter the table 6 | await knex.schema.alterTable(tableName, function (table) { 7 | // Add NOT NULL constraints to simple fields 8 | table.string('email').notNullable().alter() 9 | table.string('first_name').notNullable().alter() 10 | table.string('last_name').notNullable().alter() 11 | table.string('street_address').notNullable().alter() 12 | table.string('city').notNullable().alter() 13 | table.string('state').notNullable().alter() 14 | table.string('zip').notNullable().alter() 15 | 16 | // Add unique indexes 17 | table.unique(['email']) 18 | }) 19 | }, 20 | 21 | async down(knex) { 22 | // Alter the table 23 | await knex.schema.alterTable(tableName, function (table) { 24 | // Drop unique indexes 25 | table.dropUnique(['email']) 26 | 27 | // Drop NOT NULL constraints from simple fields 28 | table.string('email').nullable().alter() 29 | table.string('first_name').nullable().alter() 30 | table.string('last_name').nullable().alter() 31 | table.string('street_address').nullable().alter() 32 | table.string('city').nullable().alter() 33 | table.string('state').nullable().alter() 34 | table.string('zip').nullable().alter() 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/db/migrations/20240817143934_create_letter_templates_table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(knex) { 3 | await knex.schema.createTable('letter_templates', (table) => { 4 | table.increments() 5 | table.timestamps(false, true, false) 6 | table.string('name').notNullable() 7 | table.string('subject').notNullable() 8 | table.text('html').notNullable() 9 | table.jsonb('merge_variables').notNullable() 10 | }) 11 | 12 | await knex.schema.alterTable('letters', (table) => { 13 | table.integer('letter_template_id').unsigned() 14 | table.foreign('letter_template_id').references('letter_templates.id') 15 | table.enu('delivery_method', ['email', 'snail_mail'], { 16 | useNative: true, 17 | enumName: 'delivery_methods' 18 | }) 19 | table.string('email') 20 | }) 21 | 22 | await knex.schema.alterTable('campaigns', (table) => { 23 | table.integer('letter_template_id').unsigned() 24 | table.foreign('letter_template_id').references('letter_templates.id') 25 | }) 26 | }, 27 | 28 | async down(knex) { 29 | await knex.schema.dropTable('letter_templates') 30 | 31 | await knex.schema.alterTable('letters', (table) => { 32 | table.dropColumn('letter_template_id') 33 | }) 34 | 35 | await knex.schema.alterTable('campaigns', (table) => { 36 | table.dropColumn('letter_template_id') 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/frontend-onboarding.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Frontend Volunteer 3 | about: An onboarding guide for new volunteers working on frontend 4 | title: 'New Frontend Volunteer' 5 | labels: ramping, needs buddy 6 | assignees: '@dietbepsi' 7 | 8 | 9 | Hi and welcome to being one of the AdvoCats! Here is a self paced guide to get familiar with the projects stack as well as how to use available resources. 10 | 11 | Resources: 12 | - Wiki for API Endpoints 13 | - Project Specification 14 | - Invision Prototypes 15 | 16 | Week 1: Get familiar with this repo 17 | - [ ] Read Project Specification : understand app stucture 18 | - [ ] set up your repo: are you able to run npm? 19 | - [ ] pick an issue with label "widget" 20 | 21 | Week 2: Meet the team 22 | - [ ] Read Project Specifications: Vuetify 23 | - [ ] Drop into a pair hour 24 | - [ ] pick an issue with label "good first issue" 25 | 26 | 27 | Week 3: Become active 28 | - [ ] Read Project Specification: Data Structures 29 | - [ ] Write 2 questions that came up 30 | - [ ] Submit a pull request: caching input 31 | 32 | Week 4: Ramping up 33 | - [ ] Read Project Specifications: Features 34 | - [ ] Submit an issue for optimization or bug fix 35 | - [ ] Submit a pull request on any of these labels: letter append, payment verification 36 | 37 | Week 5: spread your wings 38 | - [ ] suggest subtask issues 39 | - [ ] solve a bug fix 40 | - [ ] try an issue with the label: 41 | -------------------------------------------------------------------------------- /.github/workflows/finish-hackpod.yml: -------------------------------------------------------------------------------- 1 | name: Run on Label Assignment (finish hackpod) 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - labeled 7 | 8 | jobs: 9 | run_on_label_assignment: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Install 17 | run: npm install 18 | working-directory: ./roiScript 19 | 20 | - name: Set Branch Name 21 | id: branch_name 22 | run: echo "::set-output name=issue_branch::${{ github.head_ref }}" 23 | 24 | - name: Get Current User 25 | id: current_user 26 | run: echo "::set-output name=gh_handle::${{ github.actor }}" 27 | 28 | - name: Run script on label assignment 29 | if: contains(github.event.pull_request.labels.*.name, 'completed hackpod issue') # no way to set a particular label workflow trigger -- https://github.com/orgs/community/discussions/26261 30 | env: 31 | PR_PAYLOAD: ${{ toJson(github.event.pull_request._links.comments.href) }} # was: github # github.event.pull_request._links.comments.href 32 | NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} 33 | NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }} 34 | BRANCH_NAME: ${{ steps.branch_name.outputs.issue_branch }} 35 | GH_HANDLE: ${{ steps.current_user.outputs.gh_handle }} 36 | run: node ./roiScript/send-metrics.mjs 37 | -------------------------------------------------------------------------------- /server/db/migrations/20211230165827_harden-letter_versions-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'letter_versions' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | await knex.schema.alterTable(tableName, function (table) { 6 | // Rename columns 7 | table.renameColumn('campaignid', 'campaign_id') 8 | }) 9 | 10 | await knex.schema.alterTable(tableName, function (table) { 11 | // Add NOT NULL constraints to simple fields 12 | table.integer('campaign_id').unsigned().notNullable().alter() 13 | table.string('template_id').notNullable().alter() 14 | 15 | // Add columns 16 | table.string('municipality') 17 | 18 | // Add indexes 19 | table.index(['campaign_id']) 20 | table.index(['template_id']) 21 | }) 22 | }, 23 | 24 | async down(knex) { 25 | // Alter the table 26 | await knex.schema.alterTable(tableName, function (table) { 27 | // Drop NOT NULL constraints from simple fields 28 | table.integer('campaign_id').unsigned().nullable().alter() 29 | table.string('template_id').nullable().alter() 30 | 31 | // Drop columns 32 | table.dropColumn('municipality') 33 | 34 | // Drop indexes 35 | table.dropIndex(['campaign_id']) 36 | table.dropIndex(['template_id']) 37 | }) 38 | 39 | await knex.schema.alterTable(tableName, function (table) { 40 | // Rename columns 41 | table.renameColumn('campaign_id', 'campaignid') 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CHECKOUT.md: -------------------------------------------------------------------------------- 1 | # Documentation - Stripe Functions 2 | 3 | ## Introduction 4 | 5 | Amplify uses Stripe to process donations for non-profit organizations. Amplify uses these API endpoints to store and complete transactions. 6 | 7 | ## Implementation 8 | 9 | A step by step guide for how to interact with Stripe API in Amplify. 10 | 11 | ### create-transaction API 12 | 13 | Retrieves session from Stripe to insert into database. 14 | 15 | 1. Send session ID to API endpoint. 16 | 2. Retrieve Stripe session with that session ID. 17 | 3. Format session data to be inserted into database. 18 | 4. Insert into database. 19 | 20 | ### create-checkout-session API 21 | 22 | Create Stripe session. 23 | 24 | 1. Send donation amount to API. 25 | 2. Format and validate donation amount. 26 | 3. Create Stripe session with the following parameters: 27 | 1. line_items: donation amount 28 | 2. allow_promotion_code: true 29 | 3. success_url: redirects to /complete?session_id={CHECKOUT_SESSION_ID} 30 | 4. cancel_url: redirects to origin 31 | 4. Send back session.url and session.id 32 | 33 | ### create-payment-intent API Endpoint 34 | 35 | 1. Send request to `/create-payment-intent` with a `donationAmount` as string or integer. If user doesn’t select any particular `donationAmount`, send `1` in the `donationAmount` 36 | 2. This API will redirect the client to a Stripe Checkout page 37 | 3. Once user completes payment, it will redirect back to `success_url` with a Stripe session_id included in the URL. -------------------------------------------------------------------------------- /script/stringify.js: -------------------------------------------------------------------------------- 1 | /* 2 | const reps = JSON.stringify([ 3 | { 4 | name: 'Stephen Hoffman', 5 | title: 'LANL SWEIS Document Manager, DOE/NNSA ', 6 | photoUrl: 'https://imgs.search.brave.com/G46oqr3VnummnoFmkqT_NqalvvWvKQxpoX-EJITKqGQ/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9uYXRp/b25hbG1hZ2xhYi5v/cmcvaW1hZ2VzL2xh/eW91dC9sb3MtYWxh/bW9zLWxvZ28uanBn/P2Q9MjAyMzAyMDc', 7 | address_line1: '3747 West Jemez Road ', 8 | address_line2: '', 9 | address_city: 'Los Alamos', 10 | address_state: 'NM', 11 | address_zip: '87544', 12 | address_country: 'US', 13 | email: 'lanlsweis@nnsa.doe.gov' 14 | } 15 | ]) 16 | */ 17 | 18 | const assets = JSON.stringify({ 19 | campaign_logo: 'https://i.imgur.com/0I80XGh.png', 20 | campaign_background: 21 | 'https://media.assettype.com/freepressjournal/2024-09-04/i722yxsy/lg_NR15OldQueensGate0889_edit.jpg?rect=0%2C0%2C3900%2C2194&w=1200&auto=format%2Ccompress&ogImage=true', 22 | infographic: 'https://i.imgur.com/I7E3J5Q.png', 23 | 'campaign-img-1': 24 | 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ-yngrxizvU5kAZ3CP9-iQnn1OD14eXkpeXUYo96KOL6sg19Icwoes_JYlm_a61pzAcRE&usqp=CAU', 25 | 'campaign-img-2': 26 | 'https://images-prod.gothamist.com/images/GettyImages-1481222828.width-1000.jpg', 27 | 'campaign-img-3': 28 | 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS2X0OYEKhzrxmyMymuEaqDUIq0jDakYX4i4jaqRI3AUs0svGfcNDbpdatY4WUD6LMPRqg&usqp=CAU' 29 | }) 30 | 31 | //console.log(reps) 32 | console.log(assets) 33 | -------------------------------------------------------------------------------- /server/db/migrations/20210513215151_create-campaigns-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'campaigns' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Create the table 6 | await knex.schema.createTable(tableName, (table) => { 7 | // Auto-incrementing non-nullable unsigned integer primary key "id" field 8 | table.increments() 9 | 10 | // Simple fields 11 | table.string('name').notNullable() 12 | table.string('organization').notNullable() 13 | table.text('page_url').notNullable() 14 | table.integer('letters_sent').notNullable() 15 | 16 | // Fields using native enum types 17 | table 18 | .enum('cause', ['Civic Rights', 'Education', 'Climate Justice'], { 19 | useNative: true, 20 | enumName: 'cause_type' 21 | }) 22 | .notNullable() 23 | 24 | table 25 | .enum('type', ['Starter', 'Accelerator', 'Grant'], { 26 | useNative: true, 27 | enumName: 'campaign_type' 28 | }) 29 | .notNullable() 30 | 31 | // Indexes 32 | table.index(['name']) 33 | table.index(['organization']) 34 | 35 | // Unique indexes 36 | table.unique(['name', 'organization']) 37 | }) 38 | }, 39 | 40 | async down(knex) { 41 | // Drop the table 42 | await knex.schema.dropTable(tableName) 43 | 44 | // Manually remove the native enum types 45 | await knex.raw(`DROP TYPE IF EXISTS cause_type;`) 46 | await knex.raw(`DROP TYPE IF EXISTS campaign_type;`) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/emojiPR.yml: -------------------------------------------------------------------------------- 1 | # this is an action that will look for keywords in the title of a pull request and add a corresponding emoji to the comment section of the Pull Request so users can quickly identify the intention of a commit by the emoji generated 2 | name: An Emoji for Your Hard Work! 3 | # this action will trigger on every new pull request opened 4 | on: 5 | pull_request: 6 | types: 7 | - opened 8 | 9 | jobs: 10 | # This is the job definition for 'gitemote', which will run as part of the GitHub Actions workflow. 11 | gitemote: 12 | # This job will run on the latest version of Ubuntu 13 | runs-on: ubuntu-latest 14 | # This step checks out a copy of your repository and see if any changes have been made 15 | steps: 16 | - name: Checkout Code 17 | uses: actions/checkout@v2 18 | # Step 2: Run PR emote generator. 19 | # This custom action is used to generate emotes for pull requests. 20 | # It's referencing to the main branch of an action that I created based off of the original PR Emote Generator action created by @rcmtcristian. 21 | - name: PR emote generator 22 | uses: lingeorge88/gitemotePR_GL@main 23 | with: 24 | # The GITHUB_TOKEN is a special type of secret that is automatically created by GitHub. 25 | # It is used to authenticate in the workflow and perform actions on the repository. 26 | # Here it is passed to the custom action, which uses it to interact with GitHub API. 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /server/db/migrations/20211224223204_match-production-letter_versions-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'letter_versions' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Alter the table 6 | await knex.schema.alterTable(tableName, function (table) { 7 | // Drop NOT NULL constraints from simple fields 8 | table.integer('campaign_id').unsigned().nullable().alter() 9 | table.string('template_id').nullable().alter() 10 | 11 | // Drop columns 12 | table.dropColumn('municipality') 13 | 14 | // Drop indexes 15 | table.dropIndex(['campaign_id']) 16 | 17 | // Drop unique indexes 18 | table.dropUnique(['template_id']) 19 | }) 20 | 21 | await knex.schema.alterTable(tableName, function (table) { 22 | // Rename columns 23 | table.renameColumn('campaign_id', 'campaignid') 24 | }) 25 | }, 26 | 27 | async down(knex) { 28 | await knex.schema.alterTable(tableName, function (table) { 29 | // Rename columns 30 | table.renameColumn('campaignid', 'campaign_id') 31 | }) 32 | 33 | await knex.schema.alterTable(tableName, function (table) { 34 | // Add NOT NULL constraints to simple fields 35 | table.integer('campaign_id').unsigned().notNullable().alter() 36 | table.string('template_id').notNullable().alter() 37 | 38 | // Add columns 39 | table.string('municipality') 40 | 41 | // Add indexes 42 | table.index(['campaign_id']) 43 | 44 | // Add unique indexes 45 | table.unique(['template_id']) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | ########################### 2 | # Environment Variables # 3 | ########################### 4 | 5 | # API key for the Cicero API 6 | # Check with the team leads for the latest Cicero API key to be used 7 | CICERO_API_KEY= 8 | 9 | # Test environment API key for the Lob API 10 | # This is only required for running the integration tests successfully! 11 | LOB_API_KEY=test_eee78731c29fe718a5322e0cd51e9a02f0e 12 | LOB_BASE_URL=https://api.lob.com/v1 13 | 14 | # Sendgrid 15 | SENDGRID_API_KEY= 16 | 17 | # Auth0 authentication parameters 18 | # For local development, you can just use these literal nonsense values for now 19 | SERVER_PORT=8080 20 | CLIENT_ORIGIN_URL=http://localhost:8080 21 | AUTH0_AUDIENCE=your_Auth0_identifier_value 22 | AUTH0_DOMAIN=your_Auth0_domain 23 | 24 | # Stripe 25 | STRIPE_SECRET_KEY=sk_test_51IravfFqipIA40A3FOW6EzlXlJiXjL9V0FXKfb9n7cxh25Ww9QMA9aWwCzTSQscBOQFcB7s1TI6UCtW1JG83Hz1z000Sg2vSIr 26 | 27 | # Twilio 28 | TWILIO_ACCOUNT_SID=your_twilio_account_id # this is user generated 29 | TWILIO_AUTH_TOKEN=TWILIO_SECRET_KEY 30 | 31 | # To run in single-campaign mode, set the following environment vars 32 | # **We always run in single-campaign mode now so this is mandatory!** 33 | # Use campaign seeder file for local dev campaigns. 34 | VUE_APP_CAMPAIGN_MODE=single 35 | VUE_APP_FEATURED_CAMPAIGN=3 36 | 37 | VUE_APP_EMPTY_TRANSACTIONS=off 38 | VUE_APP_SHOW_EXT_DONATION=false 39 | VUE_APP_EXT_DONATION_URL= 40 | VUE_APP_NO_COST_MAIL=false 41 | 42 | POSTGRES_USER=postgres 43 | POSTGRES_PASSWORD=postgres 44 | POSTGRES_PORT=5432 45 | POSTGRES_HOST=amplify_db -------------------------------------------------------------------------------- /server/routes/api/v1/letter-templates/index.js: -------------------------------------------------------------------------------- 1 | // Routes for getting letter templates 2 | const express = require('express') 3 | const LetterTemplate = require('../../../../db/models/letter-template') 4 | const Handlebars = require('../../../../lib/handlebars') 5 | 6 | const router = express.Router() 7 | 8 | /** 9 | * Retrieves a letterTemplate, given its as a query param id 10 | */ 11 | router.get('/:id', async (req, res) => { 12 | const id = req.params.id 13 | 14 | if (!id) return res.status(404).end() 15 | 16 | try { 17 | const letterTemplate = await LetterTemplate.query().findById(id) 18 | 19 | if (!letterTemplate) { 20 | return res.status(404).end() 21 | } 22 | 23 | return res.status(200).json({ letterTemplate }).end() 24 | } catch (err) { 25 | console.error(err) 26 | return res.status(500).end() 27 | } 28 | }) 29 | 30 | /** 31 | * @params mergeVariables - object 32 | * @params id - number, string 33 | * Renders a template 34 | */ 35 | router.post('/render', async (req, res) => { 36 | const { mergeVariables, templateId } = req.body 37 | 38 | try { 39 | const template = await LetterTemplate.query().findById(templateId) 40 | 41 | if (!template) { 42 | return res.status(404).json({ error: 'Template id does not exist' }) 43 | } 44 | 45 | const letter = Handlebars.render(mergeVariables, template.html) 46 | 47 | return res.status(200).json({ letter }) 48 | } catch (err) { 49 | console.error(err.message) 50 | return res.status(500).end() 51 | } 52 | }) 53 | 54 | module.exports = router 55 | -------------------------------------------------------------------------------- /server/db/models/letter.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base') 2 | 3 | class Letter extends BaseModel { 4 | static get tableName() { 5 | return 'letters' 6 | } 7 | 8 | static get jsonSchema() { 9 | return { 10 | type: 'object', 11 | required: [ 12 | 'constituentId', 13 | 'transactionId', 14 | 'letterTemplate', 15 | 'addressee', 16 | 'addressLine_1', 17 | 'city', 18 | 'state', 19 | 'zip', 20 | 'deliveryMethod' 21 | ], 22 | properties: { 23 | letter_template: { type: 'string', minLength: 1, maxLength: 255 }, // Don't use this field now. Saving it just for compatibility 24 | lob_template_id: { type: 'string', maxLength: 255 }, 25 | sendgrid_template_id: { type: 'string', maxLength: 255 }, 26 | letter_version: { type: 'string', minLength: 1, maxLength: 255 }, 27 | addressee: { type: 'string', minLength: 1, maxLength: 255 }, 28 | address_line_1: { type: 'string', minLength: 1, maxLength: 255 }, 29 | address_line_2: { type: 'string', minLength: 1, maxLength: 255 }, 30 | city: { type: 'string', minLength: 1, maxLength: 255 }, 31 | state: { type: 'string', minLength: 1, maxLength: 255 }, 32 | zip: { type: 'string', minLength: 1, maxLength: 255 }, 33 | email: { type: 'string', minLength: 1, maxLength: 255 }, 34 | deliveryMethod: { type: 'string', minLength: 1, maxLength: 255 }, 35 | trackingNumber: { type: 'string', minLength: 1, maxLength: 255 } 36 | } 37 | } 38 | } 39 | } 40 | 41 | module.exports = Letter 42 | -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | // We need to require this here (AGAIN) to ensure it works with the Vue CLI loader 2 | require('dotenv').config() 3 | 4 | const express = require('express') 5 | const rateLimit = require('express-rate-limit') 6 | 7 | const representatives = require('./routes/api/representatives') 8 | const campaigns = require('./routes/api/campaigns') 9 | const authentication = require('./routes/api/authentication') 10 | const letterVersions = require('./routes/api/letter_versions') 11 | const lob = require('./routes/api/lob') 12 | const checkout = require('./routes/api/checkout') 13 | const twilio = require('./routes/api/twilio') 14 | const eventLogger = require('./routes/api/event_logger') 15 | 16 | // import the whole collection of v1 routes 17 | const v1Router = require('./routes/api/v1/v1.js') 18 | 19 | // Created a nested router 20 | const apiRouter = express.Router() 21 | 22 | // Middleware 23 | apiRouter.use(express.json()) 24 | 25 | // Rate Limiting 26 | const apiLimiter = rateLimit({ 27 | windowMs: 1 * 60 * 1000, // 1 minutes 28 | max: 100000 // to unblock for now, let's use a high number 29 | }) 30 | apiRouter.use(apiLimiter) 31 | 32 | // Routes 33 | apiRouter.use('/representatives', representatives) 34 | apiRouter.use('/campaigns', campaigns) 35 | apiRouter.use('/authentication', authentication) 36 | apiRouter.use('/letter_versions', letterVersions) 37 | apiRouter.use('/lob', lob) 38 | apiRouter.use('/checkout', checkout) 39 | apiRouter.use('/twilio', twilio) 40 | apiRouter.use('/event_logger', eventLogger) 41 | 42 | // Create the /v1 portion of the url. 43 | apiRouter.use('/v1', v1Router) 44 | 45 | module.exports = apiRouter 46 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | 7 | permissions: 8 | contents: read 9 | 10 | # This allows a subsequently queued workflow run to interrupt previous runs 11 | concurrency: 12 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | if: ${{ github.repository == 'ProgramEquity/amplify' }} 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 5 20 | steps: 21 | - name: Check out repo 22 | uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b 23 | 24 | - name: Setup node 25 | uses: actions/setup-node@969bd2663942d722d85b6a8626225850c2f7be4b 26 | with: 27 | node-version-file: '.node-version' 28 | cache: npm 29 | 30 | - name: Install dependencies 31 | run: npm ci 32 | 33 | - name: Run tests 34 | run: npm test -- server/__tests__/integration/ 35 | env: 36 | # Cicero API key 37 | CICERO_API_KEY: ${{ secrets.TEST_CICERO_API_KEY }} 38 | # Test environment Lob API key 39 | LOB_API_KEY: ${{ secrets.TEST_LOB_API_KEY }} 40 | # Stripe test secret key 41 | STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }} 42 | # Auth0 authentication parameters with nonsensical sample values 43 | SERVER_PORT: 8080 44 | CLIENT_ORIGIN_URL: http://localhost:8080 45 | AUTH0_AUDIENCE: your_Auth0_identifier_value 46 | AUTH0_DOMAIN: your_Auth0_domain 47 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | args: 9 | # Update 'VARIANT' to pick a default version of Node.js: 16, 14, 12. 10 | # Append -bullseye to pin to local arm64/Apple Silicon. 11 | # Append -buster to pin to Debian. 12 | VARIANT: '16-buster' 13 | 14 | # Specify environment variables to set 15 | environment: 16 | - POSTGRESS_DB=postgres 17 | - POSTGRES_USER=postgres 18 | - POSTGRES_PASSWORD=postgres 19 | - POSTGRES_PORT=5432 20 | 21 | volumes: 22 | - ..:/workspace:cached 23 | 24 | # Overrides default command so things don't shut down after the process ends. 25 | command: sleep infinity 26 | 27 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 28 | network_mode: service:db 29 | 30 | # Uncomment the next line to use a non-root user for all processes. 31 | # user: node 32 | 33 | db: 34 | image: postgres:latest 35 | restart: unless-stopped 36 | volumes: 37 | - postgres-data:/var/lib/postgresql/data 38 | environment: 39 | POSTGRES_DB: postgres 40 | POSTGRES_USER: postgres 41 | POSTGRES_PASSWORD: postgres 42 | POSTGRES_PORT: 5432 43 | expose: 44 | - '5432' # Publishes port to other containers but NOT to host machine 45 | ports: 46 | - '5432:5432' # Expose PostgreSQL to the host machine on port 5433 47 | command: '-p 5432' # Run the container's PostgreSQL server on non-standard port 5433 as well 48 | 49 | volumes: 50 | postgres-data: null 51 | -------------------------------------------------------------------------------- /src/components/CampaignCard.vue: -------------------------------------------------------------------------------- 1 | // Reusable card for cause-campaigns. 2 | 32 | 33 | 58 | 59 | 64 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Amplify 15 | 19 | 23 | 27 | 31 | 32 | 33 | 40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /shared/presenters/payment-presenter.js: -------------------------------------------------------------------------------- 1 | // Business logic for donations and payments 2 | require('dotenv').config 3 | 4 | class PaymentPresenterError extends Error { 5 | constructor(message) { 6 | super(message) 7 | this.name = 'PaymentPresenterError' 8 | } 9 | } 10 | 11 | class PaymentPresenter { 12 | constructor() { 13 | this.minimumPayment = process.env.MINIMUM_PAYMENT_AMOUNT 14 | this.maximumPayment = process.env.MAXIMUM_PAYMENT_AMOUNT 15 | } 16 | 17 | /** 18 | * Validates that payments aren't something weird, like NaN or a very large number. 19 | * @param {number} payment 20 | */ 21 | validatePaymentAmount(payment) { 22 | if (typeof payment !== 'number') { 23 | throw new PaymentPresenterError('Payment is not a number') 24 | } 25 | 26 | if (payment < this.minimumPayment || payment > this.maximumPayment) { 27 | throw new PaymentPresenterError('Payment amount is out of range') 28 | } 29 | } 30 | 31 | /** 32 | * Formats a value to cents, given a number, float, or numerical string 33 | * @param {any} payment 34 | * @returns {number} payment in cents 35 | */ 36 | formatPaymentAmount(payment) { 37 | if (typeof payment == 'string') { 38 | const nonNumerics = new RegExp(/[a-z.,]/, 'g') // Strips alphanumerics, commas, and periods out. 39 | 40 | payment = payment.replace(nonNumerics, '') 41 | payment = Number(payment) 42 | 43 | if (isNaN(payment)) { 44 | throw new PaymentPresenterError('Amount is in unexpected format') 45 | } 46 | } 47 | 48 | if (typeof payment == 'object') { 49 | throw new PaymentPresenterError('Unparsable argument') 50 | } 51 | 52 | return payment 53 | } 54 | } 55 | 56 | module.exports = { PaymentPresenter, PaymentPresenterError } 57 | -------------------------------------------------------------------------------- /src/assets/scm/text/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "campaign_tagline": "Protect Our Sacred Shellmounds", 3 | "campaign_text": "Sogorea Te' calls on us all to heal and to do the work our ancestors and future generations are calling us to do. The West Berkeley Shellmound sacred site is more at risk than ever now that the property owner, a commercial real estate developer, holds an SB-35 permit and has taken additional steps towards development on the property.\n\nWhile the Shellmound hasn't been making headlines over the past year since the California Supreme Court denied our appeal, forcing Berkeley to issue a development permit, work has continued steadily to advocate for the sacred Ohlone heritage site.\n\nSmall Ohlone ceremonies and semi-public gatherings have been regularly held at the Shellmound, and a constant stream of messages of support, prayer ribbons and other offerings from community members have been affixed to the chain link fence surrounding the land. Conversations between local organizations, community leaders and elected officials are ongoing, regarding the fate of the Shellmound and what can be done.\n\nWhile there are various paths forward to achieving protection of the land, continued support from Berkeley City Councilmembers is critical at this moment. If you live in Berkeley, or elsewhere in the Bay Area, we ask that you please consider writing an email to, or otherwise reaching out to Berkeley Councilmembers (especially the representative of your district, if you live in Berkeley).", 4 | "supplemental_text": "Indigenous communities steward more than 80% of the world's biodiversity. Returning Shellmounds means endangered species like the Condor and Salmon can return, trees like the Madrone and Mugwort can return. Our strongest strategy towards lowering carbon emissions is Indigenous led conservation of biodiversity." 5 | } 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import App from './App.vue' 4 | import './registerServiceWorker' 5 | import router from './router' 6 | import store from './store' 7 | import vuetify from './plugins/vuetify' 8 | import '@babel/polyfill' 9 | // Auth0 is currently not working. No time to debug 10 | // import { domain, clientId, audience } from '../auth_config.json' 11 | // import { Auth0Plugin } from '@/auth/auth0-plugin' 12 | import 'vuetify/dist/vuetify.min.css' 13 | 14 | // for front-end logging 15 | import vueLogger from './utilities/vue_logger' 16 | 17 | // fontawesome icons 18 | import { library } from '@fortawesome/fontawesome-svg-core' 19 | import { faUserSecret } from '@fortawesome/free-solid-svg-icons' 20 | import { 21 | faInstagram, 22 | faFacebookF, 23 | faTwitter, 24 | faYoutube 25 | } from '@fortawesome/free-brands-svg-icons' 26 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 27 | 28 | /* import specific icons */ 29 | library.add(faUserSecret, faInstagram, faFacebookF, faTwitter, faYoutube) 30 | 31 | /* add font awesome icon component */ 32 | // eslint-disable-next-line vue/component-definition-name-casing 33 | Vue.component('font-awesome-icon', FontAwesomeIcon) 34 | 35 | Vue.use(Vuetify) 36 | 37 | /* 38 | Vue.use(Auth0Plugin, { 39 | domain, 40 | clientId, 41 | audience, 42 | onRedirectCallback: (appState) => { 43 | router.push( 44 | appState && appState.targetUrl 45 | ? appState.targetUrl 46 | : window.location.pathname 47 | ) 48 | } 49 | }) 50 | */ 51 | 52 | // for using front-end logger 53 | Vue.use(vueLogger) 54 | 55 | Vue.config.productionTip = false 56 | 57 | new Vue({ 58 | router, 59 | store, 60 | vuetify, 61 | components: { FontAwesomeIcon }, 62 | 63 | render: (h) => h(App) 64 | }).$mount('#app') 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/batch-templade.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Batch 3 | about: An item of work inside an Epic that represents 1-2 weeks of effort. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | ### Description 17 | 18 | 19 | 20 | ### Spec 21 | 22 | 31 | 32 | ### Target Date: yyyy-mm-dd 33 | 34 | **Please consider (or make a note) of anyone who is on-call or on support during this feature! This should be included in the timeline** 35 | 36 | ### Links/Attachments 37 | 38 | 39 | 40 | ### Done Done 41 | 42 | 43 | 44 | - [ ] Is unit tested 45 | - [ ] Is covered by integration tests 46 | - [ ] UX Automation Added 47 | - [ ] Is continuously tested in each environment 48 | - [ ] Monitoring and Diagnostics added 49 | - [ ] Is sufficient user telemetry and Dashboarding 50 | - [ ] Relevant architectural review 51 | - [ ] Is sufficient public documentation 52 | - [ ] CSS and team has been appropriately briefed on how to support 53 | - [ ] Engineering Lead signoff 54 | - [ ] PM signed off 55 | - [ ] Design signed off 56 | - [ ] Customer value demo screencast sent to team 57 | -------------------------------------------------------------------------------- /.github/workflows/issue-metrics.yml: -------------------------------------------------------------------------------- 1 | name: Measure issue timestamp 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | 7 | jobs: 8 | record_issue_timestamp: 9 | runs-on: ubuntu-latest 10 | if: github.event_name == 'issues' 11 | steps: 12 | - name: Checkout the repo 13 | uses: actions/checkout@v3 14 | 15 | - name: Create branch linked to the issue 16 | id: create_branch 17 | run: | 18 | issue_number=$(echo "${{ github.event.issue.number }}") 19 | git checkout -b "issue-${issue_number}" 20 | echo "::set-output name=issue_branch::issue-${issue_number}" 21 | 22 | - name: Install 23 | run: npm install 24 | working-directory: ./roiScript 25 | 26 | - name: Run 27 | env: 28 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 29 | ISSUE_NUMBER: ${{ github.event.issue.number }} 30 | run: node ./roiScript/log-branch.mjs 31 | 32 | - name: Log Current Branch (debugging) 33 | run: | 34 | current_branch=$(git rev-parse --abbrev-ref HEAD) 35 | echo "Currently checked out branch: $current_branch" 36 | 37 | - name: Add Issue Timestamp To File 38 | working-directory: roiScript 39 | run: | 40 | issue_num=$(echo "${{ github.event.issue.number }}") 41 | timestamp=$(date +"%Y-%m-%d %H:%M:%S") 42 | echo "$GITHUB_ACTOR, issue #${issue_num}: $timestamp" >> issue_timestamp.log 43 | 44 | - name: Commit Issue Timestamp 45 | run: | 46 | git config --global user.email "Polinka7max@gmail.com" 47 | git config --global user.name "Dunridge" 48 | git add . 49 | git commit -m "Add issue timestamp by $GITHUB_ACTOR" 50 | git push https://${{ secrets.GH_TOKEN }}@github.com/${{ github.repository }}.git 51 | -------------------------------------------------------------------------------- /server/db/models/letter-version.js: -------------------------------------------------------------------------------- 1 | const BaseModel = require('./_base') 2 | 3 | class LetterVersion extends BaseModel { 4 | static get tableName() { 5 | return 'letter_versions' 6 | } 7 | 8 | // Optional JSON schema. This is not the database schema! Nothing is generated 9 | // based on this. This is only used for validation. Whenever a model instance 10 | // is created, it is checked against this schema. http://json-schema.org/ 11 | static get jsonSchema() { 12 | return { 13 | type: 'object', 14 | required: ['campaign_id', 'template_id', 'office_division'], 15 | 16 | properties: { 17 | id: { type: 'integer' }, 18 | campaign_id: { type: 'integer', minimum: 1 }, 19 | template_id: { type: 'string', minLength: 1, maxLength: 255 }, 20 | office_division: { 21 | type: 'string', 22 | enum: ['Federal', 'State', 'County', 'Municipality'] 23 | }, 24 | state: { 25 | anyOf: [ 26 | { type: 'string', minLength: 2, maxLength: 2 }, 27 | { type: 'null' } 28 | ] 29 | }, 30 | county: { 31 | anyOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] 32 | }, 33 | municipality: { 34 | anyOf: [{ type: 'string', minLength: 1 }, { type: 'null' }] 35 | }, 36 | mergeVariables: {} 37 | } 38 | } 39 | } 40 | 41 | // This object defines the relations to other models. 42 | static get relationMappings() { 43 | const Campaign = require('./campaign') 44 | return { 45 | Campaign: { 46 | relation: BaseModel.BelongsToOneRelation, 47 | modelClass: Campaign, 48 | join: { 49 | from: 'letter_versions.campaign_id', 50 | to: 'campaigns.id' 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | module.exports = LetterVersion 58 | -------------------------------------------------------------------------------- /server/__tests__/unit/event_logger.test.js: -------------------------------------------------------------------------------- 1 | const ErrorLog = require('../../db/models/error_log') 2 | const request = require('supertest') 3 | const app = require('../../app') 4 | const logger = require('../../utilities/winston_logger') 5 | 6 | const logMessage = { severity: 'foo', data: 'bar' } 7 | const route = '/api/event_logger/log' 8 | 9 | beforeEach(() => { 10 | jest.clearAllMocks() 11 | }) 12 | 13 | describe('sending a log message to Winston', () => { 14 | test('calls logger.info with the correct parameters', async () => { 15 | const spy = jest.spyOn(logger, 'info') 16 | 17 | // call a function that should call logger.info() 18 | const response = await request(app).post(route).send(logMessage) 19 | expect(response.status).toBe(200) 20 | 21 | expect(spy).toHaveBeenCalledWith(JSON.stringify(logMessage)) 22 | 23 | // restore the original logger.info method 24 | spy.mockRestore() 25 | }) 26 | }) 27 | 28 | // mock the ErrorLog model 29 | jest.mock('../../db/models/error_log', () => { 30 | let elQuery = jest.fn() 31 | let elInsert = jest.fn() 32 | let mockErrorLog = { 33 | query: elQuery, 34 | insert: elInsert 35 | } 36 | // in order to chain class methods, we return the class that called the method 37 | elQuery.mockImplementation(() => mockErrorLog) 38 | elInsert.mockImplementation(() => mockErrorLog) 39 | 40 | return mockErrorLog 41 | }) 42 | 43 | describe('Saving a log message to the database', () => { 44 | test('should call ErrorLog.query().insert()', async () => { 45 | // call a function that should call ErrorLog.query().insert() 46 | await request(app).post(route).send(logMessage) 47 | 48 | expect(ErrorLog.query).toHaveBeenCalledTimes(1) 49 | expect(ErrorLog.insert).toHaveBeenCalledWith({ 50 | level: logMessage.severity, 51 | data: JSON.stringify(logMessage.data) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Template to create bug/buildsafe issues that need to be designed for 4 | title: QA Bug 5 | labels: '' 6 | assignees: teakopp 7 | 8 | --- 9 | 10 | Remember, an issue is not the place to ask questions. You can use [Stack Overflow](http://stackoverflow.com/questions/tagged/angular-meteor) for that, or you may want to start a discussion on the [Meteor forum](https://forums.meteor.com/). 11 | 12 | Before you open an issue, please check if a similar issue already exists or has been closed before. 13 | 14 | 15 | 16 | ## Expected Behavior 17 | 18 | 19 | ## Current Behavior 20 | 21 | 22 | ## Possible Solution 23 | 24 | 25 | ## Steps to Reproduce 26 | 27 | 28 | 1. 29 | 2. 30 | 3. 31 | 4. 32 | 33 | ## Context (Environment) 34 | 35 | 36 | 37 | 38 | 39 | ## Detailed Description 40 | 41 | 42 | ## Possible Implementation 43 | 44 | 45 | ## Labels 46 | Please add the following labels dependent on the bug type 47 | - Security Bug: Creates an issue that can cause harm 48 | - Vulnerability Bug: Creates an issue to update a supply chain dependency error 49 | - Functional Bug: something that is causing a crash of the app 50 | -------------------------------------------------------------------------------- /src/components/CauseCarousel.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 64 | 65 | 70 | -------------------------------------------------------------------------------- /server/db/migrations/20220413154827_rename-volunteers-table-to-constituents.js: -------------------------------------------------------------------------------- 1 | const referencingTableName = 'sent_letters' 2 | const oldTableName = 'volunteers' 3 | const newTableName = 'constituents' 4 | 5 | module.exports = { 6 | async up(knex) { 7 | // Step 1: Unbind the referencing table 8 | await knex.schema.alterTable(referencingTableName, (table) => { 9 | table.dropIndex(['volunteer_id']) 10 | table.dropForeign('volunteer_id') 11 | }) 12 | 13 | // Step 2: Rename the primary table 14 | await knex.schema.renameTable(oldTableName, newTableName) 15 | 16 | // Step 3: Rebind the referencing table 17 | await knex.schema.alterTable(referencingTableName, (table) => { 18 | table.renameColumn('volunteer_id', 'constituent_id') 19 | table.foreign('constituent_id').references(`${newTableName}.id`) 20 | table.index(['constituent_id']) 21 | }) 22 | 23 | // Step 4: Add 'phone_number' column to the 'constituents' table 24 | await knex.schema.alterTable(newTableName, function (table) { 25 | table.string('phone_number') 26 | }) 27 | }, 28 | 29 | async down(knex) { 30 | // Step 1: Unbind the referencing table 31 | await knex.schema.alterTable(referencingTableName, (table) => { 32 | table.dropIndex(['constituent_id']) 33 | table.dropForeign('constituent_id') 34 | }) 35 | 36 | // Step 2: Rename the primary table 37 | await knex.schema.renameTable(newTableName, oldTableName) 38 | 39 | // Step 3: Rebind the referencing table 40 | await knex.schema.alterTable(referencingTableName, (table) => { 41 | table.renameColumn('constituent_id', 'volunteer_id') 42 | table.foreign('volunteer_id').references(`${oldTableName}.id`) 43 | table.index(['volunteer_id']) 44 | }) 45 | 46 | // Step 4: Remove the 'phone_number' column from the 'constituents' table 47 | await knex.schema.alterTable(oldTableName, function (table) { 48 | table.dropColumn('phone_number') 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/__tests__/integration/server/routes/api/v1/campaigns/campaign_put_endpoint.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const app = require('../../../../../../../app') 3 | const Campaign = require('../../../../../../../db/models/campaign') 4 | 5 | jest.mock('../../../../../../../db/models/campaign') 6 | 7 | describe('PUT /api/v1/campaigns/:id', () => { 8 | it('should return 200 if successfully updated the campaign', async () => { 9 | const campaignData = { 10 | id: 5, 11 | name: "Sogorea 'Te Land Trust", 12 | organization: "Sogorea 'Te Land Trust", 13 | page_url: 'https://sogoreate-landtrust.org/', 14 | cause: 'Civic Rights', 15 | type: 'Grant' 16 | } 17 | // Mock patchAndFetchById to return a resolved promise 18 | Campaign.query.mockReturnValue({ 19 | patchAndFetchById: jest.fn().mockResolvedValue(campaignData) 20 | }) 21 | const response = await request(app).put('/api/v1/campaigns/5').send({ 22 | campaign: campaignData 23 | }) 24 | expect(response.status).toBe(200) 25 | expect(response.body.campaign).toEqual(campaignData) 26 | }) 27 | 28 | it('should return 400 if no campaign is found', async () => { 29 | // Mock patchAndFetchById to return undefined 30 | Campaign.query.mockReturnValue({ 31 | patchAndFetchById: jest.fn().mockResolvedValue(undefined) 32 | }) 33 | const response = await request(app) 34 | .put('/api/v1/campaigns/999999') 35 | .send({ campaign: { type: 'Accelerator' } }) 36 | 37 | expect(response.status).toBe(404) 38 | expect(response.body.msg).toBe('Campaign not found') 39 | }) 40 | 41 | it('should return 500 for invalid update', async () => { 42 | // Mock patchAndFetchById to throw an error 43 | Campaign.query.mockReturnValue({ 44 | patchAndFetchById: jest.fn().mockRejectedValue(new Error('Server error')) 45 | }) 46 | const response = await request(app) 47 | .put('/api/v1/campaigns/5') 48 | .send({ 49 | campaign: { 50 | type: 'random' 51 | } 52 | }) 53 | 54 | expect(response.status).toBe(500) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /server/__tests__/fixtures/stripe/payment-failure.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_1NG8Du2eZvKYlo2CUI79vXWy", 3 | "object": "event", 4 | "api_version": "2019-02-19", 5 | "created": 1686089970, 6 | "data": { 7 | "object": { 8 | "id": "pi_test-failed-5678", 9 | "object": "payment_intent", 10 | "amount": 5000, 11 | "amount_capturable": 0, 12 | "amount_details": { 13 | "tip": {} 14 | }, 15 | "amount_received": 0, 16 | "application": null, 17 | "application_fee_amount": null, 18 | "automatic_payment_methods": { 19 | "enabled": true 20 | }, 21 | "canceled_at": null, 22 | "cancellation_reason": null, 23 | "capture_method": "automatic", 24 | "client_secret": "pi_3MtwBwLkdIwHu7ix28a3tqPa_secret_YrKJUKribcBjcG8HVhfZluoGH", 25 | "confirmation_method": "automatic", 26 | "created": 1680800504, 27 | "currency": "usd", 28 | "customer": null, 29 | "description": null, 30 | "invoice": null, 31 | "last_payment_error": null, 32 | "latest_charge": null, 33 | "livemode": false, 34 | "metadata": {}, 35 | "next_action": null, 36 | "on_behalf_of": null, 37 | "payment_method": null, 38 | "payment_method_options": { 39 | "card": { 40 | "installments": null, 41 | "mandate_options": null, 42 | "network": null, 43 | "request_three_d_secure": "automatic" 44 | }, 45 | "link": { 46 | "persistent_token": null 47 | } 48 | }, 49 | "payment_method_types": ["card", "link"], 50 | "processing": null, 51 | "receipt_email": null, 52 | "review": null, 53 | "setup_future_usage": null, 54 | "shipping": null, 55 | "source": null, 56 | "statement_descriptor": null, 57 | "statement_descriptor_suffix": null, 58 | "status": "failed", 59 | "transfer_data": null, 60 | "transfer_group": null 61 | } 62 | }, 63 | "livemode": false, 64 | "pending_webhooks": 0, 65 | "request": { 66 | "id": null, 67 | "idempotency_key": null 68 | }, 69 | "type": "payment_intent.failed" 70 | } 71 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | import SearchReps from '../components/SearchReps' 5 | import RepresentativeCard from '../components/RepresentativeCard' 6 | import scrollBehavior from './scroll-behavior' 7 | // import Reps from '../components/Reps' 8 | 9 | Vue.use(VueRouter) 10 | 11 | const routes = [ 12 | { 13 | path: '/', 14 | name: 'Home', 15 | component: () => import('../views/Campaign.vue'), 16 | children: [ 17 | { 18 | path: 'postalcode/:postalCode?', 19 | name: 'Reps', 20 | component: SearchReps, 21 | props: true, 22 | children: [ 23 | { 24 | path: 'member/:member', 25 | component: RepresentativeCard, 26 | name: 'RepClick', 27 | props: true 28 | } 29 | ] 30 | } 31 | ] 32 | }, 33 | { 34 | path: '/about', 35 | name: 'About', 36 | component: () => import('../views/About.vue') 37 | }, 38 | /* Some campaigns have a lot of user friction when they interact with 39 | the original landing page, so we are going to try to link to directly 40 | to the campaigns page instead. 41 | I will leave the original entry here so it's easy to undo later. 42 | { 43 | path: '/campaign/:campaignId', 44 | name: 'Campaign', 45 | component: () => import('../views/Campaign.vue'), 46 | children: [ 47 | { 48 | path: 'postalcode/:postalCode?', 49 | name: 'Reps', 50 | component: SearchReps, 51 | props: true, 52 | children: [ 53 | { 54 | path: 'member/:member', 55 | component: RepresentativeCard, 56 | name: 'RepClick', 57 | props: true 58 | } 59 | ] 60 | } 61 | ] 62 | }, 63 | */ 64 | { 65 | path: '/complete', 66 | name: 'CompletePage', 67 | component: () => import('../views/CompletePage.vue') 68 | } 69 | ] 70 | 71 | const router = new VueRouter({ 72 | mode: 'history', 73 | scrollBehavior, 74 | base: process.env.BASE_URL, 75 | routes 76 | }) 77 | 78 | export default router 79 | -------------------------------------------------------------------------------- /server/__tests__/fixtures/stripe/payment-success.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evt_1NG8Du2eZvKYlo2CUI79vXWy", 3 | "object": "event", 4 | "api_version": "2019-02-19", 5 | "created": 1686089970, 6 | "data": { 7 | "object": { 8 | "id": "pi_test-success-1234", 9 | "object": "payment_intent", 10 | "amount": 10000, 11 | "amount_capturable": 0, 12 | "amount_details": { 13 | "tip": {} 14 | }, 15 | "amount_received": 0, 16 | "application": null, 17 | "application_fee_amount": null, 18 | "automatic_payment_methods": { 19 | "enabled": true 20 | }, 21 | "canceled_at": null, 22 | "cancellation_reason": null, 23 | "capture_method": "automatic", 24 | "client_secret": "pi_3MtwBwLkdIwHu7ix28a3tqPa_secret_YrKJUKribcBjcG8HVhfZluoGH", 25 | "confirmation_method": "automatic", 26 | "created": 1680800504, 27 | "currency": "usd", 28 | "customer": null, 29 | "description": null, 30 | "invoice": null, 31 | "last_payment_error": null, 32 | "latest_charge": null, 33 | "livemode": false, 34 | "metadata": {}, 35 | "next_action": null, 36 | "on_behalf_of": null, 37 | "payment_method": null, 38 | "payment_method_options": { 39 | "card": { 40 | "installments": null, 41 | "mandate_options": null, 42 | "network": null, 43 | "request_three_d_secure": "automatic" 44 | }, 45 | "link": { 46 | "persistent_token": null 47 | } 48 | }, 49 | "payment_method_types": ["card", "link"], 50 | "processing": null, 51 | "receipt_email": null, 52 | "review": null, 53 | "setup_future_usage": null, 54 | "shipping": null, 55 | "source": null, 56 | "statement_descriptor": null, 57 | "statement_descriptor_suffix": null, 58 | "status": "succeeded", 59 | "transfer_data": null, 60 | "transfer_group": null 61 | } 62 | }, 63 | "livemode": false, 64 | "pending_webhooks": 0, 65 | "request": { 66 | "id": null, 67 | "idempotency_key": null 68 | }, 69 | "type": "payment_intent.succeeded" 70 | } 71 | -------------------------------------------------------------------------------- /server/db/migrations/20210514145630_create-letter-versions-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'letter_versions' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Create the table 6 | await knex.schema.createTable(tableName, (table) => { 7 | // Auto-incrementing non-nullable unsigned integer primary key "id" field 8 | table.increments() 9 | 10 | // Simple fields 11 | table.string('template_id').notNullable() 12 | 13 | // Foreign key references 14 | table.integer('campaign_id').unsigned().notNullable() 15 | table.foreign('campaign_id').references('campaigns.id') 16 | 17 | // Fields using native enum types 18 | // table 19 | // .enum( 20 | // 'office_division', 21 | // ['Federal', 'State', 'County', 'Municipality'], 22 | // { useNative: true, enumName: 'political_division' } 23 | // ) 24 | // .notNullable() 25 | // .defaultTo('Federal') 26 | 27 | // More simple fields 28 | table.string('office_division') 29 | table.string('state') 30 | table.string('county') 31 | table.string('municipality') 32 | 33 | // Indexes 34 | table.index(['campaign_id']) 35 | 36 | // Unique indexes 37 | table.unique(['template_id']) 38 | }) 39 | 40 | // 41 | // Special CHECK constraints to verify row integrity 42 | // 43 | 44 | // If "office_division" is set to 'Federal', then: 45 | // - the value for "state" must be NULL 46 | // - the value for "county" must be NULL 47 | // - the value for "municipality" must be NULL 48 | // await knex.raw( 49 | // ` 50 | // ALTER TABLE "${tableName}" 51 | // ADD CONSTRAINT "office_divisions_are_valid" 52 | // CHECK ( 53 | // ( 54 | // office_division = 'Federal' AND 55 | // state IS NULL AND 56 | // county IS NULL AND 57 | // municipality IS NULL 58 | // ) 59 | // ); 60 | // ` 61 | // ) 62 | }, 63 | 64 | async down(knex) { 65 | // Drop the table (and its special CHECK constraints) 66 | await knex.schema.dropTable(tableName) 67 | 68 | // Manually remove the native enum types 69 | // await knex.raw(`DROP TYPE IF EXISTS political_division;`) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/javascript-node-postgres 3 | // Update the VARIANT arg in docker-compose.yml to pick a Node.js version 4 | { 5 | "name": "ProgramEquity/amplify", 6 | "dockerComposeFile": "docker-compose.yml", 7 | "service": "app", 8 | "workspaceFolder": "/workspace", 9 | 10 | // Set minimum specificiations for Codespaces machine types 11 | "hostRequirements": { 12 | "cpus": 4, 13 | "memory": "8gb", 14 | "storage": "32gb" 15 | }, 16 | 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": { 19 | "sqltools.connections": [ 20 | { 21 | "name": "Container database", 22 | "driver": "PostgreSQL", 23 | "previewLimit": 50, 24 | "server": "localhost", 25 | "port": 5433, // Default PostgreSQL port is 5432 26 | "database": "postgres", 27 | "username": "postgres", 28 | "password": "postgres" 29 | } 30 | ], 31 | "files.watcherExclude": { 32 | "**/.git": true, 33 | "**/node_modules": true 34 | } 35 | }, 36 | 37 | // Add the IDs of extensions you want installed when the container is created. 38 | "extensions": [ 39 | "dbaeumer.vscode-eslint", 40 | "mtxr.sqltools", 41 | "mtxr.sqltools-driver-pg", 42 | "Vue.volar" 43 | ], 44 | 45 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 46 | "forwardPorts": [8080, 5433], 47 | "portsAttributes": { 48 | "8080": { 49 | "label": "app" 50 | }, 51 | "5433": { 52 | "label": "db" 53 | } 54 | }, 55 | 56 | "containerEnv": { 57 | "POSTGRES_PORT": 5433 58 | }, 59 | 60 | // Use 'onCreateCommand' to run commands when the container is being created. 61 | "onCreateCommand": ".devcontainer/on-create-command.sh", 62 | 63 | // Use 'postCreateCommand' to run commands after the container is created. 64 | "postCreateCommand": ".devcontainer/post-create-command.sh", 65 | 66 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root 67 | "remoteUser": "node" 68 | } 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/design issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Frontend or Design bug 3 | about: template to submit design changes or accessibility bugs 4 | title: Frontend or Design bug 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | Thank you for taking the time to file an issue. Please make sure you have completed the relevant items from the checklist below. After doing so, you may delete the checklist. 11 | 12 | ## What screen is this? 13 | 14 | ## Which component? Which piece of copy or graphic? 15 | 16 | **What is the change proposed? (add a figma screenshot, follow the workflow here)** 17 | 18 | 19 | - [ ] Briefly describe the bug's *user-visible* impact in the title 20 | - [ ] If possible, include reproduction steps using the template below. 21 | - [ ] If this is an API bug, please include curl commands and output if possible. 22 | - [ ] If this is a visual bug, please include a screenshot or a GIF. 23 | - [ ] Content bug 24 | - [ ] Record any ideas you have as to the cause of the problem or how to fix it. 25 | - [ ] Add any labels that you might think are relevant to the problem. 26 | 27 | 28 | **Steps to reproduce:** 29 | 30 | 1. 31 | 2. 32 | 3. 33 | 34 | **Actual behaviour:** 35 | 36 | [Describe the behavior that actually happens under the conditions listed above.] 37 | 38 | **Expected behaviour:** 39 | 40 | [Describe what you expect the behaviour under the conditions listed above to be.] 41 | 42 | 43 | **Which topic does this educate the constituent around? (add a short description on how its clearer than the original) ?** 44 | 45 | _Advocacy values to consider:_ 46 | - [ ] Testimonials should be personal 47 | - [ ] Language should be simple 48 | - [ ] People are lead by causes and their impact 49 | - [ ] Accessibility across abilities 50 | 51 | 52 | **What are frontend tasks?** (if theres any tasks needed outside of the template below, pick a different color like blue) 53 | - [ ] Add image here in this file 54 | - [ ] Insert copy in this file 55 | 56 | _List files that need to be changed next to task_ 57 | 58 | **CC:** @frontend-team member, @frontend-coordinator, @research-coordinator 59 | 60 | -------------------------- 61 | For Coordinator 62 | - [ ] add appropriate labels: "good-first-issue", "design", "screen label", "intermediate" 63 | - [ ] assign time label 64 | - [ ] Approved and on project board 65 | -------------------------------------------------------------------------------- /server/db/migrations/20231022014507_update_constituent_transaction_letters_tables.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | async up(knex) { 3 | await knex.schema.alterTable('constituents', (table) => { 4 | table.timestamps(false, true, false) 5 | table.renameColumn('street_address', 'address_line_1') 6 | table.renameColumn('address_two', 'address_line_2') 7 | }) 8 | 9 | // Renaming 10 | await knex.schema.renameTable('sent_letters', 'letters') 11 | 12 | await knex.schema.alterTable('letters', (table) => { 13 | table.integer('transaction_id') 14 | table.foreign('transaction_id').references('transactions.id') 15 | table.string('letter_template') 16 | table.string('letter_version') // Removes fkey for letter versions 17 | table.string('addressee') 18 | table.string('address_line_1') 19 | table.string('address_line_2') 20 | table.string('city') 21 | table.string('state') 22 | table.string('zip') 23 | table.string('return_address') 24 | table.boolean('sent').defaultTo(false) 25 | table.timestamps(false, true, false) 26 | 27 | table.dropColumn('request_id') 28 | table.dropColumn('requested_at') 29 | table.dropColumn('rep_name') 30 | table.dropColumn('rep_address') 31 | table.dropColumn('letter_version_id') 32 | }) 33 | 34 | await knex.schema 35 | .alterTable('transactions', (table) => { 36 | table.integer('constituent_id') 37 | table.foreign('constituent_id').references('constituents.id') 38 | 39 | // Timestamps are not implemented with proper defaults so we need to drop them here... 40 | table.dropTimestamps() 41 | 42 | table.dropColumn('payment_method_type') 43 | table.dropColumn('email') 44 | }) 45 | .then(() => { 46 | // ...then we add timestamps again here. 47 | return knex.schema.alterTable('transactions', (table) => { 48 | table.timestamps(false, true, false) 49 | }) 50 | }) 51 | 52 | await knex.schema.dropTable('letter_versions') 53 | }, 54 | 55 | async down(knex) { 56 | await knex.schema.dropTable('constituents') 57 | await knex.schema.dropTable('transactions') 58 | await knex.schema.dropTable('sent_letters') 59 | await knex.schema.dropTable('letter_versions') 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 74 | 75 | 91 | -------------------------------------------------------------------------------- /AmplifyApp.md: -------------------------------------------------------------------------------- 1 | https://user-images.githubusercontent.com/9143339/149375273-ce0f0b9f-1c07-417f-b2a7-cea488e552d6.mp4 2 | 3 | ProgramEquity Hacks brings the community informed design to an open source workflow. When designing for marginalized communities, we found that past attempts at civic engagement were found to have created insignificant changes because research focused on the constituent and forgot to consider the community the constituent was a part of. As a result, this open source initiative addresses solutions as ecosystems including the constituent, Advocacy experts, DevSecOps architects, and OSS maintainership lens as part of a Community Informed Design framework which extends beyond Human Centered design - the missing inigredient specified by [MIT Studies](https://mitgovlab.org/news/is-civic-tech-fulfilling-its-promise/) 4 | 5 | ### [Try it out in our Invision](https://manishapriyadarshini245795.invisionapp.com/console/Amplify-cknropnaf0s0901873w3z29g8/ckwvw736t00ll01996u9d2ih2/play) 6 | 7 | The MIT study astutley observed that tech solutions must engage with the ecosystem before designing for solutions. Goals between CivTech, understanding of bureaucratic processes and leading by the impacted group leads to results that fall short of the vision is often due a lack of communication. [MIT GOVLAB]() 8 | 9 | 10 | ### Community Informed Design Explained 11 | - Our **first layer** of defining the problem looks at the pain points of users (these workshops will be provided throughout the day) 12 | - User advocacy research via [MERL ](https://merltech.org/) 13 | - Data protection and compliance through [PII](https://www.red-gate.com/simple-talk/devops/data-privacy-and-protection/introduction-to-devops-security-privacy-and-compliance/) 14 | - Person informed design via [TrueNorth](https://www.truenorthedi.com/how-we-work.html) 15 | - Our **second layer** focuses on implementation and orchestrates things via a [DevSecOps workflow](https://github.com/learn/devops) 16 | - Our **third layer** focuses on the sustainability and security of the project on open source through an [MVG framework ](https://github.blog/2021-07-22-minimum-viable-governance-lightweight-community-structure-foss-projects/) 17 | Screen Shot 2022-01-08 at 1 40 47 PM 18 | -------------------------------------------------------------------------------- /server/db/migrations/20211230165803_harden-campaigns-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'campaigns' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Alter the table 6 | await knex.schema.alterTable(tableName, function (table) { 7 | // Add NOT NULL constraints to simple fields 8 | table.string('name').notNullable().alter() 9 | table.string('organization').notNullable().alter() 10 | table.integer('letters_counter').notNullable().alter() 11 | 12 | // Change type from string to text and add NOT NULL constraint 13 | table.text('page_url').notNullable().alter() 14 | 15 | // Change type from string to native enum types 16 | table 17 | .enum('cause', ['Civic Rights', 'Education', 'Climate Justice'], { 18 | useNative: true, 19 | enumName: 'cause_type' 20 | }) 21 | .notNullable() 22 | .alter() 23 | 24 | table 25 | .enum('type', ['Starter', 'Accelerator', 'Grant'], { 26 | useNative: true, 27 | enumName: 'campaign_type' 28 | }) 29 | .notNullable() 30 | .alter() 31 | 32 | // Add indexes 33 | table.index(['name']) 34 | table.index(['organization']) 35 | 36 | // Add unique indexes 37 | table.unique(['name', 'organization']) 38 | }) 39 | }, 40 | 41 | async down(knex) { 42 | // Alter the table 43 | await knex.schema.alterTable(tableName, function (table) { 44 | // Drop unique indexes 45 | table.dropUnique(['name', 'organization']) 46 | 47 | // Drop indexes 48 | table.dropIndex(['organization']) 49 | table.dropIndex(['name']) 50 | 51 | // Drop NOT NULL constraints from simple fields 52 | table.string('name').nullable().alter() 53 | table.string('organization').nullable().alter() 54 | table.integer('letters_counter').nullable().alter() 55 | 56 | // Change type from text to string and drop NOT NULL constraint 57 | table.string('page_url').nullable().alter() 58 | 59 | // Change type from native enum to string and drop NOT NULL constraint 60 | table.string('cause').nullable().alter() 61 | table.string('type').nullable().alter() 62 | }) 63 | 64 | // Manually remove the native enum types 65 | await knex.raw(`DROP TYPE IF EXISTS cause_type;`) 66 | await knex.raw(`DROP TYPE IF EXISTS campaign_type;`) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/__tests__/integration/axios-cache-interceptor.test.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const Axios = require('axios') 3 | const { setupCache } = require('axios-cache-interceptor') 4 | 5 | // 1. we test that the configuration we set is valid and available to check 6 | describe('test Axios configuration', () => { 7 | test('tests argument composition', () => { 8 | const axios = Axios.create() 9 | const withAxios = setupCache(axios) 10 | // we expect axios to be properly configured with setupCache 11 | expect(withAxios).not.toBeUndefined() 12 | 13 | // we expect the cache to be properly configured and contain our properties after initialization 14 | const withConfig = setupCache(axios, { ttl: 1234 }) 15 | expect(withConfig).not.toBeUndefined() 16 | expect(withConfig.defaults.cache.ttl).toBe(1234) 17 | }) 18 | }) 19 | 20 | // 2. we test that the cache is working as expected with concurrent requests 21 | describe('test Axios cache with two requests', () => { 22 | test('tests cache', async () => { 23 | const axios = Axios.create() 24 | const cache_interceptor = setupCache(axios) 25 | 26 | // we make two sequential requests to the same api endpoint 27 | const req1 = cache_interceptor.get('https://github.com/OpenSourceFellows/amplify') 28 | const req2 = cache_interceptor.get('https://github.com/OpenSourceFellows/amplify/') 29 | const res1 = await req1 30 | const res2 = await req2 31 | 32 | // assertions: we expect the second response to be cached 33 | expect(res1.cached).toBe(false) 34 | expect(res2.cached).toBe(true) 35 | }) 36 | }) 37 | 38 | // 3. we test that the cache is invalidated when the ttl expires 39 | describe('test Axios cache invalidation option', () => { 40 | test('tests cache invalidation', async () => { 41 | // we set the ttl to 10 seconds 42 | const axios = Axios.create() 43 | const cache_interceptor = setupCache(axios, { ttl: 10000 }) 44 | // make a simple request 45 | await cache_interceptor.get('https://github.com/OpenSourceFellows/amplify') 46 | // wait for 11 seconds and make another call 47 | await new Promise((resolve) => setTimeout(() => resolve(), 11000)) 48 | const res3 = await cache_interceptor.get('https://github.com/OpenSourceFellows/amplify') 49 | // assertions: the second request is expected not to be cached, due to the ttl 50 | expect(res3.cached).toBe(false) 51 | }, 70000) 52 | }) 53 | -------------------------------------------------------------------------------- /src/components/CampaignCards.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /.github/workflows/scorecards-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | 3 | on: 4 | # Only the default branch is supported. 5 | branch_protection_rule: 6 | schedule: 7 | - cron: '43 10 * * 6' 8 | push: 9 | branches: 10 | - main 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | analysis: 17 | runs-on: ubuntu-latest 18 | 19 | permissions: 20 | actions: read 21 | contents: read 22 | # Used to receive a badge. 23 | id-token: write 24 | # Needed to upload the results to code-scanning dashboard. 25 | security-events: write 26 | 27 | steps: 28 | - name: Check out repo 29 | uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b 30 | with: 31 | persist-credentials: false 32 | 33 | - name: Run analysis 34 | uses: ossf/scorecard-action@e363bfca00e752f91de7b7d2a77340e2e523cb18 35 | with: 36 | results_file: results.sarif 37 | results_format: sarif 38 | # Read-only PAT token. To create it, 39 | # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. 40 | repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} 41 | # Publish the results to enable scorecard badges. For more details, see 42 | # https://github.com/ossf/scorecard-action#publishing-results. 43 | # For private repositories, `publish_results` will automatically be set to `false`, 44 | # regardless of the value entered here. 45 | publish_results: true 46 | 47 | # Upload the results as artifacts (optional). 48 | - name: Upload artifact 49 | uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 50 | with: 51 | name: SARIF file 52 | path: results.sarif 53 | retention-days: 5 54 | 55 | # Upload the results to GitHub's code scanning dashboard. 56 | - name: Upload to code-scanning 57 | uses: github/codeql-action/upload-sarif@f3feb00acb00f31a6f60280e6ace9ca31d91c76a 58 | with: 59 | sarif_file: results.sarif 60 | 61 | # Create a job summary with the OSSF badge. 62 | - name: Output custom summary 63 | run: echo "["'!'"[OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/$GITHUB_REPOSITORY/badge)](https://api.securityscorecards.dev/projects/github.com/$GITHUB_REPOSITORY)" >> $GITHUB_STEP_SUMMARY 64 | -------------------------------------------------------------------------------- /roiScript/send-time.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from '@notionhq/client' 2 | 3 | /* 4 | TODO: extract the originaltime value and send it to Notion 5 | labels interface (filtered labels that contain `originaltime-`): 6 | [ 7 | { 8 | "color": "5C19CB", 9 | "default": false, 10 | "description": "", 11 | "id": 6666571227, 12 | "name": "originaltime-4000", 13 | "node_id": "LA_kwDOG0x5L88AAAABjVvN2w", 14 | "url": "https://api.github.com/repos/OpenSourceFellows/amplify/labels/originaltime-4000" 15 | } 16 | ] 17 | */ 18 | 19 | // TODO: fix the unsupported engine error 20 | // TODO: send the original time to the Notion DB 21 | const labelObjects = process.env.LABELS 22 | const NOTION_TOKEN = process.env.NOTION_TOKEN 23 | const NOTION_DATABASE_ID = process.env.NOTION_DATABASE_ID 24 | 25 | const parsedLabels = JSON.parse(labelObjects) 26 | const targetLabel = parsedLabels.find((item) => 27 | item.name.startsWith('originaltime-') 28 | ) 29 | const targetLabelName = targetLabel.name 30 | const labelArr = targetLabelName.split('-') 31 | const targetDuration = +labelArr[1] 32 | const issueNumber = process.env.ISSUE_NUMBER 33 | const ghHandle = process.env.GH_HANDLE 34 | 35 | console.log('Send time (labels): ', targetDuration) 36 | console.log('issueNumber: ', issueNumber) 37 | console.log('ghHandle: ', ghHandle) 38 | 39 | // TODO: using this issue number find and update the notion database 40 | 41 | console.log('NOTION_TOKEN: ', NOTION_TOKEN) 42 | console.log('NOTION_DATABASE_ID: ', NOTION_DATABASE_ID) 43 | 44 | // --- 45 | // TODO: rewrite this code from the send-metrics.mjs file 46 | 47 | const notion = new Client({ 48 | auth: NOTION_TOKEN 49 | }) 50 | 51 | const databaseId = NOTION_DATABASE_ID 52 | 53 | // TODO: configure to update the existing issue ids 54 | const newData = { 55 | issue_id: { 56 | rich_text: [ 57 | { 58 | text: { 59 | content: issueNumber 60 | } 61 | } 62 | ] 63 | }, 64 | gh_handle: { 65 | title: [ 66 | { 67 | text: { 68 | content: ghHandle 69 | } 70 | } 71 | ] 72 | }, 73 | original_time: { 74 | number: targetDuration 75 | } 76 | } 77 | 78 | async function addToNotionDatabase() { 79 | try { 80 | const response = await notion.pages.create({ 81 | parent: { 82 | database_id: databaseId 83 | }, 84 | properties: newData 85 | }) 86 | } catch (error) { 87 | console.error('Error adding data to Notion:', error) 88 | } 89 | } 90 | 91 | addToNotionDatabase() 92 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { getEnv } = require('./server/db/util') 3 | 4 | const targetEnv = getEnv() 5 | const isProduction = targetEnv === 'production' 6 | const { POSTGRES_USER, POSTGRES_PASSWORD } = process.env 7 | const POSTGRES_PORT = parseInt(process.env.POSTGRES_PORT, 10) || undefined 8 | const POSTGRES_HOST = process.env.POSTGRES_HOST || 'localhost' 9 | 10 | // Required for Heroku PostgreSQL 11 | // See: https://stackoverflow.com/questions/66497248/heroku-postgres-not-able-to-connect-error-no-pg-hba-conf-entry-for-host 12 | if (isProduction) { 13 | // Set this new environment variable for the process 14 | process.env.PGSSLMODE = 'no-verify' 15 | } 16 | 17 | const baseConfig = { 18 | client: 'postgresql', 19 | 20 | pool: { 21 | min: 2, 22 | max: 10 23 | }, 24 | 25 | migrations: { 26 | tableName: 'knex_migrations', 27 | directory: './server/db/migrations', 28 | stub: './server/db/_migration.stub.js' 29 | }, 30 | 31 | seeds: { 32 | // This value intentionally results in a failure if not overridden (or handled) 33 | directory: `./server/db/seeds/non-existent-directory`, 34 | stub: './server/db/_seed.stub.js' 35 | }, 36 | 37 | // Turn these off in production for performance reasons 38 | asyncStackTraces: !isProduction, 39 | 40 | // Required for Heroku PostgreSQL 41 | ...(isProduction && { 42 | ssl: { 43 | rejectUnauthorized: false 44 | } 45 | }) 46 | } 47 | 48 | // Export the configuration matrix 49 | module.exports = { 50 | development: { 51 | ...baseConfig, 52 | connection: { 53 | database: 'pe_dev', 54 | ...(POSTGRES_HOST && { host: POSTGRES_HOST }), 55 | ...(POSTGRES_USER && { user: POSTGRES_USER }), 56 | ...(POSTGRES_PASSWORD && { password: POSTGRES_PASSWORD }), 57 | ...(POSTGRES_PORT && { port: POSTGRES_PORT }) 58 | }, 59 | seeds: { 60 | ...baseConfig.seeds, 61 | directory: './server/db/seeds/development' 62 | } 63 | }, 64 | 65 | test: { 66 | ...baseConfig, 67 | connection: { 68 | database: 'pe_test', 69 | ...(POSTGRES_HOST && { host: POSTGRES_HOST }), 70 | ...(POSTGRES_USER && { user: POSTGRES_USER }), 71 | ...(POSTGRES_PASSWORD && { password: POSTGRES_PASSWORD }), 72 | ...(POSTGRES_PORT && { port: POSTGRES_PORT }) 73 | }, 74 | seeds: { 75 | ...baseConfig.seeds, 76 | directory: './server/db/seeds/test' 77 | } 78 | }, 79 | 80 | production: { 81 | ...baseConfig, 82 | connection: process.env.DATABASE_URL 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /server/lib/stripe.js: -------------------------------------------------------------------------------- 1 | // Wrappers for Stripe's npm package 2 | require('dotenv').config() 3 | const stripe = require('stripe') 4 | 5 | class StripeError extends Error { 6 | constructor(message) { 7 | super(message) 8 | this.name = 'StripeError' 9 | } 10 | } 11 | 12 | class Stripe { 13 | constructor() { 14 | this.stripeSecret = process.env.STRIPE_SECRET_KEY 15 | this.stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET 16 | this.livemode = process.env.STRIPE_LIVE_MODE 17 | this.stripe = stripe(this.stripeSecret) 18 | } 19 | 20 | /** 21 | * Checks if Stripe is set to 'live' mode. It should be true in production, but false otherwise. 22 | * Think of this as a server-side analog to the 'livemode' attribute on mode Stripe objects. In fact, 23 | * an event object's livemode value and this method should always match. 24 | */ 25 | livemode() { 26 | return this.livemode === 'true' 27 | } 28 | 29 | /** 30 | * Validates that an event actually comes from Stripe. Returns event or throws StripeError. 31 | * @param {string} signature - Stripe signature header. 32 | * @param {object} rawBody - Requests rawBody attribute. 33 | */ 34 | validateEvent(signature, rawBody) { 35 | try { 36 | return this.stripe.webhooks.constructEvent( 37 | rawBody, 38 | signature, 39 | this.stripeWebhookSecret 40 | ) 41 | } catch (error) { 42 | throw new StripeError(error.message) 43 | } 44 | } 45 | 46 | /** 47 | * Creates a checkout session and returns the url and session id. 48 | * @param {number} donationAmount - Donation amount (in cents). 49 | * @param {string} customerEmail - For (optionally) pre-filling customer email on checkout page. 50 | */ 51 | async createCheckoutSession(donationAmount, redirectUrl, cancelUrl) { 52 | try { 53 | const session = await this.stripe.checkout.sessions.create({ 54 | line_items: [ 55 | { 56 | price_data: { 57 | currency: 'usd', 58 | product_data: { 59 | name: 'Donation' 60 | }, 61 | unit_amount: donationAmount 62 | }, 63 | quantity: 1 64 | } 65 | ], 66 | mode: 'payment', 67 | allow_promotion_codes: true, 68 | success_url: redirectUrl, 69 | cancel_url: cancelUrl 70 | }) 71 | 72 | return { 73 | url: session.url, 74 | id: session.id, 75 | paymentIntent: session.payment_intent 76 | } 77 | } catch (error) { 78 | throw new StripeError(error.message) 79 | } 80 | } 81 | } 82 | 83 | module.exports = { Stripe, StripeError } 84 | -------------------------------------------------------------------------------- /src/components/RepresentativeCard.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 80 | 81 | 102 | -------------------------------------------------------------------------------- /server/db/migrations/20211222210238_match-production-campaigns-table.js: -------------------------------------------------------------------------------- 1 | const tableName = 'campaigns' 2 | 3 | module.exports = { 4 | async up(knex) { 5 | // Alter the table 6 | await knex.schema.alterTable(tableName, function (table) { 7 | // Drop unique indexes 8 | table.dropUnique(['name', 'organization']) 9 | 10 | // Drop indexes 11 | table.dropIndex(['organization']) 12 | table.dropIndex(['name']) 13 | 14 | // Drop NOT NULL constraints from simple fields 15 | table.string('name').nullable().alter() 16 | table.string('organization').nullable().alter() 17 | table.integer('letters_sent').nullable().alter() 18 | 19 | // Change type from text to string and drop NOT NULL constraint 20 | table.string('page_url').nullable().alter() 21 | 22 | // Change type from native enum to string and drop NOT NULL constraint 23 | table.string('cause').nullable().alter() 24 | table.string('type').nullable().alter() 25 | }) 26 | 27 | await knex.schema.alterTable(tableName, function (table) { 28 | // Rename column 29 | table.renameColumn('letters_sent', 'letters_counter') 30 | }) 31 | 32 | // Manually remove the native enum types 33 | await knex.raw(`DROP TYPE IF EXISTS cause_type;`) 34 | await knex.raw(`DROP TYPE IF EXISTS campaign_type;`) 35 | }, 36 | 37 | async down(knex) { 38 | // Alter the table 39 | await knex.schema.alterTable(tableName, function (table) { 40 | // Rename column 41 | table.renameColumn('letters_counter', 'letters_sent') 42 | }) 43 | 44 | await knex.schema.alterTable(tableName, function (table) { 45 | // Add NOT NULL constraints to simple fields 46 | table.string('name').notNullable().alter() 47 | table.string('organization').notNullable().alter() 48 | table.integer('letters_sent').notNullable().alter() 49 | 50 | // Change type from string to text and add NOT NULL constraint 51 | table.text('page_url').notNullable().alter() 52 | 53 | // Fields using native enum types 54 | table 55 | .enum('cause', ['Civic Rights', 'Education', 'Climate Justice'], { 56 | useNative: true, 57 | enumName: 'cause_type' 58 | }) 59 | .notNullable() 60 | .alter() 61 | 62 | table 63 | .enum('type', ['Starter', 'Accelerator', 'Grant'], { 64 | useNative: true, 65 | enumName: 'campaign_type' 66 | }) 67 | .notNullable() 68 | .alter() 69 | 70 | // Add indexes 71 | table.index(['name']) 72 | table.index(['organization']) 73 | 74 | // Add unique indexes 75 | table.unique(['name', 'organization']) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/ActionComplete.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 92 | 93 | 98 | -------------------------------------------------------------------------------- /src/components/CampaignBlurb.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 71 | 72 | 114 | -------------------------------------------------------------------------------- /src/auth/auth0-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External Modules 3 | */ 4 | 5 | import Vue from 'vue' 6 | import createAuth0Client from '@auth0/auth0-spa-js' 7 | 8 | /** 9 | * Vue.js Instance Definition 10 | */ 11 | 12 | let instance 13 | 14 | export const getInstance = () => instance 15 | 16 | /** 17 | * Vue.js Instance Initialization 18 | */ 19 | 20 | export const useAuth0 = ({ 21 | onRedirectCallback = () => 22 | window.history.replaceState({}, document.title, window.location.pathname), 23 | redirectUri = window.location.origin, 24 | ...pluginOptions 25 | }) => { 26 | if (instance) return instance 27 | 28 | instance = new Vue({ 29 | data() { 30 | return { 31 | auth0Client: null, 32 | isLoading: true, 33 | isAuthenticated: false, 34 | user: {}, 35 | error: null 36 | } 37 | }, 38 | 39 | async created() { 40 | this.auth0Client = await createAuth0Client({ 41 | ...pluginOptions, 42 | domain: pluginOptions.domain, 43 | client_id: pluginOptions.clientId, 44 | audience: pluginOptions.audience, 45 | redirect_uri: redirectUri 46 | }) 47 | 48 | try { 49 | if ( 50 | window.location.search.includes('code=') && 51 | window.location.search.includes('state=') 52 | ) { 53 | const { appState } = await this.auth0Client.handleRedirectCallback() 54 | 55 | onRedirectCallback(appState) 56 | } 57 | } catch (error) { 58 | this.error = error 59 | } finally { 60 | this.isAuthenticated = await this.auth0Client.isAuthenticated() 61 | this.user = await this.auth0Client.getUser() 62 | this.isLoading = false 63 | } 64 | }, 65 | methods: { 66 | async handleRedirectCallback() { 67 | this.isLoading = true 68 | try { 69 | await this.auth0Client.handleRedirectCallback() 70 | this.user = await this.auth0Client.getUser() 71 | this.isAuthenticated = true 72 | } catch (error) { 73 | this.error = error 74 | } finally { 75 | this.isLoading = false 76 | } 77 | }, 78 | 79 | loginWithRedirect(options) { 80 | return this.auth0Client.loginWithRedirect(options) 81 | }, 82 | 83 | logout(options) { 84 | return this.auth0Client.logout(options) 85 | }, 86 | 87 | getTokenSilently(o) { 88 | return this.auth0Client.getTokenSilently(o) 89 | } 90 | } 91 | }) 92 | 93 | return instance 94 | } 95 | 96 | /** 97 | * Vue.js Plugin Definition 98 | */ 99 | 100 | export const Auth0Plugin = { 101 | install(Vue, options) { 102 | Vue.prototype.$auth = useAuth0(options) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | package-lock.json 5 | 6 | 7 | # local env files 8 | .env.test 9 | .env.local 10 | .env.*.local 11 | .env.test 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | # Logs 28 | logs 29 | *.log 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | lerna-debug.log* 34 | 35 | # Diagnostic reports (https://nodejs.org/api/report.html) 36 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 37 | 38 | # Runtime data 39 | pids 40 | *.pid 41 | *.seed 42 | *.pid.lock 43 | 44 | # Directory for instrumented libs generated by jscoverage/JSCover 45 | lib-cov 46 | 47 | # Coverage directory used by tools like istanbul 48 | coverage 49 | *.lcov 50 | 51 | # nyc test coverage 52 | .nyc_output 53 | 54 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 55 | .grunt 56 | 57 | # Bower dependency directory (https://bower.io/) 58 | bower_components 59 | 60 | # node-waf configuration 61 | .lock-wscript 62 | 63 | # Compiled binary addons (https://nodejs.org/api/addons.html) 64 | build/Release 65 | 66 | # Dependency directories 67 | node_modules/ 68 | jspm_packages/ 69 | 70 | # Snowpack dependency directory (https://snowpack.dev/) 71 | web_modules/ 72 | 73 | # TypeScript cache 74 | *.tsbuildinfo 75 | 76 | # Optional npm cache directory 77 | .npm 78 | 79 | # Optional eslint cache 80 | .eslintcache 81 | 82 | # Microbundle cache 83 | .rpt2_cache/ 84 | .rts2_cache_cjs/ 85 | .rts2_cache_es/ 86 | .rts2_cache_umd/ 87 | 88 | # Optional REPL history 89 | .node_repl_history 90 | 91 | # Output of 'npm pack' 92 | *.tgz 93 | 94 | # Yarn Integrity file 95 | .yarn-integrity 96 | 97 | # parcel-bundler cache (https://parceljs.org/) 98 | .cache 99 | .parcel-cache 100 | 101 | # Next.js build output 102 | .next 103 | out 104 | 105 | # Nuxt.js build / generate output 106 | .nuxt 107 | server/views 108 | 109 | # Gatsby files 110 | .cache/ 111 | # Comment in the public line in if your project uses Gatsby and not Next.js 112 | # https://nextjs.org/blog/next-9-1#public-directory-support 113 | # public 114 | 115 | # vuepress build output 116 | .vuepress/dist 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | # TernJS port file 128 | .tern-port 129 | 130 | # Stores VSCode versions used for testing VSCode extensions 131 | .vscode-test 132 | 133 | # yarn v2 134 | .yarn/cache 135 | .yarn/unplugged 136 | .yarn/build-state.yml 137 | .yarn/install-state.gz 138 | .pnp.* 139 | 140 | # Homebrew 141 | Brewfile.lock.json 142 | -------------------------------------------------------------------------------- /src/components/CampaignHero.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 89 | 90 | 116 | -------------------------------------------------------------------------------- /script/create-db.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { createClient, getConfig, getEnv } = require('../server/db') 4 | 5 | // See https://www.postgresql.org/docs/13/errcodes-appendix.html 6 | const DUPLICATE_DATABASE_ERROR = '42P04' 7 | 8 | bootstrapDatabase() 9 | 10 | async function bootstrapDatabase() { 11 | const targetEnv = getEnv() 12 | if (targetEnv === 'production') { 13 | throw new Error('This script should not be used in production!') 14 | } 15 | 16 | const config = getConfig(targetEnv) 17 | await createDatabase(config) 18 | await migrateToLatestSchemas(config) 19 | await runDataSeeders(config) 20 | } 21 | 22 | async function createDatabase(config) { 23 | const { database } = config.connection 24 | let db 25 | 26 | try { 27 | // Connect with system database selected 28 | db = createClient({ 29 | ...config, 30 | connection: { 31 | ...config.connection, 32 | database: 'postgres' 33 | } 34 | }) 35 | 36 | // Create the database if it doesn't already exist 37 | await db.raw(`CREATE DATABASE ${database}`) 38 | console.log(`Created database "${database}"!`) 39 | } catch (error) { 40 | if (error.code === DUPLICATE_DATABASE_ERROR) { 41 | console.warn(`Error creating database "${database}": it already exists!`) 42 | } else { 43 | console.error(`Error creating database "${database}": ${error.message}`) 44 | throw error 45 | } 46 | } finally { 47 | // Disconnect 48 | await db.destroy() 49 | } 50 | } 51 | 52 | async function migrateToLatestSchemas(config) { 53 | const { database } = config.connection 54 | let db 55 | 56 | try { 57 | db = createClient(config) 58 | await db.migrate.latest() 59 | console.log(`Migrated database "${database}" to latest schemas!`) 60 | } catch (error) { 61 | console.error( 62 | `Error migrating to latest schemas for database "${database}": ${error.message}` 63 | ) 64 | throw error 65 | } finally { 66 | // Disconnect 67 | await db.destroy() 68 | } 69 | } 70 | 71 | // eslint-disable-next-line no-unused-vars 72 | async function runDataSeeders(config) { 73 | const { database } = config.connection 74 | let db 75 | 76 | if (!config || !config.seeds || !config.seeds.directory) { 77 | console.warn('Skipping! No data seed directory is configured.') 78 | return 79 | } 80 | 81 | const seedDir = path.resolve(process.cwd(), config.seeds.directory) 82 | const seedDirStats = fs.statSync(seedDir, { throwIfNoEntry: false }) 83 | if (!seedDirStats) { 84 | console.warn( 85 | `Skipping! The data seed directory does not exist: ${config.seeds.directory}` 86 | ) 87 | return 88 | } 89 | 90 | try { 91 | if (!seedDirStats.isDirectory()) { 92 | throw new Error( 93 | `The data seed path exists but is not a directory: ${config.seeds.directory}` 94 | ) 95 | } 96 | 97 | db = createClient(config) 98 | await db.seed.run() 99 | console.log(`Created seed data in database "${database}"!`) 100 | } catch (error) { 101 | console.error( 102 | `Error creating seed data for database "${database}": ${error.message}` 103 | ) 104 | throw error 105 | } finally { 106 | // Disconnect 107 | if (db) { 108 | await db.destroy() 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /server/lib/lob.js: -------------------------------------------------------------------------------- 1 | // Wrapper for the Lob API 2 | // We have the SDK in this project but it has incomplete functionality 3 | // so we will use endpoints directly. 4 | require('dotenv').config() 5 | const axios = require('axios') 6 | 7 | class LobError extends Error { 8 | constructor(message) { 9 | super(message) 10 | this.name = 'LobError' 11 | } 12 | } 13 | 14 | class Lob { 15 | constructor() { 16 | this.apiKey = process.env.LOB_API_KEY 17 | this.lobUrl = process.env.LOB_BASE_URL 18 | this.env = process.env.NODE_ENV 19 | } 20 | 21 | // auth headers 22 | authHeaders() { 23 | const encodedKey = btoa(`${this.apiKey}:`) 24 | console.log(encodedKey) 25 | 26 | return { 27 | Authorization: `Basic ${encodedKey}` 28 | } 29 | } 30 | 31 | // Merges all headers 32 | headers() { 33 | return { ...this.authHeaders() } 34 | } 35 | 36 | async template(id) { 37 | try { 38 | const letters = await axios.get(`${this.lobUrl}/templates/${id}`, { 39 | headers: this.headers() 40 | }) 41 | 42 | return letters.data.published_version 43 | } catch (err) { 44 | throw new LobError(err.message) 45 | } 46 | } 47 | 48 | async send(payload) { 49 | const data = { 50 | to: payload.to, 51 | from: payload.from, 52 | file: payload.templateId, 53 | merge_variables: payload.mergeVariables, 54 | use_type: 'operational', 55 | color: false 56 | } 57 | 58 | try { 59 | if (this.env != 'production') { 60 | return { response: 200, payload } 61 | } 62 | 63 | const response = await axios.post({ 64 | url: `${this.lobUrl}/letters}`, 65 | headers: this.headers(), 66 | 'Idempotency-key': payload.uuid, 67 | data 68 | }) 69 | 70 | return response.data 71 | } catch (err) { 72 | throw new LobError(err.message) 73 | } 74 | } 75 | 76 | async verifyAddress(address) { 77 | const payload = { 78 | recipient: address.name, 79 | primary_line: address.line1, 80 | secondary_line: address.line2, 81 | city: address.city, 82 | state: address.state, 83 | zip_code: address.zip 84 | } 85 | 86 | try { 87 | const addr = axios.post({ 88 | url: `${this.lobUrl}/us_verifications`, 89 | headers: this.headers(), 90 | data: payload 91 | }) 92 | 93 | return addr.data 94 | } catch (err) { 95 | throw new LobError(err.message) 96 | } 97 | } 98 | 99 | // Args should be the payload returned by verifyAddress() 100 | // See https://docs.lob.com/#tag/US-Verifications/operation/us_verification 101 | // For some reason, the parameter names are different b/w endpoints --_-- 102 | async returnAddress(verifiedAddress) { 103 | const payload = { 104 | name: verifiedAddress.name, 105 | address_line1: verifiedAddress.primary_line, 106 | address_line2: verifiedAddress.secondary_line, 107 | address_city: verifiedAddress.components.city, 108 | address_state: verifiedAddress.components.state, 109 | address_zip: verifiedAddress.components.zip_code 110 | } 111 | 112 | try { 113 | const addr = axios.post({ 114 | url: `${this.lobUrl}/addresses`, 115 | headers: this.headers(), 116 | data: payload 117 | }) 118 | 119 | return addr.data 120 | } catch (err) { 121 | throw new LobError(err.message) 122 | } 123 | } 124 | } 125 | 126 | module.exports = { Lob, LobError } 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amplify 2 | 3 | Amplify is an open-source app created for users to take the initiative in being part of an actionable step in the efforts to protect against climate change. The user is able to choose a climate campaign, then using their zip code, they will be able to select a representative of their choice. The user then donates to have their letter sent out by Amplify 4 | 5 | 6 | ## Table of Contents 7 | 8 | * [Getting Started](#getting-started) 9 | * [Project Setup](#project-setup) 10 | * [Customize configuration](#customize-configuration) 11 | * [App Walkthrough](#app-walkthrough) 12 | 13 | ## Getting Started 14 | 15 | This repo contains both the frontend and backend portions of the Amplify application. 16 | 17 | The frontend code is stored in the `src/` directory. 18 | 19 | The backend (API) code is stored in the `server/` directory. 20 | 21 | **Project Workflow:** 22 | 23 | [Enablement Deck](https://docs.google.com/presentation/d/1llJgeTU1EzRSYB8kL-IQeAdoq7p6xb4ApEac5E3M8Qo/edit?usp=sharing) 24 | 25 | Screen Shot 2021-05-15 at 11 26 45 AM 26 | 27 | - [Overall Project Board](https://github.com/ProgramEquity/amplify/projects?type=beta) 28 | - [Feature Breakdown](https://github.com/ProgramEquity/amplify/discussions/62) 29 | - [OSS Architecture](https://github.com/ProgramEquity/amplify/discussions/61) 30 | - [Wiki](https://github.com/ProgramEquity/amplify/wiki): API methods, Data Structures 31 | 32 | **Resources:** 33 | - We meet every **Wednesday from 12-1 p.m. PT and Thursdays from 8-8:30 p.m. PT**. Sign up for an [orientation](https://forms.gle/4miQJ8ccuWdeJha16) 34 | - Try out our [demo](https://www.figma.com/file/46c9cmuTiCpFA4DHB8OK0H/Amplify-User-Interface-%2B-Design-Guide?node-id=1585%3A653) or review [App Research](https://www.notion.so/programequity/Dare-to-Dream-Civic-Engagement-is-key-to-change-595ca4db3a2948c6b44569b58d530c8c) 35 | 36 | # Project setup 37 | In order to get started, you can clone, download a ZIP, or fork this repository to work on your local machine. If you would like to get started with Codespaces instead, the video below will walk you through setting up your Codespace. 38 | 39 | https://user-images.githubusercontent.com/9143339/159093687-6fc90733-0599-445c-b08b-a6378d988e4b.mov 40 | 41 | Codespaces Set Up 42 | 43 | ## Contributing 44 | Would you like to become a contributor? Please check out our [contributors guide](./CONTRIBUTING.md)! 💝 45 | 46 | Run the following script first: 47 | ```shell 48 | script/bootstrap 49 | ``` 50 | 51 | You will need to copy the `.env.example` file to a `.env` file in this repo. You can use the following command in your terminal: 52 | ```shell 53 | cp .env.example .env 54 | ``` 55 | 56 | ### Compiles and hot-reloads full app for development 57 | ```shell 58 | npm run dev 59 | ``` 60 | 61 | ### Compiles and minifies for production 62 | ```shell 63 | npm run build 64 | ``` 65 | 66 | ### Lints and fixes files 67 | ```shell 68 | npm run lint 69 | ``` 70 | 71 | ### Runs Prettier and fixes files 72 | ```shell 73 | npm run format 74 | ``` 75 | 76 | ### Build and run as if in prod 77 | ```shell 78 | npm start 79 | ``` 80 | 81 | ### Customize configuration 82 | See [Configuration Reference](https://cli.vuejs.org/config/). 83 | 84 | # App Walkthrough 85 | 86 | **App Structure** 87 | 88 | Current Structure: 89 | 90 | Current_Structure 91 | 92 | Goal Structure: 93 | 94 | Goal_Structure 95 | 96 | -------------------------------------------------------------------------------- /roiScript/send-metrics.mjs: -------------------------------------------------------------------------------- 1 | import { Client } from '@notionhq/client' 2 | // const { Client } = require('@notionhq/client'); 3 | 4 | // console.log('entered the file') 5 | // TODO: test and move to the secrets in the repo 6 | // integration key 7 | const NOTION_TOKEN = process.env.NOTION_TOKEN 8 | // database id 9 | const NOTION_DATABASE_ID = process.env.NOTION_DATABASE_ID 10 | 11 | const notion = new Client({ 12 | auth: NOTION_TOKEN 13 | }) 14 | 15 | const commentsDataJSON = process.env.PR_PAYLOAD 16 | const commentsUrl = JSON.parse(commentsDataJSON) 17 | console.log('commentsUrl: ', commentsUrl) 18 | 19 | const issueID = process.env.BRANCH_NAME 20 | const parsedIssueId = issueID.split('-')?.at(1) 21 | console.log('issueID: ', issueID) 22 | console.log('parsedIssueId: ', parsedIssueId) 23 | 24 | const ghHandle = process.env.GH_HANDLE 25 | console.log('ghHandle', ghHandle) 26 | 27 | const fetchCommentsJSON = async (commentsUrlStr) => { 28 | try { 29 | const response = await fetch(commentsUrlStr) 30 | if (!response.ok) { 31 | throw new Error(`Failed to fetch comments. Status: ${response.status}`) 32 | } 33 | const commentsData = await response.json() 34 | 35 | return commentsData 36 | } catch (error) { 37 | console.error('Error fetching comments:', error.message) 38 | return null 39 | } 40 | } 41 | 42 | function extractNumberFromComment(commentBody) { 43 | const match = commentBody.match( 44 | /Time from assignment to PR for #\d+: (\d+) seconds/ 45 | ) 46 | return match ? parseInt(match[1]) : null 47 | } 48 | 49 | const commentsArr = await fetchCommentsJSON(commentsUrl) 50 | const targetComment = commentsArr?.find((comment) => 51 | comment.body.startsWith('Time from assignment to PR for') 52 | ) 53 | const targetCommentBody = targetComment.body 54 | console.log('targetComment.body: ', targetComment.body) 55 | const durationValue = extractNumberFromComment(targetCommentBody) 56 | console.log('durationValue', durationValue) 57 | 58 | const databaseId = NOTION_DATABASE_ID 59 | 60 | // --- 61 | 62 | // current version 63 | async function updateNotionDatabase() { 64 | try { 65 | const response = await notion.databases.query({ 66 | database_id: databaseId, 67 | filter: { 68 | property: 'issue_id', 69 | rich_text: { 70 | equals: parsedIssueId 71 | } 72 | } 73 | }) 74 | 75 | console.log('response', response) 76 | 77 | // const pageId = response.results[0].id; 78 | // const pageProperties = response.results[0].properties; 79 | 80 | // pageProperties.duration = { 81 | // number: durationValue 82 | // }; 83 | 84 | // await notion.pages.update({ 85 | // page_id: pageId, 86 | // properties: pageProperties 87 | // }); 88 | } catch (error) { 89 | console.error('Error updating data in Notion:', error) 90 | } 91 | } 92 | 93 | updateNotionDatabase() 94 | 95 | // --- 96 | 97 | // previous version 98 | // mock data to test the connection 99 | // const newData = { 100 | // // properties: 101 | // issue_id: { 102 | // rich_text: [ 103 | // { 104 | // text: { 105 | // content: parsedIssueId 106 | // } 107 | // } 108 | // ] 109 | // }, 110 | // gh_handle: { 111 | // title: [ 112 | // { 113 | // text: { 114 | // content: ghHandle 115 | // } 116 | // } 117 | // ] 118 | // }, 119 | // duration: { 120 | // number: durationValue 121 | // } 122 | // } 123 | 124 | // async function addToNotionDatabase() { 125 | // try { 126 | // const response = await notion.pages.create({ 127 | // parent: { 128 | // database_id: databaseId 129 | // }, 130 | // properties: newData 131 | // }) 132 | // // console.log(response) 133 | // } catch (error) { 134 | // console.error('Error adding data to Notion:', error) 135 | // } 136 | // } 137 | 138 | // addToNotionDatabase() 139 | --------------------------------------------------------------------------------