Browse Source

Initial commit.

main
Atridad Lahiji 2 months ago
parent
commit
fbbc4b4d1b
  1. 25
      .env.example
  2. 2
      .gitignore
  3. 28
      .gitlab-ci.yml
  4. 92
      README.md
  5. 12
      assets/css/styles.css
  6. BIN
      assets/img/favicon.png
  7. BIN
      assets/img/logo.png
  8. 29
      build-backend.sh
  9. 6
      build-frontend.sh
  10. 3
      frontend/.browserslistrc
  11. 7
      frontend/.editorconfig
  12. 28
      frontend/.eslintrc.js
  13. 23
      frontend/.gitignore
  14. 29
      frontend/README.md
  15. 5
      frontend/babel.config.js
  16. 3
      frontend/jest.config.js
  17. 16585
      frontend/package-lock.json
  18. 56
      frontend/package.json
  19. 18
      frontend/public/index.html
  20. 270
      frontend/src/App.vue
  21. 218
      frontend/src/components/Index.vue
  22. 27
      frontend/src/main.js
  23. 7
      frontend/src/plugins/bootstrap-vue.js
  24. 96
      frontend/tests/unit/index.spec.js
  25. 11555
      frontend/yarn.lock
  26. 13
      go.mod
  27. 60
      go.sum
  28. 245
      main.go

25
.env.example

@ -0,0 +1,25 @@
OWNER_NAME="Owner Person"
META_TITLE="EleutheriaPay"
TITLE="EleutheriaPay"
DESCRIPTION="Customizable FOSS donation platform for FOSS developers"
DEFAULT_CURRENCY="usd"
STRIPE_PK=""
STRIPE_SK=""
STRIPE_ACC=""
STRIPE_API_VERSION="2020-08-27"
STRIPE_LOCALE="auto"
OWNCAST_HOSTNAME=""
OWNCAST_TOKEN=""
BADGE_TEXT="Support Me"
BADGE_COLOR="2980b9"
BADGE_FONT_SIZE="20"
BADGE_HEIGHT="28"
BACK_LINK=""
EMAIL_LINK=""
GIT_LINK="https://git.atrilahiji.dev/atrilahiji/eleutheriapay"
GITHUB_LINK=""
GITLAB_LINK=""
FACEBOOK_LINK=""
TWITTER_LINK=""
INSTAGRAM_LINK=""
LINKEDIN_LINK=""

2
.gitignore

@ -0,0 +1,2 @@
.env
bin/

28
.gitlab-ci.yml

@ -0,0 +1,28 @@
build_front:
image: node:latest
stage: build
script:
- chmod +x ./build-frontend.sh
- ./build-frontend.sh
artifacts:
expire_in: 1 week
paths:
- dist
only:
- main
- master
build_back:
image: golang:latest
stage: build
script:
- chmod +x ./build-frontend.sh
- ./build-backend.sh
artifacts:
expire_in: 1 week
paths:
- dist
only:
- main
- master

92
README.md

@ -1,2 +1,92 @@
# EletheriaPay
# EleutheriaPay
![EleutheriaPay](https://cloud.atridad.dev/s/jAeYW6nYd7W6sma/preview "EleutheriaPay")
<a href="https://support.atrilahiji.dev/" target="_blank">
<img src="https://support.atrilahiji.dev/api/badge" />
</a>
Elutheria Pay is a customizable self-hosted FOSS donation page for FOSS developers
DEMO: https://eleutheriapaydemo.atrilahiji.dev
# Requirements
- Node.js 12 or higher (Build)
- Golang 1.16.x or higher (Run)
- A linux machine or WSL if you would like to use the helper scripts I wrote (Build + Run)
- A stripe account with email notifications for transactions enabled: https://stripe.com/docs/receipts (Run)
# Install
## Download the latest release and extract it (sort by name)
https://s3.atridad.dev/minio/eleutheriapay/bin/
## Edit the default .env file that is created (the parameters are outlined below)
IMPORTANT: Please ensure your stripe publishable and secret keys are set before running EleutheriaPay.
## Run the binary
EleutheriaPay will run a server on localhost:3000. Please use this port when configuring a reverse proxy such as NGINX.
```
./eleutheriapay-${VERSION}-${GOOS}-${GOARCH}
```
# Build from source
## Install the rice tool to bundle the frontend with the backend
```
go get github.com/GeertJohan/go.rice/rice
```
## Run the build script
```
./build.sh
```
# Configuraton
## Site Configuration
```
OWNER_NAME: The name of the individual or organization this page is accepting donations for.
META_TITLE: The metadata title for the site.
TITLE: The title on the homepage.
DESCRIPTION: The description of the homepage.
DEFAULT_CURRENCY: The default currency when the page is first reached.
STRIPE_PK: Stripe publishable key.
STRIPE_SK: Stripe secret key.
STRIPE_ACC: Stripe account ID
STRIPE_API_VERSION: Stripe API version
STRIPE_LOCALE: Stripe locale (auto is recommended)
OWNCAST_HOSTNAME: The hostname of your Owncast instance (optional)
OWNCAST_TOKEN: Your Owncast API Token
BADGE_TEXT: The text show on the donation badge.
BADGE_COLOR: The hex value of the donation badge color.
BADGE_FONT_SIZE: An integer value representing the font size of the donationbadge.
BADGE_HEIGHT: The height of the donation badge in pixels.
BACK_LINK: The link the user a redirected to when clicking on the back button.
Social links:
EMAIL_LINK
GIT_LINK
GITHUB_LINK
GITLAB_LINK
FACEBOOK_LINK
TWITTER_LINK
INSTAGRAM_LINK
LINKEDIN_LINK
```
## Theme Configuration
The theme can be customized via the assets directory. Here you can change the logo, favicon, and modify the CSS.
# Embeddable badge
A donate badge can be embedded onto any page or repository to route users to your EleutheriaPay instance. You will need to embed the following endpoint and link it to your instance:
```
<a href="https://donate.<DOMAIN>" target="_blank">
<img src="https://donate.<DOMAIN>/api/badge" />
</a>
```
# Official Integrations
<a href="https://owncast.online/" target="_blank" rel="nofollow">
<img src="https://owncast.online/images/logo.svg" width="50" height="50" />
</a>

12
assets/css/styles.css

@ -0,0 +1,12 @@
:root {
--bg-color: #ffffff;
--text-color: #2c3e50;
--button-color: #2980b9;
--top-nav-color: #16a085;
}
[data-theme="dark"] {
--bg-color: #2c3e50;
--text-color: #ffffff;
--button-color: #2980b9;
--top-nav-color: #16a085;
}

BIN
assets/img/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
assets/img/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

29
build-backend.sh

@ -0,0 +1,29 @@
#!/bin/bash
# build backend
VERSION="1.2.2"
export OLD_GOOS=$(go env GOOS)
export OLD_GOARCH=$(go env GOARCH)
mkdir bin
# buid for macos x86 64-bit
export GOOS=darwin
export GOARCH=amd64
mkdir bin/$(go env GOOS)-$(go env GOARCH)
cp .env.example ./bin/$(go env GOOS)-$(go env GOARCH)/.env.example
cp -r ./assets ./bin/$(go env GOOS)-$(go env GOARCH)
go build -o ./bin/$(go env GOOS)-$(go env GOARCH)/eleutheriapay-$VERSION-$(go env GOOS)-$(go env GOARCH)
$(go env GOPATH)/bin/rice append --exec ./bin/$(go env GOOS)-$(go env GOARCH)/eleutheriapay-$VERSION-$(go env GOOS)-$(go env GOARCH)
tar -czvf ./bin/$(go env GOOS)-$(go env GOARCH).tar.gz ./bin/$(go env GOOS)-$(go env GOARCH)
# buid for linux x86 64-bit
export GOOS=linux
export GOARCH=amd64
mkdir bin/$(go env GOOS)-$(go env GOARCH)
cp .env.example ./bin/$(go env GOOS)-$(go env GOARCH)/.env.example
cp -r ./assets ./bin/$(go env GOOS)-$(go env GOARCH)
go build -o ./bin/$(go env GOOS)-$(go env GOARCH)/eleutheriapay-$VERSION-$(go env GOOS)-$(go env GOARCH)
$(go env GOPATH)/bin/rice append --exec ./bin/$(go env GOOS)-$(go env GOARCH)/eleutheriapay-$VERSION-$(go env GOOS)-$(go env GOARCH)
tar -czvf ./bin/$(go env GOOS)-$(go env GOARCH).tar.gz ./bin/$(go env GOOS)-$(go env GOARCH)

6
build-frontend.sh

@ -0,0 +1,6 @@
#!/bin/bash
# build frontend
cd ./frontend
npm install
npm run build

3
frontend/.browserslistrc

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

7
frontend/.editorconfig

@ -0,0 +1,7 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100

28
frontend/.eslintrc.js

@ -0,0 +1,28 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/essential',
'@vue/airbnb',
],
parserOptions: {
parser: 'babel-eslint',
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
jest: true,
},
},
],
};

23
frontend/.gitignore

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

29
frontend/README.md

@ -0,0 +1,29 @@
# frontend
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Run your unit tests
```
yarn test:unit
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

5
frontend/babel.config.js

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
};

3
frontend/jest.config.js

@ -0,0 +1,3 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest',
};

16585
frontend/package-lock.json

File diff suppressed because it is too large

56
frontend/package.json

@ -0,0 +1,56 @@
{
"name": "frontend",
"version": "1.2.2",
"private": true,
"description": "EleutheriaPay Frontend",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint",
"start": "vue-cli-service serve --open"
},
"dependencies": {
"@babel/polyfill": "^7.11.5",
"@stripe/stripe-js": "^1.11.0",
"@vue-stripe/vue-stripe": "^4.1.8",
"axios": "^0.21.0",
"bootstrap": "^4.5.3",
"bootstrap-vue": "^2.17.3",
"core-js": "^3.6.5",
"mutationobserver-shim": "^0.3.7",
"vue": "^2.6.11",
"vue-axios": "^2.1.4",
"vue-feather-icons": "^5.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/test-utils": "^1.0.3",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-vue": "^6.2.2",
"lint-staged": "^9.5.0",
"node-sass": "^4.12.0",
"popper.js": "^1.16.1",
"portal-vue": "^2.1.7",
"sass": "^1.26.11",
"sass-loader": "^10.0.2",
"vue-cli-plugin-bootstrap-vue": "^0.7.0",
"vue-cli-plugin-mdb": "~2.1.0",
"vue-template-compiler": "^2.6.11"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,vue}": [
"vue-cli-service lint",
"git add"
]
}
}

18
frontend/public/index.html

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="shortcut icon" href="/assets/img/favicon.png">
<link rel="stylesheet" type="text/css" href="/assets/css/styles.css">
<title>EleutheriaPay</title>
</head>
<body>
<noscript>
<strong>We're sorry but EleutheriaPay doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

270
frontend/src/App.vue

@ -0,0 +1,270 @@
<template>
<div
id="app"
>
<b-navbar
id="top-nav"
>
<b-navbar-nav>
<b-nav-item
:href="backLink"
>
<arrow-left-circle-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
</b-navbar-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item
v-if="emailLink !== ''"
:href="'mailto:' + emailLink"
>
<mail-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="gitLink !== ''"
:href="gitLink"
>
<git-pull-request-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="githubLink !== ''"
:href="githubLink"
>
<github-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="gitlabLink !== ''"
:href="gitlabLink"
>
<gitlab-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="facebookLink !== ''"
:href="facebookLink"
>
<facebook-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="twitterLink !== ''"
:href="twitterLink"
>
<twitter-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="instagramLink !== ''"
:href="instagramLink"
>
<instagram-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="linkedinLink !== ''"
:href="linkedinLink"
>
<linkedin-icon
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="theme == 'light'"
>
<moon-icon
@click="toggleTheme()"
size="1.5x"
class="navIcons"
/>
</b-nav-item>
<b-nav-item
v-if="theme == 'dark'"
>
<sun-icon
@click="toggleTheme()"
size="1.5x"
class="navIcons"
/>
</b-nav-item>
</b-navbar-nav>
</b-navbar>
<Index
v-if="configLoaded"
:title="title"
:description="description"
:defaultCurrency="defaultCurrency"
/>
</div>
</template>
<script>
import {
SunIcon,
MoonIcon,
ArrowLeftCircleIcon,
MailIcon,
GithubIcon,
GitlabIcon,
GitPullRequestIcon,
FacebookIcon,
TwitterIcon,
InstagramIcon,
LinkedinIcon,
} from 'vue-feather-icons';
import Index from './components/Index.vue';
export default {
name: 'App',
components: {
Index,
SunIcon,
MoonIcon,
ArrowLeftCircleIcon,
MailIcon,
GithubIcon,
GitlabIcon,
GitPullRequestIcon,
FacebookIcon,
TwitterIcon,
InstagramIcon,
LinkedinIcon,
},
data: () => ({
configLoaded: false,
theme: '',
metaTitle: '',
title: '',
description: '',
defaultCurrency: '',
acceptedCurrencies: [
{ value: 'usd', text: 'USD - United States Dollar' },
{ value: 'cad', text: 'CAD - Canadian Dollar' },
{ value: 'gbp', text: 'GBP - Pounds Sterling' },
{ value: 'aud', text: 'AUD - Australian Dollar' },
{ value: 'eur', text: 'EUR - Euro' },
],
enableOneTime: true,
enableMonthly: true,
enableAnnual: true,
backLink: '',
emailLink: '',
gitLink: '',
githubLink: '',
gitlabLink: '',
facebookLink: '',
twitterLink: '',
instagramLink: '',
linkedinLink: '',
}),
mounted() {
const userPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
let themePreference = 'dark';
if (userPrefersDark) {
themePreference = 'dark';
} else {
themePreference = 'light';
}
this.theme = localStorage.getItem('data-theme') || themePreference;
this.axios.get('/api/site/config').then((response) => {
// Passed to the Index component
this.title = response.data.Title;
this.description = response.data.Description;
this.defaultCurrency = response.data.DefaultCurrency;
// For this component
this.metaTitle = response.data.MetaTitle;
this.backLink = response.data.BackLink;
this.emailLink = response.data.EmailLink;
this.gitLink = response.data.GitLink;
this.githubLink = response.data.GithubLink;
this.gitlabLink = response.data.GitlabLink;
this.facebookLink = response.data.FacebookLink;
this.twitterLink = response.data.TwitterLink;
this.instagramLink = response.data.InstagramLink;
this.linkedinLink = response.data.LinkedinLink;
}).then(() => {
document.title = this.metaTitle;
this.configLoaded = true;
});
},
methods: {
toggleTheme() {
const newTheme = this.theme === 'light' ? 'dark' : 'light';
this.theme = newTheme;
},
},
watch: {
theme() {
const htmlElement = document.documentElement;
if (this.theme === 'dark') {
localStorage.setItem('data-theme', 'dark');
htmlElement.setAttribute('data-theme', 'dark');
} else {
localStorage.setItem('data-theme', 'light');
htmlElement.setAttribute('data-theme', 'light');
}
},
},
};
</script>
<style lang="scss">
#app {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
}
body {
background-color: var(--bg-color);
}
legend,label,h1,h2,h3,h4,h5,h6,p {
color: var(--text-color);
}
.navIcons {
color: var(--text-color);
}
.navIcons:hover {
filter: invert(0.5);
}
#top-nav {
border-top: 14px solid var(--top-nav-color);
}
.radio.btn-theme {
background-color: #ffffff !important;
border-color: var(--button-color) !important;
color: var(--button-color) !important;
}
.radio.btn-theme.btn-sm.active {
background-color: var(--button-color) !important;
border-color: var(--button-color) !important;
color: #ffffff !important;
}
.btn-theme {
background-color: var(--button-color) !important;
color: #ffffff !important;
}
.form-item {
width: 100%;
margin: auto;
}
</style>

218
frontend/src/components/Index.vue

@ -0,0 +1,218 @@
<template>
<b-container>
<b-alert
:show="paymentSuccess"
dismissible
fade
variant="success"
>
Payment was successful!
</b-alert>
<b-alert
:show="paymentError"
dismissible
fade
variant="danger"
>
There was an error making your payment. Please try again.
</b-alert>
<img alt="Vue logo" src="/assets/img/logo.png" width="200px">
<h2>{{ title }}</h2>
<p>{{ description }}</p>
<b-row class="h-100 align-items-center">
<b-col
xl="5"
lg="6"
md="8"
sm="10"
class="mx-auto text-center"
>
<span>
<b-form-select
v-model="currency"
:options="currencyOptions"
size="sm"
class="my-3 radio form-item"
/>
</span>
<b-form-group
id="increments"
>
<b-form-radio-group
id="incrementSelector"
v-model="amount"
buttons
button-variant="theme"
size="sm"
name="radio-btn-outline"
class="form-item"
>
<b-form-radio
class="radio"
:value="3"
>
3
</b-form-radio>
<b-form-radio
class="radio"
:value="5"
>
5
</b-form-radio>
<b-form-radio
class="radio"
:value="10"
>
10
</b-form-radio>
<b-form-radio
class="radio"
:value="20"
>
20
</b-form-radio>
</b-form-radio-group>
</b-form-group>
<b-form-input
type="number"
v-model="amount"
class="my-2 form-item"
size="sm"
/>
<b-form-input
type="text"
v-model="donationMessage"
placeholder="Donation Message"
class="my-3 form-item"
size="sm"
/>
<stripe-element-card
ref="elementRef"
v-if="paymentInitiated"
:pk="publishableKey"
:elementsOptions="elementsOptions"
@token="tokenCreated"
/>
<b-btn
v-if="paymentInitiated"
@click="submitPayment"
class="my-2 form-item"
size="sm"
variant="theme"
>
Confirm Donation
</b-btn>
<b-btn
v-else
@click="beginDonation"
class="my-2 form-item"
size="sm"
variant="theme"
>
Donate
</b-btn>
</b-col>
</b-row>
</b-container>
</template>
<script>
import { StripeElementCard } from '@vue-stripe/vue-stripe';
export default {
components: {
StripeElementCard,
},
props: {
title: String,
description: String,
defaultCurrency: String,
},
data: () => ({
amount: 0,
donationMessage: '',
publishableKey: '',
clientSecret: '',
currency: '',
elementsOptions: {
locale: 'auto',
},
paymentSuccess: false,
paymentError: false,
currencyOptions: [
{ value: 'usd', text: 'USD - United States Dollar' },
{ value: 'cad', text: 'CAD - Canadian Dollar' },
{ value: 'gbp', text: 'GBP - Pounds Sterling' },
{ value: 'aud', text: 'AUD - Australian Dollar' },
{ value: 'eur', text: 'EUR - Euro' },
],
paymentIncrementOptions: [
{ text: '3', value: 3 },
{ text: '5', value: 5 },
{ text: '10', value: 10 },
{ text: '20', value: 20 },
],
paymentType: 0,
paymentInitiated: false,
}),
mounted() {
this.amount = this.paymentIncrementOptions[1].value;
console.log(this.defaultCurrency);
/* eslint-disable */
const configCurrencyCheck = this.currencyOptions.filter((currency) => currency.value === this.defaultCurrency);
/* eslint-enable */
if (configCurrencyCheck.length > 0) {
this.currency = configCurrencyCheck[0].value;
} else {
this.currency = 'usd';
}
},
methods: {
beginDonation() {
this.axios.get('/api/pay/config').then((response) => {
this.publishableKey = response.data.StripePK;
this.paymentInitiated = true;
});
},
submitPayment() {
this.$refs.elementRef.submit();
},
tokenCreated(token) {
this.axios.post('/api/pay/secret', {
amount: this.amount * 100,
currency: this.currency,
description: this.donationMessage,
}).then((response) => {
this.clientSecret = response.data.client_secret;
}).then(() => {
this.$stripe.confirmCardPayment(this.clientSecret, {
payment_method: {
card: {
token: token.id,
},
},
}).then((result) => {
if (result.error) {
this.paymentError = 5;
this.paymentInitiated = false;
this.clientSecret = '';
this.publishableKey = '';
this.donationMessage = '';
} else if (result.paymentIntent.status === 'succeeded') {
this.paymentSuccess = 5;
this.paymentInitiated = false;
this.clientSecret = '';
this.publishableKey = '';
this.donationMessage = '';
}
});
});
},
},
};
</script>

27
frontend/src/main.js

@ -0,0 +1,27 @@
import '@babel/polyfill';
import 'mutationobserver-shim';
import '@stripe/stripe-js';
import Vue from 'vue';
import { StripePlugin } from '@vue-stripe/vue-stripe';
import axios from 'axios';
import VueAxios from 'vue-axios';
import './plugins/bootstrap-vue';
import App from './App.vue';
Vue.config.productionTip = false;
axios.get('/api/pay/config').then((response) => {
const options = {
pk: response.data.StripePK,
stripeAccount: response.data.StripeACC,
apiVersion: response.data.StripeApiVersion,
locale: response.data.StripeLocale,
};
Vue.use(StripePlugin, options);
});
Vue.use(VueAxios, axios);
new Vue({
render: (h) => h(App),
}).$mount('#app');

7
frontend/src/plugins/bootstrap-vue.js

@ -0,0 +1,7 @@
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
Vue.use(BootstrapVue);

96
frontend/tests/unit/index.spec.js

@ -0,0 +1,96 @@
import { shallowMount } from '@vue/test-utils';
import Index from '@/components/Index.vue';
import BootstrapVue from 'bootstrap-vue';
import { createLocalVue } from '@vue/test-utils'
import Vue from 'vue';
// create an extended `Vue` constructor
const localVue = createLocalVue()
// install plugins as normal
localVue.use(BootstrapVue)
describe('Index.vue', () => {
it('Index.vue - title and description render', async () => {
const title = 'Title';
const description = 'Description';
const wrapper = shallowMount(Index, {
propsData: { title, description },
localVue
});
expect(wrapper.text()).toMatch(title);
expect(wrapper.text()).toMatch(description);
expect(wrapper.find('#nonAnnualIncrements').exists()).toBeTruthy();
expect(wrapper.find('#nonAnnualIncrements').exists()).toBeTruthy();
await wrapper.setData({paymentType: 2});
expect(wrapper.vm.paymentType).toBe(2);
expect(wrapper.find('#annualIncrements').exists()).toBeTruthy();
});
it('Index.vue - payment increments work correctly', async () => {
const enableOneTime = true;
const enableMonthly = true;
const enableAnnual = true;
const wrapper = shallowMount(Index, {
propsData: { enableOneTime, enableMonthly, enableAnnual },
localVue
});
expect(wrapper.find('#nonAnnualIncrements').exists()).toBeTruthy();
expect(wrapper.find('#nonAnnualIncrements').exists()).toBeTruthy();
await wrapper.setData({paymentType: 2});
expect(wrapper.vm.paymentType).toBe(2);
expect(wrapper.find('#annualIncrements').exists()).toBeTruthy();
});
it('Index.vue - monthly payment type disabled', async () => {
const enableOneTime = true;
const enableMonthly = false;
const enableAnnual = true;
const wrapper = shallowMount(Index, {
propsData: { enableOneTime, enableMonthly, enableAnnual },
localVue
});
expect(wrapper.find('#oneTimePaymentSelector').exists()).toBeTruthy();
expect(!wrapper.find('#monthlyPaymentSelector').exists()).toBeTruthy();
expect(wrapper.find('#annualPaymentSelector').exists()).toBeTruthy();
});
it('Index.vue - annual payment type disabled', async () => {
const enableOneTime = true;
const enableMonthly = true;
const enableAnnual = false;
const wrapper = shallowMount(Index, {
propsData: { enableOneTime, enableMonthly, enableAnnual },
localVue
});
expect(wrapper.find('#oneTimePaymentSelector').exists()).toBeTruthy();
expect(wrapper.find('#monthlyPaymentSelector').exists()).toBeTruthy();
expect(!wrapper.find('#annualPaymentSelector').exists()).toBeTruthy();
});
it('Index.vue - one-time payment type only', async () => {
const enableOneTime = true;
const enableMonthly = false;
const enableAnnual = false;
const wrapper = shallowMount(Index, {
propsData: { enableOneTime, enableMonthly, enableAnnual },
localVue
});
expect(wrapper.find('#oneTimePaymentSelector').exists()).toBeTruthy();
expect(!wrapper.find('#monthlyPaymentSelector').exists()).toBeTruthy();
expect(!wrapper.find('#annualPaymentSelector').exists()).toBeTruthy();
});
it('Index.vue - all payment tupes disabled', async () => {
const enableOneTime = false;
const enableMonthly = false;
const enableAnnual = false;
const wrapper = shallowMount(Index, {
propsData: { enableOneTime, enableMonthly, enableAnnual },
localVue
});
expect(!wrapper.find('#oneTimePaymentSelector').exists()).toBeTruthy();
expect(!wrapper.find('#monthlyPaymentSelector').exists()).toBeTruthy();
expect(!wrapper.find('#annualPaymentSelector').exists()).toBeTruthy();
});
});

11555
frontend/yarn.lock

File diff suppressed because it is too large

13
go.mod

@ -0,0 +1,13 @@
module git.atrilahiji.dev/atrilahiji/eleutheriapay
go 1.16
require (
github.com/GeertJohan/go.rice v1.0.2 // indirect
github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb // indirect
github.com/campoy/svg-badge v0.0.0-20180116034456-40c38fcad9f6 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/joho/godotenv v1.3.0 // indirect
github.com/stripe/stripe-go/v72 v72.41.0 // indirect
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb // indirect
)

60
go.sum

@ -0,0 +1,60 @@
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.2 h1:PtRw+Tg3oa3HYwiDBZyvOJ8LdIyf6lAovJJtr7YOAYk=
github.com/GeertJohan/go.rice v1.0.2/go.mod h1:af5vUNlDNkCjOZeSGFgIJxDje9qdjsO6hshx0gTmZt4=
github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb h1:EVl3FJLQCzSbgBezKo/1A4ADnJ4mtJZ0RvnNzDJ44nY=
github.com/ajstarks/svgo v0.0.0-20200725142600-7a3c8b57fecb/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/aws/aws-sdk-go v1.37.29 h1:OlePDQg2idesIZKPy8egpN51RIF3DHhtREnvgNpTZhE=
github.com/aws/aws-sdk-go v1.37.29/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/campoy/svg-badge v0.0.0-20180116034456-40c38fcad9f6 h1:tqkfnr9GEZzMyWtQKvgPwWvWxH8RCMA+GeNylbYYEb0=
github.com/campoy/svg-badge v0.0.0-20180116034456-40c38fcad9f6/go.mod h1:kDWVB5nIXF847uNfomMkNoV4QEdH0q0JwMPQvgsvsLI=
github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stripe/stripe-go v70.15.0+incompatible h1:hNML7M1zx8RgtepEMlxyu/FpVPrP7KZm1gPFQquJQvM=
github.com/stripe/stripe-go v70.15.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY=
github.com/stripe/stripe-go/v71 v71.48.0 h1:xSmbjHB1fdt6ieIf9yCGggafbzbXHPIhQj+R1gxTUHM=
github.com/stripe/stripe-go/v71 v71.48.0/go.mod h1:BXYwMQe+xjYomcy5/qaTGyoyVMTP3wDCHa7DVFvg8+Y=
github.com/stripe/stripe-go/v72 v72.37.0 h1:y/PW0SeIk17S1uq6tQ0RdyeizG1anZlvowMZ4AQ17YY=
github.com/stripe/stripe-go/v72 v72.37.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0=
github.com/stripe/stripe-go/v72 v72.41.0 h1:HkyJew+GkD/ClBT306+5vKLjBE4PRCJDiZ1enQyxeGQ=
github.com/stripe/stripe-go/v72 v72.41.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

245
main.go

@ -0,0 +1,245 @@
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"bytes"
rice "github.com/GeertJohan/go.rice"
"github.com/campoy/svg-badge/badge"
"github.com/joho/godotenv"
stripe "github.com/stripe/stripe-go/v72"
"github.com/stripe/stripe-go/v72/paymentintent"
"golang.org/x/image/font/gofont/goregular"
)
func loadEnv() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
}
func stripeConfigHandler(w http.ResponseWriter, r *http.Request) {
type Config struct {
StripePK string
StripeACC string
StripeApiVersion string
StripeLocale string
}
switch r.Method {
case http.MethodGet:
config := Config{
os.Getenv("STRIPE_PK"),
os.Getenv("STRIPE_ACC"),
os.Getenv("STRIPE_API_VERSION"),
os.Getenv("STRIPE_LOCALE"),
}
// log.Fatal(configJson)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(config)
default:
http.Error(w, "This endpoint only supports GET requrests.", http.StatusMethodNotAllowed)
return
}
}
func configHandler(w http.ResponseWriter, r *http.Request) {
type Config struct {
MetaTitle string
Title string
Description string
DefaultCurrency string
BackLink string
EmailLink string
GitLink string
GithubLink string
GitlabLink string
FacebookLink string
TwitterLink string
InstagramLink string
LinkedinLink string
BGColor string
TextColor string
ButtonColor string
TopNavColor string
BGColorDark string
TextColorDark string
ButtonColorDark string
TopNavColorDark string
}
switch r.Method {
case http.MethodGet:
config := Config{
os.Getenv("META_TITLE"),
os.Getenv("TITLE"),
os.Getenv("DESCRIPTION"),
os.Getenv("DEFAULT_CURRENCY"),
os.Getenv("BACK_LINK"),
os.Getenv("EMAIL_LINK"),
os.Getenv("GIT_LINK"),
os.Getenv("GITHUB_LINK"),
os.Getenv("GITLAB_LINK"),
os.Getenv("FACEBOOK_LINK"),
os.Getenv("TWITTER_LINK"),
os.Getenv("INSTAGRAM_LINK"),
os.Getenv("LINKEDIN_LINK"),
os.Getenv("BG_COLOR"),
os.Getenv("TEXT_COLOR"),
os.Getenv("BUTTON_COLOR"),
os.Getenv("TOP_NAV_COLOR"),
os.Getenv("BG_COLOR_DARK"),
os.Getenv("TEXT_COLOR_DARK"),
os.Getenv("BUTTON_COLOR_DARK"),
os.Getenv("TOP_NAV_COLOR_DARK"),
}
// log.Fatal(configJson)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(config)
default:
http.Error(w, "This endpoint only supports GET requrests.", http.StatusMethodNotAllowed)
return
}
}
func sendOwncastNotification(message string) {
type ReqBody struct {
Body string `json:"body"`
}
if os.Getenv("OWNCAST_HOSTNAME") != "" {
reqUrl := "https://" + os.Getenv("OWNCAST_HOSTNAME") + "/api/integrations/chat/system"
// Create a Bearer string by appending string access token
var bearer = "Bearer " + os.Getenv("OWNCAST_TOKEN")
ownCastBody := &ReqBody{
Body: message,
}
payloadBuf := new(bytes.Buffer)
json.NewEncoder(payloadBuf).Encode(ownCastBody)
// Create a new request using http
req, err := http.NewRequest(http.MethodPost, reqUrl, payloadBuf)
// add authorization header to the req
req.Header.Add("Authorization", bearer)
req.Header.Set("Content-Type", "application/json")
// Send req using http Client
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Println("Error on response.\n[ERROR] -", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Error while reading the response bytes:", err)
}
log.Println(string([]byte(body)))
log.Println(resp)
}
}
func getSecretHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
type LineItems struct {
Amount int64
Currency string
Description string
}
type CheckoutData struct {
ClientSecret string `json:"client_secret"`
}
type Response struct {
SessionID string
StripePK string
}
var items LineItems
_ = json.NewDecoder(r.Body).Decode(&items)
params := &stripe.PaymentIntentParams{
Amount: stripe.Int64(items.Amount),
Currency: stripe.String(items.Currency),
Description: stripe.String(items.Description),
}
intent, _ := paymentintent.New(params)
data := CheckoutData{
ClientSecret: intent.ClientSecret,
}
sendOwncastNotification("Donation of " + strconv.FormatInt(items.Amount / 100, 10) + items.Currency + ". Message: " + items.Description)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(data)
default:
http.Error(w, "This endpoint only supports POST requrests.", http.StatusMethodNotAllowed)
return
}
}
func badgeHandler(w http.ResponseWriter, r *http.Request) {
m, err := badge.NewMaker("Go Regular", goregular.TTF)
if err != nil {
log.Fatal(err)
}
badgeFontSize, err := strconv.ParseFloat(os.Getenv("BADGE_FONT_SIZE"), 32)
badgeHeight, err := strconv.Atoi(os.Getenv("BADGE_HEIGHT"))
b, err := m.New("EL", os.Getenv("BADGE_TEXT"), os.Getenv("BADGE_COLOR"), badgeFontSize, badgeHeight)
if err != nil {
log.Fatal(err)
}
w.Header().Set("Content-Type", "image/svg+xml; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(b)
}
func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Health Check"))
}
func main() {
loadEnv()
stripe.Key = os.Getenv("STRIPE_SK")
assets := http.FileServer(http.Dir("./"))
http.Handle("/", http.FileServer(rice.MustFindBox("frontend/dist").HTTPBox()))
http.Handle("/assets/", assets)
http.HandleFunc("/api/site/config", configHandler)
http.HandleFunc("/api/pay/config", stripeConfigHandler)
http.HandleFunc("/api/pay/secret", getSecretHandler)
http.HandleFunc("/api/badge", badgeHandler)
http.HandleFunc("/api/healthcheck", healthCheckHandler)
log.Println("Listening on :3000...")
err := http.ListenAndServe(":3000", nil)
if err != nil {
log.Fatal(err)
}
}
Loading…
Cancel
Save