├── 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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/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 | 
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 |
--------------------------------------------------------------------------------
/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 |
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 |
2 |
17 |
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 |
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 | [](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 | 
64 | - Далее, перейти в само приложение и во вкладку "OAuth2"
65 | 
66 | - Затем, копируем CLIENT ID и CLIENT SECRET ставим его в строчку `DISCORD_CLIENT_ID` и `DISCORD_CLIENT_SECRET` в файле .env
67 | 
68 | - И для того чтобы авторизация работала, надо указать доверенную ссылку перенаправления в формате `https://вашдомен.ру/oauth2/discord/callback`
69 | 
70 | Готово! Мы настроили авторизацию через Discord
71 | ### VK
72 | - Для настройки авторизации через VK, вы должны перейти на [страницу приложений](https://vk.com/apps?act=manage) и создать своё приложение
73 | 
74 | - При создании в поле "Название" пишите своё имя. В поле "Адрес сайта" **Адрес сайта** на котором будет таблица, в поле **Базовый домен** ссылку на бекенд проекта.
75 | 
76 | - После этого, идём во вкладку "Настройки", и скопируйте ID приложения и вставьте его в .env в значение `VK_CLIENT_ID`, защищённый ключ в `VK_CLIENT_SECRET`, сервисный ключ доступа в `VK_API_KEY`.
77 | 
78 | - И для того чтобы авторизация работала, надо указать доверенную ссылку перенаправления в формате `https://вашдомен.ру/oauth2/vk/callback`, как делали ранее
79 | 
80 | Готово! Мы настроили авторизацию через VK
81 |
82 | ## Приём донатов
83 | ### Donation alerts
84 | Для получения донатов через [DonationAlerts](https://donationalerts.com) необходимо сделать следующие действия:
85 | - Авторизоваться на сайте через соц. сети и перейти в панель управления.
86 | 
87 | - В панели управления открыть вкладку "Основные настройки" и скопируйте секретный токен и укажите его в `DA_SECRET`
88 | 
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 | 
98 | - После этого, в панели открываем вкладку "API" и листаем до "Аутентификационные данные"
99 | 
100 | - Жмём на кнопку "Создать пару ключей и настроить", пишите желаемое имя и **обязательно** ставим галочку на "Использовать эту пару ключей для серверных уведомлений об изменении статусов счетов
101 | Подробнее об уведомлениях" и указываем туда ссылку в форме `https://вашдомен.ру/qiwi/callback`, после этого копируем секретный ключ и указываем его в .env в `QIWI_SECRET_KEY`
102 | 
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 | 
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 |
2 |
3 |
9 |
78 |
79 |
80 |
111 |
112 |
113 | Идёт загрузка......
114 |
115 |
116 |
117 | Похоже, здесь пусто.....
118 |
119 |
120 |
121 |
122 |
123 | | Никнейм |
124 | Сумма |
125 | Дата |
126 | Комментарий |
127 |
128 |
129 |
130 |
131 | | {{ donate.username || "Неизвестный" }} |
132 | {{ donate.money }} |
133 | {{ getDate(donate.time) }} |
134 | {{ donate.comment || "Не указано" }} |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
295 |
--------------------------------------------------------------------------------