├── Procfile ├── .github ├── FUNDING.yml └── workflows │ ├── backend.yml │ └── frontend.yml ├── backend ├── src │ ├── types │ │ ├── api.d.ts │ │ ├── Tokens.d.ts │ │ ├── Donations.d.ts │ │ ├── main.d.ts │ │ ├── socket.d.ts │ │ ├── database.d.ts │ │ ├── donationalerts.d.ts │ │ ├── yoomoneydata.d.ts │ │ ├── global.d.ts │ │ └── qiwi.d.ts │ ├── Modeles │ │ ├── Tokens.ts │ │ └── Donations.ts │ ├── routers.ts │ ├── index.ts │ └── Servies │ │ ├── qiwi.ts │ │ ├── yoomoney.ts │ │ └── oauth2.ts ├── views │ └── index.ejs ├── .eslintrc.js ├── index.ts ├── .env.example ├── package.json ├── tsconfig.json └── README.md ├── frontend ├── config.json ├── src │ ├── App.vue │ ├── views │ │ ├── 404.vue │ │ └── Home.vue │ ├── main.js │ ├── router │ │ └── index.js │ ├── components │ │ └── Footer.vue │ └── css │ │ └── index.scss ├── public │ ├── heart.png │ ├── IgraSans.otf │ ├── favicon.png │ ├── gradient.png │ ├── font │ │ ├── Paket Web.woff │ │ ├── Paket Web.woff2 │ │ ├── Paket Bold Web.woff │ │ └── Paket Bold Web.woff2 │ ├── io.svg │ ├── index.html │ ├── aem_logo.svg │ └── wrapped.svg ├── babel.config.js ├── vue.config.js ├── .gitignore ├── README.md └── package.json ├── .gitignore ├── package.json ├── README.md ├── LICENSE └── app.json /Procfile: -------------------------------------------------------------------------------- 1 | worker: npm run startandbuild 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://donate.mrlivixx.me'] 2 | -------------------------------------------------------------------------------- /backend/src/types/api.d.ts: -------------------------------------------------------------------------------- 1 | interface Api { 2 | ip: string; 3 | port: number 4 | } -------------------------------------------------------------------------------- /frontend/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "apidonate.mrlivixx.me", 3 | "page_title": "MrLivixx" 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/public/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EveryDayRains/Donate-page/HEAD/frontend/public/heart.png -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/public/IgraSans.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EveryDayRains/Donate-page/HEAD/frontend/public/IgraSans.otf -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EveryDayRains/Donate-page/HEAD/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EveryDayRains/Donate-page/HEAD/frontend/public/gradient.png -------------------------------------------------------------------------------- /backend/src/types/Tokens.d.ts: -------------------------------------------------------------------------------- 1 | interface Tokens { 2 | userid: string; 3 | accessToken: string; 4 | exp: number; 5 | } -------------------------------------------------------------------------------- /frontend/public/font/Paket Web.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EveryDayRains/Donate-page/HEAD/frontend/public/font/Paket Web.woff -------------------------------------------------------------------------------- /frontend/public/font/Paket Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EveryDayRains/Donate-page/HEAD/frontend/public/font/Paket Web.woff2 -------------------------------------------------------------------------------- /frontend/public/font/Paket Bold Web.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EveryDayRains/Donate-page/HEAD/frontend/public/font/Paket Bold Web.woff -------------------------------------------------------------------------------- /frontend/public/font/Paket Bold Web.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EveryDayRains/Donate-page/HEAD/frontend/public/font/Paket Bold Web.woff2 -------------------------------------------------------------------------------- /backend/src/types/Donations.d.ts: -------------------------------------------------------------------------------- 1 | interface Donations { 2 | id: string; 3 | username: string; 4 | money: string; 5 | comment: string; 6 | time: string; 7 | } -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: [ 3 | 'vuetify' 4 | ], 5 | productionSourceMap: false, 6 | devServer: { 7 | disableHostCheck: true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/types/main.d.ts: -------------------------------------------------------------------------------- 1 | interface ProcessEnv { 2 | DB_URL: string; 3 | PORT: number; 4 | CLIENT_ID: string; 5 | REDIRECT_URL: string; 6 | SECRET: string; 7 | CLIENT_SECRET: string; 8 | } -------------------------------------------------------------------------------- /backend/src/types/socket.d.ts: -------------------------------------------------------------------------------- 1 | declare var io : { 2 | connect(url: string): Socket; 3 | }; 4 | interface Socket { 5 | on(event: string, callback: (data: any) => void ); 6 | emit(event: string, data: any); 7 | send(data: any); 8 | } -------------------------------------------------------------------------------- /backend/src/types/database.d.ts: -------------------------------------------------------------------------------- 1 | interface Tokens { 2 | userid: string; 3 | accessToken: string; 4 | } 5 | interface Donations { 6 | id: string; 7 | username: string; 8 | money: string; 9 | comment: string; 10 | time: string; 11 | } -------------------------------------------------------------------------------- /frontend/src/views/404.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /backend/src/Modeles/Tokens.ts: -------------------------------------------------------------------------------- 1 | import { model,Schema } from 'mongoose'; 2 | class Tokens extends Schema { 3 | constructor() { 4 | super({ 5 | userid: String, 6 | accessToken: String, 7 | exp: Number 8 | }); 9 | } 10 | } 11 | export default model('tokens', new Tokens()); -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config() 5 | 6 | import router from './router'; 7 | 8 | Vue.config.productionTip = false; 9 | import './css/index.scss'; 10 | 11 | new Vue({ 12 | router, 13 | render: (h) => h(App) 14 | }).$mount('#app'); -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /backend/src/Modeles/Donations.ts: -------------------------------------------------------------------------------- 1 | import { model,Schema } from 'mongoose'; 2 | class Dontations extends Schema { 3 | constructor() { 4 | super({ 5 | id: String, 6 | username: String, 7 | money: String, 8 | comment: String, 9 | time: Number 10 | }); 11 | } 12 | } 13 | export default model('donations', new Dontations()); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | package-lock.json 5 | .env 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | backend/dist 26 | frontend/dist -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Donate-page", 3 | "version": "1.0.0", 4 | "author": "MrLivixx", 5 | "license": "MIT", 6 | "engines": { 7 | "node": "16.x" 8 | }, 9 | "scripts": { 10 | "build": "cd backend && npm i && npx tsc --build tsconfig.json", 11 | "start": "node ./backend/dist/index" 12 | }, 13 | "dependencies": { 14 | "fastify-formbody": "^5.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /backend/index.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register.js' 2 | import Dotenv from 'dotenv'; 3 | import ApiWorker from "@/index"; 4 | Dotenv.config(); 5 | const port = process.env.PORT; 6 | new ApiWorker({port, ip: '0.0.0.0'}).start(); 7 | if(!process.env.QIWI_SECRET_KEY) console.log('Qiwi secret key isn\'t provided, Qiwi service disabled'); 8 | if(!process.env.YOOMONEY_NUMBER) console.log('Yoomoney secret key isn\'t provided, Yoomoney service disabled'); 9 | process.on('unhandledRejection', console.log); 10 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | Vue.use(VueRouter) 5 | 6 | const routes = [ 7 | { 8 | path: '/', 9 | name: 'Home', 10 | component: () => import(/* webpackChunkName: "home" */ '../views/Home') 11 | }, 12 | { 13 | path: '*', 14 | name: '404', 15 | component: () => import(/* webpackChunkName: "404" */ '../views/404.vue') 16 | } 17 | ] 18 | 19 | const router = new VueRouter({ 20 | routes, 21 | mode: 'history' 22 | }) 23 | 24 | export default router 25 | -------------------------------------------------------------------------------- /backend/src/types/donationalerts.d.ts: -------------------------------------------------------------------------------- 1 | interface Donationalerts { 2 | id: number; 3 | alert_type: string; 4 | is_shown: string; 5 | additional_data: string; 6 | billing_system: string; 7 | billing_system_type: null, 8 | username: string; 9 | amount: string; 10 | amount_formatted: string; 11 | amount_main: number; 12 | currency: string; 13 | message: string; 14 | header: string; 15 | date_created: string; 16 | emotes: any, 17 | ap_id: any, 18 | _is_test_alert: boolean; 19 | message_type: string; 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | name: Backend tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | run-tests: 13 | name: tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out Git repository 17 | uses: actions/checkout@v2 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 13 22 | - name: Install dependencies for backend 23 | run: cd backend && npm i 24 | - name: Run test 25 | run: cd backend && npm run build -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | DB_URL=mongodb+srv://****/donatepage 2 | PORT=1045 3 | CORS_URL=http://localhost:8080 4 | 5 | #DISCORD OAUTH 6 | DISCORD_CLIENT_ID=ID 7 | DISCORD_REDIRECT_URL=https://api.donate.mrlivixx.me/oauth2/discord/callback 8 | DISCORD_CLIENT_SECRET=**** 9 | 10 | #VK OAUTH 11 | VK_CLIENT_ID=ID 12 | VK_CLIENT_SECRET=**** 13 | VK_REDIRECT_URL=https://api.donate.mrlivixx.me/oauth2/vk/callback 14 | VK_API_KEY=fghjlwe.gdhyh 15 | #JWT 16 | 17 | JWT_SECRET=HASH_KEY 18 | 19 | # PAYMENTS 20 | #Yoomoney api key 21 | YOOMONEY_NUMBER=410013706619445 22 | YOOMONEY_CALLBACK_SECRET= 23 | #QIWI SECRET p2p KEY 24 | QIWI_SECRET_KEY=****** 25 | #QIWI THEME 26 | QIWI_THEME=Nykyta-S0FqLeU_kv 27 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: Frontend tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | run-tests: 13 | name: tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out Git repository 17 | uses: actions/checkout@v2 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 13 22 | - name: Install dependencies for frontend 23 | run: cd frontend && npm i 24 | - name: Run list 25 | run: cd frontend && npm run lint 26 | - name: Run build 27 | run: cd frontend && npm run build -------------------------------------------------------------------------------- /backend/src/types/yoomoneydata.d.ts: -------------------------------------------------------------------------------- 1 | interface Yoomoneydata { 2 | notification_type: string; 3 | operation_id: string; 4 | amount: string; 5 | withdraw_amount: string; 6 | currency: string; 7 | datetime: string; 8 | sender: string; 9 | codepro: string; 10 | label: string; 11 | sha1_hash: string; 12 | test_notification: boolean; 13 | unaccepted: boolean; 14 | notification_secret: string; 15 | lastname: string; 16 | firstname: string; 17 | fathersname: string; 18 | email: string; 19 | phone: string; 20 | city: string; 21 | street: string; 22 | building: string; 23 | suite: string; 24 | flat: string; 25 | zip: string; 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | DB_URL: string; 4 | PORT: number; 5 | CORS_URL: string; 6 | DISCORD_CLIENT_ID: string | undefined; 7 | DISCORD_REDIRECT_URL: string | undefined; 8 | DISCORD_CLIENT_SECRET: string | undefined; 9 | VK_CLIENT_ID: number | undefined; 10 | VK_CLIENT_SECRET: string | undefined; 11 | VK_REDIRECT_URL: string | undefined; 12 | VK_API_KEY: string | undefined; 13 | YOOMONEY_NUMBER: number; 14 | YOOMONEY_CALLBACK_SECRET: string; 15 | DA_SECRET: string | undefined; 16 | QIWI_SECRET_KEY: string | undefined; 17 | QIWI_THEME: string | undefined; 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Donate-page 2 | Крутая донат-страница с интеграцией DonationAlerts, Qiwi и авторизацией через Discord/VK 3 | ![](https://imgs.mrlivixx.me/opera_E34ICHnGrP.png) 4 | ## Зачем она нужна? 5 | Ваша таблица донаторов которая автоматически обновляется. 6 | 7 | ## Установка 8 | [Как установить Front-end часть](https://github.com/MrLivixx/Donate-page/tree/main/frontend) 9 |
[Как установить Back-end часть](https://github.com/MrLivixx/Donate-page/tree/main/backend) 10 | 11 | ## Зависимости проекта 12 | `vue` - Js фреймворк для Front-end. 13 |
`socket.io` - Обмен данными между Front-end и Back-end частями сайта. 14 |
`fastify` - Используется в качестве веб-сервера в Back-end части. 15 |
`mongoose` - Для использования базы данных MongoBD, хранения донатов и сессий 16 |
`jsonwebtoken` - Используется для подписи токенов, [подробнее](https://jwt.io) 17 | # Contributing 18 | Мы приветствуем всех желающих которые хотят внести вклад в данный проект. 19 | -------------------------------------------------------------------------------- /frontend/public/io.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 MrLivixx & Egor_m 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. -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # donate-page 2 | Установка front-end части проекта. 3 | 4 | ### Установка на VDS/VPS машину 5 | - Для установки front-end части на VDS/VPS машину, выполните ряд действий: 6 | 1. Склонируйте и установите зависимости проекта. 7 | ```shell 8 | $ git clone https://github.com/MrLivixx/Donate-page.git 9 | $ npm i 10 | ``` 11 | 2. Измените файл config.json подставив свои значения 12 | ```json 13 | { 14 | "url": "localhost:8045", 15 | "dalink": "https://www.donationalerts.com/r/livixx", 16 | "page_title": "MrLivixx" 17 | } 18 | ``` 19 | Где: 20 | - `url` - адрес бекенд сервера и вебсокета 21 | - `dalink` - ваша ссылка на DonationAlerts 22 | - `page_title` - заголовок страницы 23 | 24 | Затем, соберите проект 25 | ```shell 26 | npm run build 27 | ``` 28 | Пример конфигурации Nginx 29 | ```conf 30 | server { 31 | server_name donate.mrlivixx.me; 32 | listen 80; 33 | 34 | location / { 35 | root /var/www/www-root/data/www/donate/dist; 36 | index index.php index.html; 37 | } 38 | } 39 | ``` 40 | ### Отдельное спасибо 41 | egor_m за помощь с frontend-частью и дизайном 42 |
art egor_m -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donate-page", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "dotenv": "^8.6.0", 13 | "moment": "^2.29.1", 14 | "sass": "^1.32.0", 15 | "sass-loader": "^10.0.0", 16 | "socket.io-client": "^4.1.2", 17 | "vue": "^2.6.11", 18 | "vue-router": "^3.2.0" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "~4.5.0", 22 | "@vue/cli-plugin-eslint": "~4.5.0", 23 | "@vue/cli-plugin-router": "~4.5.0", 24 | "@vue/cli-service": "~4.5.0", 25 | "babel-eslint": "^10.1.0", 26 | "eslint": "^6.7.2", 27 | "eslint-plugin-vue": "^6.2.2", 28 | "sass": "^1.32.0", 29 | "sass-loader": "^10.0.0", 30 | "vue-template-compiler": "^2.6.11" 31 | }, 32 | "eslintConfig": { 33 | "root": true, 34 | "env": { 35 | "node": true 36 | }, 37 | "extends": [ 38 | "plugin:vue/essential", 39 | "eslint:recommended" 40 | ], 41 | "parserOptions": { 42 | "parser": "babel-eslint" 43 | }, 44 | "rules": {} 45 | }, 46 | "browserslist": [ 47 | "> 1%", 48 | "last 2 versions", 49 | "not dead" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donate-page-backend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "build-dev": "tsc --build tsconfig.json --w", 8 | "build": "tsc --build tsconfig.json", 9 | "dev": "nodemon ./dist/index", 10 | "start": "node ./dist/index", 11 | "startandbuild": "npm i && tsc --build tsconfig.json && node ./dist/index" 12 | }, 13 | "_moduleAliases": { 14 | "@": "dist/src" 15 | }, 16 | "dependencies": { 17 | "@qiwi/bill-payments-node-js-sdk": "^3.2.1", 18 | "@types/socket.io": "^3.0.2", 19 | "axios": "^0.21.1", 20 | "cors": "^2.8.5", 21 | "dotenv": "^8.6.0", 22 | "ejs": "^2.7.4", 23 | "fastify": "^3.15.1", 24 | "fastify-cors": "^6.0.1", 25 | "fastify-rate-limit": "^5.5.0", 26 | "fastify-formbody": "^5.1.0", 27 | "jsonwebtoken": "^8.5.1", 28 | "module-alias": "^2.2.2", 29 | "mongoose": "^5.12.9", 30 | "node-fetch": "^2.6.1", 31 | "point-of-view": "^4.14.0", 32 | "socket.io": "^4.1.1", 33 | "typescript": "^4.2.4" 34 | }, 35 | "devDependencies": { 36 | "@types/ejs": "^3.1.0", 37 | "@types/node-fetch": "^2.5.10", 38 | "@types/socket.io-client": "^3.0.0", 39 | "@typescript-eslint/eslint-plugin": "^4.24.0", 40 | "@typescript-eslint/parser": "^4.24.0", 41 | "eslint": "^7.26.0", 42 | "nodemon": "^2.0.7" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <%= htmlWebpackPlugin.options.title %> 25 | 26 | 27 | 28 | 29 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donate-page-backend", 3 | "description": "backend часть сайта", 4 | "keywords": [ 5 | "typescript", 6 | "ts", 7 | "nodejs", 8 | "socket.io", 9 | "mongodb" 10 | ], 11 | "repository": "https://github.com/MrLivixx/Donate-page/blob/main/backend", 12 | "env": { 13 | "DB_URL": { 14 | "description": "Ссылка на базу данных mongodb", 15 | "value": "db url" 16 | }, 17 | "DISCORD_CLIENT_ID": { 18 | "description": "CLIENT ID вашего Discord приложения", 19 | "value": "client id" 20 | }, 21 | "DISCORD_REDIRECT_URL": { 22 | "description": "Ссылка после удачной авторизации", 23 | "value": "https://api.donate.mrlivixx.me/oauth2/discord/callback" 24 | }, 25 | "DISCORD_CLIENT_SECRET": { 26 | "description": "Client secret вашего приложения", 27 | "value": "asgdgedred" 28 | }, 29 | "VK_CLIENT_ID": { 30 | "description": "ID вашего приложение в ВКонтакте", 31 | "value": "7873175" 32 | }, 33 | "VK_CLIENT_SECRET": { 34 | "description": "Защищённый ключ приложения", 35 | "value": "O7EVlvvJJOGZHoWdIiym" 36 | }, 37 | "VK_REDIRECT_URL": { 38 | "description": "Ссылка после удачной авторизации", 39 | "value": "https://api.donate.mrlivixx.me/oauth2/vk/callback" 40 | }, 41 | "VK_API_KEY": { 42 | "description": "Сервисный ключ доступа вашего приложения", 43 | "value": "d70304c7d70304c7d70304c745d77b2650dd703d70304c7b7bfbc98bf6d58ea6b2e9873" 44 | }, 45 | "JWT_SECRET": { 46 | "description": "Случайное значение с https://randomkeygen.com/ для работы авторизации", 47 | "value": "CHANGE THIS!" 48 | }, 49 | "DA_SECRET": { 50 | "description": "Секретный ключ с DonationAlerts", 51 | "value": "342f423" 52 | }, 53 | "DA_SECRET": { 54 | "description": "Код темы QIWI", 55 | "value": "Nykyta-S0FqLeU_kv" 56 | } 57 | }, 58 | "buildpacks": [ 59 | { 60 | "url": "heroku/nodejs" 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /frontend/public/aem_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 11 | 12 | 15 | 21 | 24 | 26 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /backend/src/types/qiwi.d.ts: -------------------------------------------------------------------------------- 1 | //types from https://github.com/mirdukkk/bill-payments-node-js-sdk 2 | declare module '@qiwi/bill-payments-node-js-sdk' { 3 | 4 | type RequestBuilderArguments = { 5 | url: string, 6 | method: string, 7 | body?: Record, 8 | } 9 | 10 | export type CreatePublicFormArguments = { 11 | billId: string | number, 12 | publicKey: string, 13 | amount: string | number, 14 | successUrl: string, 15 | } 16 | 17 | export type CreateBillArguments = { 18 | amount: string | number, 19 | currency: string, 20 | comment?: string, 21 | expirationDateTime: string, 22 | customFields?: Record, 23 | phone?: string, 24 | email?: string, 25 | account?: string, 26 | successUrl?: string, 27 | } 28 | 29 | export default class QiwiBillPaymentsAPI { 30 | private _key: string 31 | 32 | public constructor (key: string) 33 | 34 | public set key (key: string) 35 | 36 | private _requestBuilder ({ url, method, body }: RequestBuilderArguments): Promise> 37 | private _normalizeAmount (amount?: number | string): string 38 | 39 | public checkNotificationSignature( 40 | signature: string, 41 | notificationBody: any, 42 | merchantSecret: string 43 | ): boolean 44 | 45 | public getLifetimeByDay (days?: number): string 46 | 47 | public normalizeDate (date: Date): string 48 | 49 | public generateId (): string 50 | 51 | public createPaymentForm (params: CreatePublicFormArguments): Promise> 52 | 53 | public createBill(billId: string | number, params: { successUrl?: string; amount: number; expirationDateTime: string; customFields: { themeCode: string | undefined; comment: string }; currency: string; comment: string; account: string | number }): Promise> 54 | 55 | public getBillInfo (billId: string | number): Promise> 56 | 57 | public cancelBill (billId: string | number): Promise> 58 | 59 | public getRefundInfo (billId: string | number): Promise> 60 | 61 | public refund ( 62 | billId: string | number, 63 | refundId: string | number, 64 | amount?: number, 65 | currency?: string 66 | ): Promise> 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/routers.ts: -------------------------------------------------------------------------------- 1 | import Oauth2 from '@/Servies/oauth2'; 2 | import Qiwi from '@/Servies/qiwi'; 3 | import Yoomoney from '@/Servies/yoomoney'; 4 | import Donations from "@/Modeles/Donations"; 5 | import Tokens from "@/Modeles/Tokens"; 6 | import { FastifyReply, FastifyRequest } from "fastify"; 7 | export = [ 8 | { 9 | method: 'GET', 10 | path: '/', 11 | handler: (req: FastifyRequest, res:FastifyReply) => { 12 | res.code(200).send( 13 | { statusCode: 200, message: 'API works! Don\'t worry!' }) 14 | } 15 | }, 16 | { 17 | method: 'GET', 18 | path: '/oauth2/discord/callback', 19 | handler: (req: FastifyRequest,res: FastifyReply) => { 20 | new Oauth2(Tokens)?.Discordlogin(req,res) 21 | }, 22 | }, 23 | { 24 | method: 'GET', 25 | path: '/oauth2/vk/callback', 26 | handler: (req: FastifyRequest,res: FastifyReply) => { 27 | new Oauth2(Tokens)?.VKAuthorization(req,res) 28 | }, 29 | }, 30 | { 31 | method: 'GET', 32 | path: '/oauth2/vk/authorize', 33 | handler: (req: FastifyRequest,res: FastifyReply) => { 34 | res.redirect(encodeURI(`https://oauth.vk.com/authorize?client_id=${process.env.VK_CLIENT_ID}&redirect_uri=${process.env.VK_REDIRECT_URL}&display=page&response_type=code`)) 35 | }, 36 | }, 37 | { 38 | method: 'GET', 39 | path: '/oauth2/discord/authorize', 40 | handler: (req: FastifyRequest,res: FastifyReply) => { 41 | res.redirect(encodeURI(`https://discord.com/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&redirect_uri=${process.env.DISCORD_REDIRECT_URL}&response_type=code&scope=identify`)) 42 | }, 43 | }, 44 | { 45 | method: 'GET', 46 | path: '/oauth2/user', 47 | handler: (req: FastifyRequest, res: FastifyReply) => { 48 | new Oauth2(Tokens)?.getUser(req,res) 49 | } 50 | }, 51 | { 52 | method: 'POST', 53 | path: '/qiwi/callback', 54 | handler: (req: FastifyRequest, res: FastifyReply) => { 55 | new Qiwi(Donations, Tokens)?.receivePayment(req,res) 56 | } 57 | }, 58 | { 59 | method: 'POST', 60 | path: '/qiwi/create', 61 | handler: (req: FastifyRequest, res: FastifyReply) => { 62 | new Qiwi(Donations, Tokens)?.createBill(req,res) 63 | } 64 | }, 65 | { 66 | method: 'POST', 67 | path: '/yoomoney/create', 68 | handler: (req: FastifyRequest, res: FastifyReply) => { 69 | new Yoomoney(Donations, Tokens)?.createhash(req,res) 70 | } 71 | }, 72 | { 73 | method: 'POST', 74 | path: '/yoomoney/callback', 75 | handler: (req: FastifyRequest, res: FastifyReply) => { 76 | new Yoomoney(Donations, Tokens)?.RecivePayments(req,res) 77 | } 78 | } 79 | ] 80 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 18 | 95 | 96 | 99 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import fastify, { 2 | FastifyError, 3 | FastifyInstance, 4 | FastifyRequest, 5 | FastifyReply, 6 | RouteOptions 7 | } from 'fastify'; 8 | import fastifyRateLimit from "fastify-rate-limit"; 9 | import donations from '@/Modeles/Donations'; 10 | import mongoose, { Model } from 'mongoose'; 11 | import pointOfView from "point-of-view"; 12 | //@ts-ignore 13 | import formBodyPlugin from 'fastify-formbody'; 14 | //@ts-ignore 15 | import fastifyCors from "fastify-cors"; 16 | import io, { Server } from 'socket.io'; 17 | import tokens from '@/Modeles/Tokens'; 18 | import routers from '@/routers'; 19 | import path from 'path'; 20 | //@ts-ignore 21 | import ejs from 'ejs'; 22 | class ApiWorker { 23 | 24 | private readonly port: number; 25 | private readonly ip: string; 26 | private app: FastifyInstance; 27 | private io: Server; 28 | 29 | constructor(data: Api) { 30 | this.app = fastify(); 31 | this.port = data.port; 32 | this.ip = data.ip; 33 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 34 | //@ts-ignore 35 | this.io = io(this.app.server,{ 36 | cors: { 37 | origin: process.env.CORS_URL, 38 | methods: ["GET", "POST"], 39 | credentials: true 40 | } 41 | }); 42 | } 43 | async start(): Promise { 44 | 45 | //MONGODB 46 | mongoose.connect(process.env.DB_URL,{ 47 | useNewUrlParser: true, 48 | useCreateIndex: true, 49 | useUnifiedTopology: true 50 | }) 51 | mongoose.connection.on('connected',()=> { 52 | console.log('Success connect to MongoDB!'); 53 | }); 54 | 55 | //SOCKET.IO 56 | this.io.on('connection', async (socket: Socket) => { 57 | socket.send('Connected!'); 58 | console.log('Site connected!'); 59 | socket.on('donations',async () => { 60 | //@ts-ignore 61 | socket.emit('donations', await donations.find().sort({'time': 'desc'}).then((x: Model) => x)); 62 | }); 63 | }); 64 | 65 | //API 66 | this.app.register(fastifyRateLimit, { 67 | max: 100, 68 | timeWindow: 3 * 60 * 1000, 69 | cache: 5000, 70 | addHeaders: { 71 | 'x-ratelimit-limit': true, 72 | 'x-ratelimit-reset': true, 73 | 'x-ratelimit-remaining': true, 74 | 'retry-after': true, 75 | } 76 | }); 77 | this.app.register(formBodyPlugin) 78 | 79 | this.app.register(fastifyCors, { origin: process.env.CORS_URL }) 80 | 81 | this.app.register(pointOfView, { 82 | engine: { 83 | ejs 84 | }, 85 | root: path.join(__dirname,'../../views') 86 | }) 87 | this.app.setErrorHandler((error: FastifyError, request: FastifyRequest, response: FastifyReply) => { 88 | switch (response.statusCode) { 89 | case 421: 90 | response.send({ 91 | error: 'От вас поступает слишком много запросов, повторите попытку позже', code: 421 92 | }) 93 | break 94 | } 95 | }); 96 | this.app.setNotFoundHandler(function(req: FastifyRequest, res: FastifyReply) { 97 | res.status(404).send({code: 404}); 98 | }) 99 | 100 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 101 | // @ts-ignore 102 | routers.forEach((x: RouteOptions) => this.app.route(x)) 103 | 104 | this.app.listen(this.port, this.ip, async (err: Error) => { 105 | if (err) throw err; 106 | console.log(this.app.printRoutes()) 107 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 108 | //@ts-ignore 109 | console.log(`Server listening on ${await this.app.server.address()?.port}`) 110 | }); 111 | 112 | 113 | } 114 | } 115 | export default ApiWorker; 116 | -------------------------------------------------------------------------------- /backend/src/Servies/qiwi.ts: -------------------------------------------------------------------------------- 1 | import QiwiBillPaymentsAPI from "@qiwi/bill-payments-node-js-sdk"; 2 | import { FastifyRequest, FastifyReply } from "fastify"; 3 | import axios, { AxiosResponse } from "axios"; 4 | import { Model } from "mongoose"; 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | //@ts-ignore 7 | import jwt from "jsonwebtoken"; 8 | 9 | class Qiwi { 10 | private qiwi: QiwiBillPaymentsAPI; 11 | private db: Model; 12 | private tokens: Model; 13 | constructor(db: Model, tokens: Model) { 14 | this.qiwi = new QiwiBillPaymentsAPI(process.env.QIWI_SECRET_KEY!); 15 | this.db = db; 16 | this.tokens = tokens; 17 | } 18 | async receivePayment(req: FastifyRequest, res: FastifyReply) : Promise { 19 | if(!process.env.QIWI_SECRET_KEY) return res.status(503).send({code: 503, message: "Service Unavailable"}) 20 | if(!req.headers['x-api-signature-sha256']) return res.status(401).send({code: 401, message:"Unauthorized"}); 21 | const status = this.qiwi.checkNotificationSignature(req.headers['x-api-signature-sha256'].toString(), req.body, process.env.QIWI_SECRET_KEY!) 22 | if(status) { 23 | const { customer, billId, amount, customFields } = JSON.parse(JSON.stringify(req.body)).bill; 24 | await this.db.create({ 25 | id: billId, 26 | username: customer.account, 27 | money: amount.value + " " + amount.currency, 28 | comment: customFields.comment, 29 | time: Date.now() 30 | }); 31 | console.log(`Новый донат от ${customer.account}! Через Qiwi`); 32 | res.status(200).send({code: 200, message: "OK"}) 33 | } else { 34 | res.status(403) 35 | } 36 | } 37 | async createBill(req: FastifyRequest, res: FastifyReply) : Promise { 38 | if(!process.env.QIWI_SECRET_KEY) return res.status(503).send({code: 503, message: "Service Unavailable"}) 39 | if(!req.headers.authorization) return res.status(401).send({code: 401, message:"Unauthorized"}); 40 | jwt.verify(req.headers.authorization,process.env.JWT_SECRET, async (err: Error, data: Record) => { 41 | if (err) return res.code(404).send({code: 404, message: "Not found"}); 42 | else { 43 | if(req.body == null) return res.status(400).send({code: 400, message: 'Bad request'}) 44 | let { comment, amount } = JSON.parse(JSON.stringify(req.body)); 45 | amount = Number(amount) 46 | if(!comment || !amount) return res.status(400).send({code: 400, message: 'Bad request'}); 47 | if(amount < 10) return res.status(400).send({code: 400, message: 'Bad request'}); 48 | const createbill = (amount: number, comment: string, data: string | number) => { 49 | this.qiwi.createBill(this.qiwi.generateId(), { 50 | amount, 51 | currency: 'RUB', 52 | comment: `Пожертвование для MrLivixx. Комментарий ${comment}`, 53 | expirationDateTime: this.qiwi.getLifetimeByDay(1), 54 | account: data, 55 | customFields: {comment, themeCode: process.env.QIWI_THEME}, 56 | successUrl: process.env.CORS_URL 57 | }).then(data => { 58 | res.status(200).send({payUrl: data.payUrl}); 59 | console.log(data) 60 | }).catch((e: Error) => { 61 | console.log(e.stack); 62 | res.status(500).send({code: 500, message: "Internal server error"}) 63 | }); 64 | } 65 | switch (data.type.toString()) { 66 | case 'discord': { 67 | const tokendata = await this.tokens.findOne({userid: data?.id.toString(), exp: data?.exp}) 68 | const response: AxiosResponse = await axios.get('https://discord.com/api/users/@me',{ 69 | headers: { 70 | //@ts-ignore 71 | authorization: `Bearer ${tokendata?.accessToken}` 72 | } 73 | }).catch(() => {return res.status(403).send({code: 403, message: 'Forbidden'})}); 74 | if(!response?.data?.id) return res.status(403).send({code: 403, message: 'Forbidden'}) 75 | createbill(amount, comment, `${response?.data?.username}#${response?.data?.discriminator}`) 76 | } 77 | break; 78 | case 'vk': { 79 | const response: AxiosResponse = 80 | await axios.get(`https://api.vk.com/method/users.get?user_ids=${Number(data.id)}&v=5.131&fields=photo_400_orig&access_token=${process.env.VK_API_KEY}&lang=ru`) 81 | .catch(() => { 82 | return res.status(500).send({code: 500, message: "Internal server error"}) 83 | }); 84 | createbill(amount, comment, `${response?.data?.response[0]?.first_name} ${response?.data?.response[0]?.last_name}`) 85 | } 86 | } 87 | 88 | } 89 | }) 90 | } 91 | } 92 | export default Qiwi; 93 | -------------------------------------------------------------------------------- /backend/src/Servies/yoomoney.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import jwt from "jsonwebtoken"; 3 | import {Model} from "mongoose"; 4 | import { FastifyReply, FastifyRequest } from "fastify"; 5 | import axios, {AxiosResponse} from "axios"; 6 | import crypto from 'crypto'; 7 | const hashes = new Map() 8 | class Yoomoney { 9 | private db: Model; 10 | private tokens: Model 11 | 12 | constructor(db: Model, tokens: Model) { 13 | this.db = db; 14 | this.tokens = tokens; 15 | } 16 | async RecivePayments(req: FastifyRequest, res: FastifyReply): Promise { 17 | let body = req.body as Yoomoneydata 18 | if(body.sha1_hash 19 | !== 20 | crypto.createHash('sha1') 21 | .update(`${body.notification_type}&${body.operation_id}&${body.amount}&${body.currency}&${body.datetime}&${body.sender}&${body.codepro}&${process.env.YOOMONEY_CALLBACK_SECRET}&${body.label}` 22 | ).digest('hex') 23 | ) return; 24 | const { withdraw_amount, label, operation_id } = body; 25 | if(!hashes.has(Number(label))) return; 26 | if(withdraw_amount < hashes.get(Number(label)).amount) return; 27 | const ReciveDonation = async (username: string, amount: string | number, money: string, message: string) => { 28 | console.log(`Новый донат от ${username}! Через систему Yoomoney`); 29 | await this.db.create({ 30 | operation_id, 31 | username, 32 | money, 33 | comment: message, 34 | time: Date.now() 35 | }); 36 | } 37 | jwt.verify(hashes.get(Number(body.label)).token, process.env.JWT_SECRET, async (err: Error, data: Record) => { 38 | switch (data.type) { 39 | case 'discord': { 40 | const tokendata = await this.tokens.findOne({ 41 | userid: data?.id.toString(), 42 | exp: data.exp 43 | }).then(x => x) 44 | const response: AxiosResponse | null = await axios.get('https://discord.com/api/users/@me', { 45 | headers: { 46 | // @ts-ignore 47 | authorization: `Bearer ${tokendata?.accessToken}` 48 | } 49 | }).catch(() => { 50 | return null; 51 | }); 52 | if (!response?.data.id) return null; 53 | hashes.delete(label) 54 | await ReciveDonation(response?.data.username + "#" + response?.data.discriminator, withdraw_amount, 'RUB',hashes.get(Number(body.label)).comment) 55 | } 56 | break; 57 | case 'vk': { 58 | const response: AxiosResponse | null = 59 | await axios.get(`https://api.vk.com/method/users.get?user_ids=${Number(data.id)}&v=5.131&fields=photo_400_orig&access_token=${process.env.VK_API_KEY}&lang=ru`) 60 | .catch(() => { 61 | return null; 62 | }); 63 | hashes.delete(label) 64 | await ReciveDonation(response?.data.response[0].first_name + " " + response?.data.response[0].last_name, withdraw_amount, 'RUB',hashes.get(Number(body.label)).comment) 65 | } 66 | break; 67 | } 68 | }) 69 | res.send(200); 70 | } 71 | async createhash(req: FastifyRequest, res: FastifyReply): Promise { 72 | if(!req.headers.authorization) return res.status(401).send({code: 401, message:"Unauthorized"}); 73 | jwt.verify(req.headers.authorization, process.env.JWT_SECRET, (err: Error) => { 74 | if(err) return res.code(404).send({code: 404, message: "Not found"}); 75 | //@ts-ignore 76 | let { comment, amount } = JSON.parse(req.body); 77 | if(!req.body) return res.status(400).send({code: 400, message: 'Bad requesst'}) 78 | amount = Number(amount) 79 | console.log(comment, amount) 80 | if(!comment || !amount) return res.status(400).send({code: 400, message: 'Bad requetst'}); 81 | if(amount < 10) return res.status(400).send({code: 400, message: 'Bad requestt'}); 82 | function getRandomInt(min: number, max: number) { 83 | min = Math.ceil(min); 84 | max = Math.floor(max); 85 | return Math.floor(Math.random() * (max - min + 1)) + min; 86 | } 87 | 88 | const hash = getRandomInt(1000, 100000) 89 | hashes.set(hash, { 90 | token: req.headers.authorization, 91 | comment, 92 | amount 93 | }); 94 | res.send({url: encodeURI(`https://yoomoney.ru/quickpay/confirm.xml?receiver=${process.env.YOOMONEY_NUMBER}&quickpay-form=shop&targets=Пожертвование для MrLivixx&paymentType=SB&sum=${amount}&label=${hash}&successURL=${process.env.CORS_URL}`)}) 95 | }) 96 | } 97 | } 98 | export default Yoomoney; 99 | -------------------------------------------------------------------------------- /backend/src/Servies/oauth2.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | //@ts-ignore 3 | import jwt from "jsonwebtoken"; 4 | import {Model} from "mongoose"; 5 | import {FastifyReply, FastifyRequest} from "fastify"; 6 | import axios, {AxiosResponse} from "axios"; 7 | import { URLSearchParams } from "url"; 8 | 9 | class Oauth2 { 10 | private db: Model; 11 | constructor(db: Model) { 12 | this.db = db; 13 | } 14 | async Discordlogin(req: FastifyRequest,res: FastifyReply) : Promise { 15 | const { code, error } = JSON.parse(JSON.stringify(req.query)); 16 | if(error == "access_denied") return res.view('index.ejs', { 17 | token: null 18 | }); 19 | if(!code) return res.redirect('/oauth2/discord/authorize'); 20 | const response: AxiosResponse = await axios.post('https://discord.com/api/oauth2/token', 21 | //@ts-ignore 22 | new URLSearchParams({ 23 | client_id: process.env.DISCORD_CLIENT_ID, 24 | client_secret: process.env.DISCORD_CLIENT_SECRET, 25 | redirect_uri: process.env.DISCORD_REDIRECT_URL, 26 | code, 27 | grant_type: 'authorization_code', 28 | scope: 'identify', 29 | }), 30 | { 31 | headers: { 'Content-Type': 'application/x-www-form-urlencoded'}, 32 | }).catch(() => {return res.status(500).send({code: 500, message: "Internal server error"})}); 33 | const { access_token } = response?.data; 34 | if (!access_token) return res.redirect('/oauth2/discord/authorize'); 35 | const getuser: AxiosResponse = await axios.get('https://discord.com/api/users/@me',{ 36 | headers: { 37 | authorization: `Bearer ${access_token}` 38 | } 39 | }).catch(() => {return res.status(500).send({code: 500, message: "Internal server error"})}); 40 | const { id } = getuser.data; 41 | 42 | if (!id) return res.redirect('/oauth2/discord/authorize'); 43 | const token = jwt.sign( 44 | {id, type: 'discord'}, 45 | process.env.JWT_SECRET, 46 | {algorithm: 'HS256', expiresIn: '7d'}) 47 | await this.db.create({ 48 | userid: id, 49 | accessToken: access_token, 50 | exp: await jwt.verify(token, process.env.JWT_SECRET).exp 51 | 52 | }); 53 | res.view('index.ejs', { 54 | token 55 | }); 56 | } 57 | async VKAuthorization(req: FastifyRequest, res: FastifyReply): Promise { 58 | const code = JSON.parse(JSON.stringify(req.query))?.code; 59 | if(!code) return res.redirect('/oauth2/vk/authorize'); 60 | const user: AxiosResponse = await axios.get(`https://oauth.vk.com/access_token?client_id=${process.env.VK_CLIENT_ID}&client_secret=${process.env.VK_CLIENT_SECRET}&redirect_uri=${process.env.VK_REDIRECT_URL}&code=${code}`) 61 | .catch((x) => { 62 | if(x?.response?.data?.error === 'invalid_grant') return res.redirect('/oauth2/vk/authorize'); 63 | else return res.status(500).send({code: 500, message: "Internal server error"}) 64 | }); 65 | if(!user?.data?.user_id) return res.redirect('/oauth2/vk/authorize'); 66 | res.view('index.ejs', { 67 | token: jwt.sign( 68 | {id: user.data.user_id, type: 'vk'}, 69 | process.env.JWT_SECRET, 70 | {algorithm: 'HS256'}) 71 | }); 72 | 73 | } 74 | async getUser(req: FastifyRequest,res: FastifyReply) : Promise { 75 | if(!req.headers.authorization) return res.code(401).send({code: 401,message:"Unauthorized"}) 76 | jwt.verify(req.headers.authorization,process.env.JWT_SECRET, async (err:Error,data: Record) => { 77 | if(err) return res.code(404).send({code: 404, message: "Not found"}); 78 | else { 79 | switch (data.type.toString()) { 80 | case 'vk': { 81 | const response: AxiosResponse = 82 | await axios.get(`https://api.vk.com/method/users.get?user_ids=${Number(data.id)}&v=5.131&fields=photo_400_orig&access_token=${process.env.VK_API_KEY}&lang=ru`) 83 | .catch(() => { 84 | return res.status(500).send({code: 500, message: "Internal server error"}) 85 | }); 86 | res.status(200).send({type: 'vk', data: response.data.response[0]}) 87 | } break; 88 | case 'discord': { 89 | const tokendata = await this.db.findOne({userid: data?.id.toString(),exp: data?.exp}) 90 | const response: AxiosResponse = await axios.get('https://discord.com/api/users/@me', { 91 | headers: { 92 | //@ts-ignore 93 | authorization: `Bearer ${tokendata?.accessToken}` 94 | } 95 | }).catch(() => {return res.status(500).send({code: 500, message: 'Internal server error'})}); 96 | if(!response.data?.id) return res.send({code:404, message: "Not found"}) 97 | res.status(200).send({type: 'discord', data: response.data}) 98 | } 99 | 100 | } 101 | } 102 | }); 103 | } 104 | } 105 | export default Oauth2; 106 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ESNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "CommonJS", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ 46 | "paths": { 47 | "@/*": [ 48 | "./*" 49 | ] 50 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | // "typeRoots": [], /* List of folders to include type definitions from. */ 53 | // "types": [], /* Type declaration files to be included in compilation. */ 54 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 55 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 56 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 57 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 58 | 59 | /* Source Map Options */ 60 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 63 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 64 | 65 | /* Experimental Options */ 66 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 67 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 68 | 69 | /* Advanced Options */ 70 | "skipLibCheck": true /* Skip type checking of declaration files. */ 71 | // "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 72 | }, 73 | "include": [ 74 | "src", 75 | "index.ts" 76 | ], 77 | "exclude": [ 78 | "node_modules" 79 | ] 80 | } -------------------------------------------------------------------------------- /frontend/public/wrapped.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Donate page backend 2 | Back-end часть проекта 3 | 4 | ### Для запуска проекта выполните ряд действий: 5 | - Склонируйте репозиторий и установите зависимости 6 | ```shell 7 | $ git clone https://github.com/MrLivixx/Donate-page.git 8 | $ npm i 9 | ``` 10 | Или установите бекенд часть проекта на хероку по нажатию одной кнопки! 11 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/MrLivixx/donate-page) 12 | - Смените название файла .env.example на .env и подставьте свои значения 13 |
Пример: 14 | ```dotenv 15 | DB_URL=mongodb+srv://****/donatepage 16 | PORT=1045 17 | CORS_URL=http://localhost:8080 18 | 19 | #DISCORD OAUTH 20 | DISCORD_CLIENT_ID=ID 21 | DISCORD_REDIRECT_URL=https://api.donate.mrlivixx.me/oauth2/discord/callback 22 | DISCORD_CLIENT_SECRET=**** 23 | 24 | #VK OAUTH 25 | VK_CLIENT_ID=ID 26 | VK_CLIENT_SECRET=**** 27 | VK_REDIRECT_URL=https://api.donate.mrlivixx.me/oauth2/vk/callback 28 | VK_API_KEY=fghjlwe.gdhyh 29 | #JWT 30 | 31 | JWT_SECRET=HASH_KEY 32 | 33 | # PAYMENTS 34 | #Donatinon Alerts api key 35 | DA_SECRET=adlj123&* 36 | #QIWI SECRET p2p KEY 37 | QIWI_SECRET_KEY=****** 38 | #QIWI THEME 39 | QIWI_THEME=Nykyta-S0FqLeU_kv 40 | ``` 41 | ### Коротко о том что за значения надо внести в .env 42 | 43 | - `DB_URL` - Ссылка на базу данных mongodb. Как создать базу данных можете прочитать в моём другом гайде [здесь](https://github.com/sqdsh/simple-discord-bot/blob/gh-pages/index.md#прочее) 44 | - `PORT` - Порт веб сервера, для работы на своей VPS/VDS машине, использовать прокси как nginx и apache2. Если heroku/glitch оставить пустым 45 | - `CORS_URL` - Адрес сайта для защиты сокета и API с помощью [cors](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 46 | - `DISCORD_CLIENT_ID` - CLIENT ID вашего Discord приложения 47 | - `DISCORD_REDIRECT_URL` - Ссылка после удачной авторизации (должна иметь `/oauth2/discord/callback` если иная, то обязательно смените в routers.ts) 48 | - `DISCORD_CLIENT_SECRET` - Client secret вашего приложения 49 | - `VK_CLIENT_ID` - ID вашего приложение в ВКонтакте 50 | - `VK_CLIENT_SECRET` - Защищённый ключ приложения 51 | - `VK_REDIRECT_URL` - Ссылка после удачной авторизации (должна иметь `/oauth2/vk/callback` если иная, то обязательно смените в routers.ts) 52 | - `VK_API_KEY` - Сервисный ключ доступа вашего приложения 53 | - `JWT_SECRET` - Случайное значение с [сайта](https://randomkeygen.com/) для работы авторизации 54 | - `DA_SECRET` - Секретный ключ с [DonationAlerts](https://www.donationalerts.com) об котором поговорим ниже 55 | - `QIWI_SECRET_KEY` - Секретный ключ p2p для оплаты. 56 | - `QIWI_THEME` - Код вашей темы. 57 | 58 | # Настройка 59 | Перейдём к настройке бекенда, для работы 60 | ## Авторизация 61 | ### Discord 62 | - Для настройки авторизации через Discord, вы должны перейти на [портал разработчиков](https://discord.com/developers) и создать своё приложение. 63 | ![](https://imgs.mrlivixx.me/opera_sW5M8Fodd7.png) 64 | - Далее, перейти в само приложение и во вкладку "OAuth2" 65 | ![](https://imgs.mrlivixx.me/opera_nUmez0CG50.png) 66 | - Затем, копируем CLIENT ID и CLIENT SECRET ставим его в строчку `DISCORD_CLIENT_ID` и `DISCORD_CLIENT_SECRET` в файле .env 67 | ![](https://imgs.mrlivixx.me/opera_CzGdL73auE.png) 68 | - И для того чтобы авторизация работала, надо указать доверенную ссылку перенаправления в формате `https://вашдомен.ру/oauth2/discord/callback` 69 | ![](https://imgs.mrlivixx.me/opera_87kfm44xJh.png) 70 | Готово! Мы настроили авторизацию через Discord 71 | ### VK 72 | - Для настройки авторизации через VK, вы должны перейти на [страницу приложений](https://vk.com/apps?act=manage) и создать своё приложение 73 | ![](https://imgs.mrlivixx.me/opera_aSuufChRcl.png) 74 | - При создании в поле "Название" пишите своё имя. В поле "Адрес сайта" **Адрес сайта** на котором будет таблица, в поле **Базовый домен** ссылку на бекенд проекта. 75 | ![](https://imgs.mrlivixx.me/opera_8P6tsEf9xv.png) 76 | - После этого, идём во вкладку "Настройки", и скопируйте ID приложения и вставьте его в .env в значение `VK_CLIENT_ID`, защищённый ключ в `VK_CLIENT_SECRET`, сервисный ключ доступа в `VK_API_KEY`. 77 | ![](https://imgs.mrlivixx.me/opera_e7TqHFbRAu.png) 78 | - И для того чтобы авторизация работала, надо указать доверенную ссылку перенаправления в формате `https://вашдомен.ру/oauth2/vk/callback`, как делали ранее 79 | ![](https://imgs.mrlivixx.me/opera_nnZAIoBF9h.png) 80 | Готово! Мы настроили авторизацию через VK 81 | 82 | ## Приём донатов 83 | ### Donation alerts 84 | Для получения донатов через [DonationAlerts](https://donationalerts.com) необходимо сделать следующие действия: 85 | - Авторизоваться на сайте через соц. сети и перейти в панель управления. 86 | ![](https://imgs.mrlivixx.me/opera_s4bSH8r3U2.png) 87 | - В панели управления открыть вкладку "Основные настройки" и скопируйте секретный токен и укажите его в `DA_SECRET` 88 | ![](https://imgs.mrlivixx.me/opera_68BNihALpM.png) 89 | 90 | Готово! Мы сделали приём донатов через DonationAlerts! 91 | 92 | Если вы не хотите принимать донаты через DonationAlerts, то оставьте поле `DA_SECRET` в .env пустым. 93 | 94 | ### Qiwi p2p 95 | > Говорю заранее, этот способ доступен только для кошельков, **имеющие** статус **Основной** и выше, с анонимными кошельками не выйдет, для отключения оплаты через Qiwi p2p не указывайте данные в .env связанные с Qiwi. 96 | - Для настройки p2p Qiwi, вы должны перейти на страницу [Qiwi p2p](https://p2p.qiwi.com) и авторизоваться. 97 | ![](https://imgs.mrlivixx.me/opera_zRqYQFaa72.png) 98 | - После этого, в панели открываем вкладку "API" и листаем до "Аутентификационные данные" 99 | ![](https://imgs.mrlivixx.me/opera_9bW5lrCw1v.png) 100 | - Жмём на кнопку "Создать пару ключей и настроить", пишите желаемое имя и **обязательно** ставим галочку на "Использовать эту пару ключей для серверных уведомлений об изменении статусов счетов 101 | Подробнее об уведомлениях" и указываем туда ссылку в форме `https://вашдомен.ру/qiwi/callback`, после этого копируем секретный ключ и указываем его в .env в `QIWI_SECRET_KEY` 102 | ![](https://imgs.mrlivixx.me/opera_OZ2f9o65Zl.png) 103 | Готово! Мы настроили приём платежей через Qiwi p2p. 104 | 105 | ## Прочие настройки 106 |
`DB_URL` - укажите туда ссылку на вашу базу данных MongoDB 107 |
`JWT_SECRET` - Обязательно необходимо указать значение с [](https://randomkeygen.com/) для подписания токенов сессий. [Подробнее об системе](https://jwt.io) 108 |
Для работы сайта только с вашего домена, в поле `CORS_URL` укажите адрес своего сайта, если защита не нужна, укажите `*` что крайне не рекомендуется. 109 |
Если вы хотите кастомизировать страницу оплату Qiwi, то перейдите на страницу [форму переводов](https://qiwi.com/p2p-admin/transfers/link) и при нажатии на кнопку "настроить", вам будет предложено настроить внешний вид, после этого, скопируйте код который показан на скриншоте и укажите его в `QIWI_THEME` без кавычек 110 | ![](https://imgs.mrlivixx.me/opera_y5izt5UL1L.png) 111 | -------------------------------------------------------------------------------- /frontend/src/css/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800;900&display=swap'); 2 | 3 | :root{ 4 | --element-color: rgba(9, 9, 9, 0.25); 5 | --element-border: 2px solid rgba(21, 21, 21, 0.75); 6 | --border-radius: 25px; 7 | } 8 | 9 | * { 10 | -webkit-tap-highlight-color: transparent; 11 | } 12 | 13 | *, 14 | *::before, 15 | *::after { 16 | box-sizing: border-box; 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | button{ 22 | font-family: 'Montserrat', sans-serif; 23 | color: inherit; 24 | } 25 | 26 | html{ 27 | background-color: #121212; 28 | background-image: url('/wrapped.svg'); 29 | background-repeat: no-repeat; 30 | background-position: top right; 31 | background-size: 75%; 32 | } 33 | 34 | #app, html { 35 | margin: 0 auto; 36 | font-family: 'Montserrat', sans-serif; 37 | color: #FDFDFD; 38 | scroll-behavior: smooth; 39 | overflow-x: hidden; 40 | line-height: 1.5; 41 | width: 100%; 42 | height: 100%; 43 | font-weight: 400; 44 | } 45 | 46 | #app{ 47 | padding-left: 25px; 48 | padding-right: 25px; 49 | max-width: 1100px; 50 | } 51 | 52 | ::-webkit-scrollbar { 53 | display: none; 54 | } 55 | 56 | .transition{ 57 | transition: .2s 58 | } 59 | 60 | .small{ 61 | font-size: 16px; 62 | color: #5e5e5e; 63 | } 64 | 65 | .loading { 66 | position: fixed; 67 | top: 50%; 68 | left: 50%; 69 | transform: translate(-50%, -50%); 70 | text-align: center; 71 | font-weight: 600; 72 | font-size: 24px; 73 | background: rgba(18, 18, 18, 0.06); 74 | border: 2px solid rgba(105, 105, 105, 0.3); 75 | padding: 30px; 76 | -webkit-backdrop-filter: blur(42px); 77 | backdrop-filter: blur(42px); 78 | width: 300px; 79 | border-radius: 25px; 80 | } 81 | 82 | .loader { 83 | margin-bottom: 20px; 84 | } 85 | 86 | .header-container { 87 | margin-top: 40px; 88 | margin-bottom: 60px; 89 | } 90 | 91 | .page-title { 92 | font-size: 55px; 93 | font-weight: 600; 94 | animation: tracking-in-expand 0.7s cubic-bezier(0.215, 0.610, 0.355, 1.000) both; 95 | animation-delay: 1.7s; 96 | } 97 | .page-title-404 { 98 | font-size: 55px; 99 | font-weight: 600; 100 | } 101 | 102 | .page-subheading { 103 | font-size: 28px; 104 | animation: fade-in 0.7s cubic-bezier(0.390, 0.575, 0.565, 1.000) both; 105 | animation-delay: 1.8s; 106 | } 107 | 108 | .heart-emoji { 109 | width: 27px; 110 | vertical-align: baseline; 111 | margin-left: 5px; 112 | } 113 | 114 | .buttons { 115 | display: grid; 116 | grid-template-columns: 1fr 1fr; 117 | gap: 20px; 118 | animation: fade-in 0.7s cubic-bezier(0.390, 0.575, 0.565, 1.000) both; 119 | animation-delay: 1.8s; 120 | } 121 | 122 | .login { 123 | box-sizing: border-box; 124 | -webkit-backdrop-filter: blur(42px); 125 | backdrop-filter: blur(42px); 126 | border-radius: 25px; 127 | width: 100%; 128 | padding: 17px 30px 14px; 129 | text-align: left; 130 | font-size: 22px; 131 | font-weight: 600; 132 | cursor: pointer; 133 | transition: .2s; 134 | display: flex; 135 | align-items: center; 136 | gap: 25px; 137 | outline: none 138 | } 139 | 140 | .discord { 141 | background: rgba(88, 101, 242, 0.15); 142 | border: 2px solid #5865F2; 143 | } 144 | 145 | .vk { 146 | background: rgba(39, 135, 245, 0.15); 147 | border: 2px solid #2787F5; 148 | } 149 | 150 | .login svg { 151 | width: 32px; 152 | } 153 | 154 | .login:hover { 155 | background: rgb(88 101 242 / 38%); 156 | transition: .2s; 157 | } 158 | 159 | .vk:hover { 160 | background: rgba(39, 135, 245, 0.38); 161 | } 162 | 163 | 164 | .donate-table { 165 | margin-top: 30px; 166 | margin-bottom: 30px; 167 | } 168 | 169 | .main-table { 170 | background: rgba(18, 18, 18, 0.06); 171 | border: 2px solid rgba(105, 105, 105, 0.3); 172 | -webkit-backdrop-filter: blur(42px); 173 | backdrop-filter: blur(42px); 174 | border-radius: 25px; 175 | width: 100%; 176 | text-align: center; 177 | font-size: 22px; 178 | padding: 22px 28px; 179 | border-spacing: 0; 180 | animation: fade-in 0.7s cubic-bezier(0.390, 0.575, 0.565, 1.000) both; 181 | animation-delay: 1.3s; 182 | } 183 | 184 | td, th { 185 | padding: 27px 22px; 186 | } 187 | 188 | td, th { 189 | border-right: 1px solid rgba(105, 105, 105, 0.3); 190 | border-left: 1px solid rgba(105, 105, 105, 0.3); 191 | border-bottom: 2px solid rgba(105, 105, 105, 0.3); 192 | } 193 | 194 | td:first-child, th:first-child { 195 | border-left: 0; 196 | } 197 | 198 | td:last-child, th:last-child { 199 | border-right: 0; 200 | } 201 | 202 | tr:last-child td { 203 | border-bottom: 0; 204 | } 205 | 206 | thead tr th { 207 | font-weight: 600; 208 | font-weight: 600; 209 | padding: 7px 22px 16px; 210 | } 211 | 212 | .donate-container { 213 | margin-bottom: 50px; 214 | } 215 | 216 | .form-comment { 217 | width: 100%; 218 | } 219 | 220 | .donate-form { 221 | background: rgba(18, 18, 18, 0.06); 222 | border: 2px solid rgba(105, 105, 105, 0.3); 223 | -webkit-backdrop-filter: blur(42px); 224 | backdrop-filter: blur(42px); 225 | border-radius: 25px; 226 | width: 100%; 227 | font-size: 24px; 228 | padding: 23px 35px; 229 | animation: fade-in 0.7s cubic-bezier(0.390, 0.575, 0.565, 1.000) both; 230 | animation-delay: 1.2s; 231 | } 232 | 233 | .input-flex { 234 | display: flex; 235 | gap: 70px; 236 | } 237 | 238 | .form-heading { 239 | font-weight: 600; 240 | margin-bottom: 15px; 241 | } 242 | 243 | .form-loggined-as { 244 | margin-bottom: 30px; 245 | } 246 | 247 | .user-avatar { 248 | border-radius: 50%; 249 | width: 50px; 250 | } 251 | 252 | .user-username { 253 | font-weight: 500; 254 | width: 100%; 255 | text-overflow: ellipsis; 256 | white-space: nowrap; 257 | overflow: hidden; 258 | } 259 | 260 | .form-input { 261 | background: #202020; 262 | border: 2px solid rgba(105, 105, 105, 0.3); 263 | font-family: inherit; 264 | color: inherit; 265 | font-size: 22px; 266 | border-radius: 25px; 267 | padding: 15px 25px 14px; 268 | font-weight: 500; 269 | outline: none; 270 | } 271 | 272 | .flex-20 { 273 | display: flex; 274 | gap: 20px; 275 | align-items: center; 276 | } 277 | 278 | input::-webkit-outer-spin-button, 279 | input::-webkit-inner-spin-button { 280 | -webkit-appearance: none; 281 | margin: 0; 282 | } 283 | 284 | input[type=number] { 285 | -moz-appearance: textfield; 286 | } 287 | 288 | .form-currency { 289 | font-weight: 500; 290 | } 291 | 292 | .or { 293 | font-size: 16px; 294 | margin-top: 10px; 295 | margin-bottom: 10px; 296 | text-align: center; 297 | color: #6e6e6e; 298 | } 299 | 300 | .form-checkout { 301 | box-sizing: border-box; 302 | -webkit-backdrop-filter: blur(42px); 303 | backdrop-filter: blur(42px); 304 | border-radius: 25px; 305 | width: 100%; 306 | padding: 15px 25px 14px; 307 | text-align: center; 308 | font-size: 22px; 309 | font-weight: 600; 310 | cursor: pointer; 311 | transition: .2s; 312 | background: rgba(56, 126, 110, 0.25); 313 | border: 2px solid #387E6E; 314 | color: inherit; 315 | font-family: inherit; 316 | } 317 | 318 | .donate-top { 319 | margin-bottom: 30px; 320 | } 321 | 322 | .donate-buttons { 323 | display: flex; 324 | align-items: center; 325 | gap: 20px; 326 | } 327 | 328 | .comment { 329 | width: 100%; 330 | } 331 | 332 | .form-checkout:hover { 333 | background: rgba(56, 126, 110, 0.55); 334 | } 335 | 336 | @media screen and (max-width: 1076px) { 337 | table thead { 338 | display: none 339 | } 340 | 341 | .main-table { 342 | padding: 15px 15px; 343 | } 344 | 345 | table tr { 346 | display: block; 347 | border-bottom: 2px solid rgba(105, 105, 105, 0.3); 348 | } 349 | 350 | table td { 351 | display: block; 352 | text-align: right; 353 | font-size: 16px; 354 | padding-right: 20px; 355 | padding-left: 20px; 356 | } 357 | 358 | tr, td { 359 | border: 0; 360 | } 361 | 362 | td, th { 363 | padding: 15px 24px; 364 | } 365 | 366 | table tr:last-child { 367 | border-bottom: 0 368 | } 369 | 370 | table td:before { 371 | content: attr(data-label); 372 | float: left; 373 | font-weight: 700 374 | } 375 | 376 | .buttons { 377 | grid-template-columns: 1fr; 378 | } 379 | 380 | .login { 381 | line-height: 1.8; 382 | text-align: center; 383 | padding: 10px 20px 9px; 384 | justify-content: center; 385 | } 386 | 387 | .login svg { 388 | display: none; 389 | } 390 | 391 | .header-container, .donate-container { 392 | margin-bottom: 40px; 393 | } 394 | 395 | #app { 396 | padding-left: 30px; 397 | padding-right: 30px; 398 | } 399 | 400 | .header-container { 401 | margin-top: 20px; 402 | } 403 | 404 | .page-title { 405 | font-size: 45px; 406 | margin-bottom: 8px; 407 | } 408 | 409 | .page-subheading { 410 | font-size: 25px; 411 | } 412 | 413 | .heart-emoji { 414 | width: 22px; 415 | } 416 | 417 | .donate-form { 418 | grid-template-columns: 1fr; 419 | gap: 30px; 420 | padding: 25px 34px 34px; 421 | } 422 | } 423 | 424 | 425 | @media screen and (max-width: 942px) { 426 | .donate-buttons { 427 | flex-direction: column; 428 | gap: 10px; 429 | } 430 | 431 | .or{ 432 | display: none; 433 | } 434 | 435 | .input-flex{ 436 | gap: 30px; 437 | flex-direction: column; 438 | } 439 | 440 | .form-input{ 441 | width: 100%; 442 | } 443 | } 444 | 445 | @media screen and (max-width: 358px) { 446 | .form-currency{ 447 | display: none; 448 | } 449 | } 450 | 451 | @media screen and (max-width: 508px) { 452 | .full{ 453 | display: none; 454 | } 455 | } 456 | 457 | @keyframes tracking-in-expand { 458 | 0% { 459 | letter-spacing: -0.5em; 460 | opacity: 0; 461 | } 462 | 40% { 463 | opacity: 0.6; 464 | } 465 | 100% { 466 | opacity: 1; 467 | } 468 | } 469 | 470 | @keyframes fade-in { 471 | 0% { 472 | opacity: 0; 473 | } 474 | 100% { 475 | opacity: 1; 476 | } 477 | } 478 | -------------------------------------------------------------------------------- /frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 142 | 295 | --------------------------------------------------------------------------------