diff --git a/.docker/docker-compose.yaml b/.docker/docker-compose.yaml new file mode 100644 index 0000000..f10ce0e --- /dev/null +++ b/.docker/docker-compose.yaml @@ -0,0 +1,9 @@ +services: + web: + build: + context: app + target: dev-envs + ports: + - '8090:8090' + volumes: + - /var/run/docker.sock:/var/run/docker.sock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a9d9a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.vercel +*.pyc +__pycache__ +db.sqlite3 + +# Environments +.idea/ +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ diff --git a/app/.vscode/settings.json b/app/.vscode/settings.json new file mode 100644 index 0000000..a0845f4 --- /dev/null +++ b/app/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "liveServer.settings.port": 8000, + "liveServer.settings.donotShowInfoMsg": true, + "liveServer.settings.AdvanceCustomBrowserCmdLine": "", + "liveServer.settings.root": "/", + "liveServer.settings.host": "localhost", + // "liveServer.settings.https": { + // "enable": true, + // "cert": "/Users/16716942/SOFTWARRIOR/TemaBot2/app/certs/localhost.crt", + // "key": "/Users/16716942/SOFTWARRIOR/TemaBot2/app/certs/localhost.key", + // "passphrase": "" + // } +} diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..658a78e --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:1.4 + +FROM --platform=linux/arm64/v8 python:3.12 AS builder +EXPOSE 8080 +WORKDIR /app +COPY requirements.txt /app +RUN pip3 install -r requirements.txt --no-cache-dir +COPY . /app +ENTRYPOINT ["python3"] +CMD ["manage.py", "runserver", "0.0.0.0:8080"] + +FROM builder as dev-envs +RUN < + +

Hello from Vercel!

+

The current time is { now }.

+ + + ''' + return HttpResponse(html) +``` + +This view is exposed a URL through `example/urls.py`: + +```python +# django_app/urls.py +from django.urls import path + +from django_app.views import index + +urlpatterns = [ + path('', index), +] +``` + +Finally, it's made accessible to the Django server inside `vercel_app/urls.py`: + +```python +# vercel_app/urls.py +from django.urls import path, include + +urlpatterns = [ + ... + path('', include('django_app.urls')), +] +``` + +This example uses the Web Server Gateway Interface (WSGI) with Django to enable handling requests on Vercel with Serverless Functions. + +## Running Locally + +```bash + +python manage.py collectstatic +python manage.py runserver 8080 + +docker rmi -f + +docker compose down && docker compose build --no-cache && docker compose up + +Docker: https://sematext.com/blog/docker-logs-location/ +UI: https://docs.sencha.com/touch/2.3.1/#!/api +Localhost: https://letsencrypt.org/ru/docs/certificates-for-localhost/ + + +ssh first@192.168.1.100 +less +G /var/lib/docker/containers//-json.log + + +``` +https://c0.klipartz.com/pngpicture/54/511/gratis-png-meme-informacion-pegatina-telegrama-meme-thumbnail.png + +Your Django application is now available at `http://localhost:8000`. + +## One-Click Deploy + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fpython%2Fdjango&demo-title=Django%20%2B%20Vercel&demo-description=Use%20Django%204%20on%20Vercel%20with%20Serverless%20Functions%20using%20the%20Python%20Runtime.&demo-url=https%3A%2F%2Fdjango-template.vercel.app%2F&demo-image=https://assets.vercel.com/image/upload/v1669994241/random/django.png) diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..502effa --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,2 @@ +from .api import * +from .admin_api import * \ No newline at end of file diff --git a/backend/admin_api.py b/app/api/admin_api.py similarity index 98% rename from backend/admin_api.py rename to app/api/admin_api.py index 9c424e7..0e1f93c 100644 --- a/backend/admin_api.py +++ b/app/api/admin_api.py @@ -1,6 +1,6 @@ import psycopg2 -from const import DB_CONFIG +from consts import DB_CONFIG def add_product_to_shop( name: str, diff --git a/backend/api.py b/app/api/api.py similarity index 99% rename from backend/api.py rename to app/api/api.py index 7133b21..a4e977f 100644 --- a/backend/api.py +++ b/app/api/api.py @@ -9,7 +9,7 @@ from decimal import Decimal, InvalidOperation from utils import * from type import * -from const import * +from consts import * def update_token_expiry_date(token: str, expiry_date: date) -> None: connection = psycopg2.connect(**DB_CONFIG) diff --git a/app/consts/__init__.py b/app/consts/__init__.py new file mode 100644 index 0000000..983f876 --- /dev/null +++ b/app/consts/__init__.py @@ -0,0 +1 @@ +from .const import * \ No newline at end of file diff --git a/backend/const.py b/app/consts/const.py similarity index 100% rename from backend/const.py rename to app/consts/const.py diff --git a/app/manage.py b/app/manage.py new file mode 100644 index 0000000..5767e97 --- /dev/null +++ b/app/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..af9acca --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,6 @@ +Django==5.2.7 +aiohttp +requests +pytz +psycopg2-binary +bcrypt diff --git a/app/server/__init__.py b/app/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/admin.py b/app/server/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/app/server/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/server/apps.py b/app/server/apps.py new file mode 100644 index 0000000..3cf9e6a --- /dev/null +++ b/app/server/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ExampleConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'server' diff --git a/app/server/migrations/__init__.py b/app/server/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/server/urls.py b/app/server/urls.py new file mode 100644 index 0000000..42ffbde --- /dev/null +++ b/app/server/urls.py @@ -0,0 +1,11 @@ +#from django.contrib import admin +from django.urls import path + +from server.views import (get_shop, get_user, get_basket, get_history) + +urlpatterns = [ + path("shop/", get_shop, name="get_shop"), + path("user/", get_user, name="get_user"), + path("basket/", get_basket, name="get_basket"), + path("history/", get_history, name="get_history"), +] diff --git a/app/server/views.py b/app/server/views.py new file mode 100644 index 0000000..72fc1ca --- /dev/null +++ b/app/server/views.py @@ -0,0 +1,55 @@ +import json +from django.http import JsonResponse +from django.views.decorators.csrf import csrf_exempt + +from api import get_products +from utils import decimal_to_float + +@csrf_exempt +async def get_shop(request): + try: + products = None + if request.method == 'GET': + products = get_products() + user_agent = request.headers.get('User-Agent') + products = decimal_to_float(products) + print("get_shop", user_agent) + return JsonResponse({"OK": products}, status=200) + except Exception as error: + return JsonResponse({"ERROR": format(error)}, status=500) + +@csrf_exempt +async def get_user(request): + try: + if request.method == 'POST': + body: dict = json.loads(request.body) + # TODO: вызвать API метод и вернуть JSON + print("get_user", body) + return JsonResponse({"OK": "JSON cleared"}, status=200) + except Exception as error: + return JsonResponse({"error": format(error)}, status=500) + +@csrf_exempt +async def get_basket(request): + try: + if request.method == 'POST': + body: dict = json.loads(request.body) + # TODO: вызвать API метод и вернуть JSON + print("get_basket", body) + return JsonResponse({"OK": "JSON cleared"}, status=200) + except Exception as error: + return JsonResponse({"error": format(error)}, status=500) + +@csrf_exempt +async def get_history(request): + try: + if request.method == 'POST': + body: dict = json.loads(request.body) + # TODO: вызвать API метод и вернуть JSON + print("get_history", body) + return JsonResponse({"OK": "JSON cleared"}, status=200) + except Exception as error: + return JsonResponse({"error": format(error)}, status=500) + + + diff --git a/app/settings/__init__.py b/app/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/settings/asgi.py b/app/settings/asgi.py new file mode 100644 index 0000000..f042534 --- /dev/null +++ b/app/settings/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for vercel_app project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings') + +app = get_asgi_application() diff --git a/app/settings/settings.py b/app/settings/settings.py new file mode 100644 index 0000000..e41f357 --- /dev/null +++ b/app/settings/settings.py @@ -0,0 +1,125 @@ +""" +Django settings for vercel_app project. + +Generated by 'django-admin startproject' using Django 4.1.3. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-=cldztbc4jg&xl0!x673!*v2_=p$$eu)=7*f#d0#zs$44xx-h^' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['localhost', '127.0.0.1', '192.168.1.100', 'game_shop.softwarrior.ru'] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'server' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'settings.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR, 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'settings.wsgi.app' + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases +# Note: Django modules for using databases are not support in serverless +# environments like Vercel. You can use a database over HTTP, hosted elsewhere. + +DATABASES = {} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") +STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) + + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/app/settings/urls.py b/app/settings/urls.py new file mode 100644 index 0000000..ff8fcc6 --- /dev/null +++ b/app/settings/urls.py @@ -0,0 +1,30 @@ +"""vercel_app URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include, re_path +from django.views.static import serve +from settings import settings + +static_urlpatterns = [ + re_path(r"^static/(?P.*)$", serve, {"document_root": settings.STATIC_ROOT}), +] + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('server.urls')), + path('bot/', include('server.urls')), + path("", include(static_urlpatterns)), +] diff --git a/app/settings/wsgi.py b/app/settings/wsgi.py new file mode 100644 index 0000000..c9c2b2c --- /dev/null +++ b/app/settings/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for vercel_app project. + +It exposes the WSGI callable as a module-level variable named ``app``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings') + +app = get_wsgi_application() diff --git a/app/static/costants.js b/app/static/costants.js new file mode 100644 index 0000000..2d16858 --- /dev/null +++ b/app/static/costants.js @@ -0,0 +1,16 @@ +"use strict"; + +export const COMMANDS = [ + { + name: 'Запустить', + uid: '1' + }, + { + name: 'Остановить', + uid: '2' + }, + { + name: 'Применить', + uid: '3' + }, +] diff --git a/app/static/index.css b/app/static/index.css new file mode 100644 index 0000000..b0a5be8 --- /dev/null +++ b/app/static/index.css @@ -0,0 +1,167 @@ +:root { + --color-menu-point1: #e8ecef; + --color-menu-point2: #1879da; +} + +html, body { + margin: 0; + padding: 0; +} + +.admin { + display: flex; + flex-direction: column; + background: lightgray; + padding: 0 10px 10px 10px; + align-items: center; +} + +.button { + border-radius: 8px; + border: 2px solid var(--color-menu-point1); + background-color: var(--color-menu-point2); + color: var(--color-menu-point1); + padding: 5px; + cursor: pointer; + margin-top: 5px; +} + +.button:hover { + border-color: var(--color-menu-point2); + background-color: var(--color-menu-point1); + color: var(--color-menu-point2); +} + +.divider { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.divider_hr { + width: 100%; + border-top: 0.5px solid var(--color-menu-point2); +} + +.divider_h3 { + margin: 0; + margin-bottom: 10px; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + padding-top: 100px; + left: 0; + top: 0; + width: 100%; + height: calc(100% - 100px); + overflow: auto; + background-color: rgb(0,0,0); + background-color: rgba(0,0,0,0.4); +} + +.modal-content { + background-color: #fefefe; + margin: auto; + padding: 20px; + border: 1px solid #888; + width: 80%; +} + +.modal-text { + white-space: break-spaces; +} + +.modal-close { + color: #aaaaaa; + float: right; + font-size: 12px; + font-weight: bold; +} + +.modal-close:hover, +.modal-close:focus { + color: #000; + text-decoration: none; + cursor: pointer; +} + +.switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #807272; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slider { + background-color: var(--color-menu-point2); +} + +input:focus + .slider { + box-shadow: 0 0 1px var(--color-menu-point2); +} + +input:checked + .slider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 34px; +} + +.slider.round:before { + border-radius: 50%; +} + +.timeInput { + background-color: var(--color-menu-point1); +} + +.group { + display: flex; + gap: 4px; +} + +.direction_column { + flex-direction: column; +} + +.direction_row { + flex-direction: row; +} diff --git a/app/static/index.js b/app/static/index.js new file mode 100644 index 0000000..0d46c12 --- /dev/null +++ b/app/static/index.js @@ -0,0 +1,486 @@ +"use strict"; + +import { COMMANDS } from './costants.js' + +let FEATURES = {} +let LOGS = {} + +class Admin { + constructor () { + return (async () => { + const element = document.createElement('div'); + element.classList.add('admin'); + + const header = document.createElement('h2'); + header.innerHTML = "Настройки TemaBot" + element.append(header); + + const divider0 = new Divider("Фичи:") + element.append(divider0.element) + + FEATURES = await Admin.getFeatures() + console.log("!!! FEATURES", FEATURES) + const features = new Features(FEATURES) + element.append(features.element) + + const applyButton = new Button(COMMANDS[2].name, COMMANDS[2].uid, handleApplyButton) + element.append(applyButton.element) + + const divider1 = new Divider("Контент:") + element.append(divider1.element) + + const downloadButton = new Button("Скачать контент", "download", handleDownloadButton) + element.append(downloadButton.element) + + const upload = new Upload("Загрузить контент", "upload", handleSubmit) + element.append(upload.element) + + const divider2 = new Divider("Логи:") + element.append(divider2.element) + + LOGS = await Admin.getLogs() + console.log("!!! LOGS", LOGS) + const showLogsButton = new Button("Посмотреть логи", "showLogs", handleShowLogsButton) + element.append(showLogsButton.element) + const clearLogsButton = new Button("Очистить логи", "clearLogs", handleClearLogsButton) + element.append(clearLogsButton.element) + + const modal = new Modal(JSON.stringify(LOGS, null, '\\n'), handleModalClose) + element.append(modal.element) + + const divider3 = new Divider("Рассылка:") + element.append(divider3.element) + + const mailing = await Admin.getMailing() + console.log("!!! MAILING", mailing) + + const mailingSwitch = new Switch(mailing.status, "Рассылка", "mailingSwitch", handleMailingSwitch) + + element.append(mailingSwitch.element) + + const divider4 = new Divider("Подписки:") + element.append(divider4.element) + + const subscription = await Admin.getSubscriptionMailing() + console.log("!!! MAILING", subscription) + + const subscriptionSwitch = new Switch(subscription.status, "Подписки", "subscriptionSwitch", handleSubscriptionSwitch) + const subscriptionTimeInput = new TimeInput(subscription.time ,"Время рассылки", "subscriptionTimeInput", handleSubscriptionTime) + const group = new Group('row', subscriptionTimeInput.element, subscriptionSwitch.element) + + element.append(group.element) + + this.element = element + return this + })(); + } + + static async getFeatures() { + const response = await fetch('/settings/', { + method: 'POST', + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + body: JSON.stringify({}) + }) + if (response.ok) { + return await response.json(); + } else { + alert("Не получили настройки :( Ошибка HTTP: " + response.status); + return {} + } + } + + static async getMailing() { + const response = await fetch('/settings/mailing', { + method: 'POST', + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + body: JSON.stringify({}) + }) + if (response.ok) { + return await response.json(); + } else { + alert("Не смогли получить рассылки :( Ошибка HTTP: " + response.status); + return {} + } + } + + static async getSubscriptionMailing() { + const response = await fetch('/settings/mailing/subscription', { + method: 'POST', + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + body: JSON.stringify({}) + }) + if (response.ok) { + return await response.json(); + } else { + alert("Не смогли получить рассылки :( Ошибка HTTP: " + response.status); + return {} + } + } + + static async getLogs() { + const response = await fetch('/settings/logs', { + method: 'POST', + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + body: JSON.stringify({}) + }) + if (response.ok) { + return await response.json(); + } else { + alert("Не смогли получить логи :( Ошибка HTTP: " + response.status); + return {} + } + } + + static async clearLogs() { + const response = await fetch('/settings/logs/clear', { + method: 'POST', + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + body: JSON.stringify({}) + }) + if (response.ok) { + return await response.json(); + } else { + alert("Не смогли очистить логи :( Ошибка HTTP: " + response.status); + return {} + } + } + + setStatusName(name) { + this.status.setName(name) + } +} + +class Features { + constructor (features) { + const element = document.createElement('div'); + Object.entries(features).forEach(([key, value]) => { + const { name, available, readonly } = value + const checkbox = new Checkbox(name, key, available, readonly); + element.append(checkbox.element); + + }) + + this.element = element; + } +} + +class Divider { + constructor (title) { + const element = document.createElement('div') + element.classList.add('divider'); + + const hr = document.createElement('hr') + hr.classList.add('divider_hr'); + element.append(hr); + + if (title) { + const h3 = document.createElement('h3'); + h3.classList.add('divider_h3'); + h3.innerHTML = title + element.append(h3); + } + + this.element = element; + } +} + +class Modal { + constructor (text, onClose) { + const element = document.createElement('div'); + element.classList.add('modal'); + + const content = document.createElement('div'); + content.classList.add('modal-content'); + element.append(content); + + const close = document.createElement('button'); + close.classList.add('modal-close'); + close.append("X"); + close.addEventListener( + 'click', + () => { + onClose(); + } + ) + content.append(close); + + const new_text = text.replaceAll("\\n", "") + const p = document.createElement('pre'); + p.classList.add('modal-text'); + p.append(new_text); + content.append(p); + + window.onclick = function(event) { + if (event.target == element) { + element.style.display = "none"; + } + } + + this.element = element; + } +} + +class Button { + constructor (name, uid, onClick) { + const element = document.createElement('button'); + element.append(name); + element.classList.add('button'); + element.setAttribute('data-name', name) + element.setAttribute('data-uid', uid) + element.addEventListener( + 'click', + () => { + onClick(this.element); + } + ) + this.element = element; + } +} + +class Group { + constructor (direction, ...elements) { + const div = document.createElement('div'); + div.classList.add('group'); + if (direction === 'column') { + div.classList.add('direction_column'); + } else if (direction === 'row') { + div.classList.add('direction_row'); + } + for (const element of elements) { + div.append(element) + } + + this.element = div; + } +} + +class Switch { + constructor (status, name, uid, onClick) { + const element = document.createElement('label'); + element.classList.add('switch'); + + const input = document.createElement('input'); + if (status === 'checked') { + input.setAttribute('checked', "true"); + } + input.setAttribute('type', "checkbox"); + input.setAttribute('data-name', name) + input.setAttribute('data-uid', uid) + input.addEventListener( + 'click', + () => { + onClick(input); + } + ) + const span = document.createElement('span'); + span.classList.add('slider'); + span.classList.add('round'); + + element.append(input); + element.append(span); + + this.element = element; + } +} + +class TimeInput { + constructor (time, name, uid, onBlur ) { + const element = document.createElement('input'); + element.setAttribute('id', uid); + element.setAttribute('type', "time"); + element.setAttribute('min', "00.00"); + element.setAttribute('max', "23.59"); + element.setAttribute('value', time) + element.classList.add('timeInput'); + + element.setAttribute('data-name', name) + element.setAttribute('data-uid', uid) + element.addEventListener( + 'blur', + () => { + onBlur(element); + } + ) + this.element = element; + } +} + +class Upload { + constructor (name, uid, onSubmit) { + const form = document.createElement('form'); + form.setAttribute('action', "/upload/"); + form.setAttribute('method', "post"); + form.setAttribute('enctype', "multipart/form-data"); + form.addEventListener('submit', onSubmit); + + const label = document.createElement('label'); + label.setAttribute('for', "file"); + label.append("Файл: "); + form.append(label); + + const input = document.createElement('input'); + input.setAttribute('id', "file"); + input.setAttribute('name', "file"); + input.setAttribute('type', "file"); + form.append(input); + + const button = document.createElement('button'); + button.classList.add('button'); + button.setAttribute('data-name', name); + button.setAttribute('data-uid', uid); + button.append(name); + this.button = button + form.append(button); + + this.element = form; + } +} + +class Checkbox { + constructor (name, uid, available, readonly) { + const element = document.createElement('div'); + const label = document.createElement('label'); + + const checkbox = document.createElement('input'); + checkbox.setAttribute('type', 'checkbox'); + checkbox.setAttribute('data-name', name); + checkbox.setAttribute('data-uid', uid); + checkbox.setAttribute('data-available', available); + if (available === 'true') { + checkbox.setAttribute('checked',''); + } + if (readonly === 'true') { + checkbox.setAttribute('disabled',''); + } + + checkbox.addEventListener( + 'change', + () => { + changeCheckbox(checkbox); + } + ) + + label.append(checkbox) + label.append(name) + + element.append(label) + + this.element = element; + } +} + +const admin = await new Admin(); +document + .getElementById('root') + .append(admin.element) + +function changeCheckbox(checkbox) { + console.log(checkbox.dataset.name, checkbox.checked, checkbox.dataset.uid) + const feature = FEATURES[checkbox.dataset.uid] + feature.available = checkbox.checked ? "true" : "false" +} + +function specialHandle (data, fetchComponent) { + fetch(fetchComponent, { + method: "POST", + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(result => { + console.log("result", result) + }) + .catch((error) => console.log(error)); +} + +function handleApplyButton (button) { + console.log("click", button.dataset.name, button.dataset.uid); + const data = { command: button.dataset.uid, features: FEATURES} + const fetchComponent = `/tasks/` + specialHandle(data, fetchComponent) +} + +function handleMailingSwitch (input) { + console.log("click", input.dataset.name, input.dataset.uid, input.checked) + const data = { status: input.checked} + const fetchComponent = `/settings/mailing/mailing_set` + specialHandle(data, fetchComponent) +} + +function handleSubscriptionSwitch (input) { + console.log("click", input.dataset.name, input.dataset.uid, input.checked) + const data = { status: input.checked} + const fetchComponent = `/settings/mailing/subscription/set` + specialHandle(data, fetchComponent) +} + +function handleSubscriptionTime (input) { + console.log("click", input.dataset.name, input.dataset.uid, input.checked) + const data = { time: input.value } + const fetchComponent = `/settings/mailing/subscription/set` + specialHandle(data, fetchComponent) +} + +function handleDownloadButton (button) { + console.log("click", button.dataset.name, button.dataset.uid); + window.location='./download'; +} + +function handleShowLogsButton (button) { + console.log("click", button.dataset.name, button.dataset.uid); + const modal = document.getElementsByClassName("modal")[0]; + modal.style.display = "block"; +} + +function handleClearLogsButton (button) { + console.log("click", button.dataset.name, button.dataset.uid); + Admin.clearLogs() +} + +function handleModalClose () { + console.log("click", "handleModalClose"); + const modal = document.getElementsByClassName("modal")[0]; + modal.style.display = "none"; +} + +/** @param {Event} event */ +function handleSubmit(event) { + /** @type {HTMLFormElement} */ + const form = event.currentTarget; + const url = new URL(form.action); + const formData = new FormData(form); + const searchParams = new URLSearchParams(formData); + /** @type {Parameters[1]} */ + const fetchOptions = { + method: form.method, + }; + if (form.method.toLowerCase() === 'post') { + if (form.enctype === 'multipart/form-data') { + fetchOptions.body = formData; + } else { + fetchOptions.body = searchParams; + } + } else { + url.search = searchParams; + } + fetch(url, fetchOptions) + .then(response => response.json()) + .then(result => { + alert(result.ok) + console.log("result", result) + }) + .catch((error) => console.log(error)); + event.preventDefault(); +} + +function ready(callback) { + if (document.readyState !== 'loading') { + callback(); + return; + } + document.addEventListener('DOMContentLoaded', callback); +} + +ready(function() { + console.log('document ready') +}) diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..7657a04 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,12 @@ + + + + + Настройка GameShop + + + + +
+ + diff --git a/backend/tests.py b/app/tests/tests.py similarity index 97% rename from backend/tests.py rename to app/tests/tests.py index 8ada249..53d8ee8 100644 --- a/backend/tests.py +++ b/app/tests/tests.py @@ -1,6 +1,5 @@ import unittest import psycopg2 -import json # Используем json.loads вместо eval from api import ( registration, add_product_to_basket, @@ -10,8 +9,8 @@ from api import ( unregister, add_money # Импортируем напрямую ) -from admin_api import add_product_to_shop, delete_product_from_shop -from const import DB_CONFIG +from api import add_product_to_shop, delete_product_from_shop +from consts import DB_CONFIG class TestFullUserFlow(unittest.TestCase): diff --git a/app/type/__init__.py b/app/type/__init__.py new file mode 100644 index 0000000..f1c9416 --- /dev/null +++ b/app/type/__init__.py @@ -0,0 +1 @@ +from .type import * \ No newline at end of file diff --git a/backend/type.py b/app/type/type.py similarity index 100% rename from backend/type.py rename to app/type/type.py diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..90f60fd --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1 @@ +from .utils import * \ No newline at end of file diff --git a/backend/utils.py b/app/utils/utils.py similarity index 87% rename from backend/utils.py rename to app/utils/utils.py index e299c54..b2636bc 100644 --- a/backend/utils.py +++ b/app/utils/utils.py @@ -1,9 +1,9 @@ +from decimal import Decimal + import bcrypt from datetime import date from psycopg2 import sql - -from const import * -from type import * +import json def fetch_table_as_json(cursor, table_name) -> list[dict]: cursor.execute(sql.SQL("SELECT * FROM {}").format(sql.Identifier(table_name))) @@ -49,3 +49,10 @@ def get_user_id(cursor, token: str) -> int: def get_product_by_id(cursor, product_id): fetch_row_as_json(cursor, "shop", product_id) + +def decimal_to_float(data: list[dict]): + clean_data = [ + {k: float(v) if isinstance(v, Decimal) else v for k, v in item.items()} + for item in data + ] + return clean_data diff --git a/backend/main.py b/backend/main.py deleted file mode 100644 index 382efb8..0000000 --- a/backend/main.py +++ /dev/null @@ -1,31 +0,0 @@ -from api import * -from utils import * - -if __name__ == "__main__": - # products_json = get_products() - # print(json.dumps(products_json, ensure_ascii=False, indent=2)) - # - # product1_json = get_product(1) - # print(json.dumps(product1_json, ensure_ascii=False, indent=2)) - - # try: - # # registration("Vasya1", "Vasya2005", "vasili_pupkin@gmail.com") - # token = login("vasili_pupkin@gmail.com", "Vasya2005") - # logout(token) - # print(check_token(token)) - - # token = login("stepan.pilipenko@xmail.ru", "sage123") - # registration("Petia", "Petia2005", "petia_pupkin@gmail.com") - # token = login("stepan.pilipenko@xmail.ru", "Vasya2005") - # print(get_basket(token)) - # print(get_history(token)) - - token = login("vasili_pupkin@gmail.com", "Vasya2005") - # add_product_to_basket(token, 3) - # add_product_to_basket(token, 3) - # add_product_to_basket(token, 5) - # add_product_to_basket(token, 5) - add_money() - # buy_products(token) - # print(get_histories(token)) - # unregister(token) diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index 5550361..0000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -psycopg2-binary -bcrypt \ No newline at end of file diff --git a/certs/localhost.crt b/certs/localhost.crt new file mode 100644 index 0000000..4237dcc --- /dev/null +++ b/certs/localhost.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDDzCCAfegAwIBAgIUf2BJSySSLLnH0Ac5wA7I28/qnPcwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDUwMzE5MTkyMFoXDTI0MDYw +MjE5MTkyMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA4EUNokJK5XVV1sa6E3iGPqQBwCn5K02X4ktZ5hktg8bo +3dpcgdrmdP6KEB1vPwcEOBmMEasE1bI/ocCiwHGC/yY+C+z+T8YSw5WUBCSQgPEj +xcxNt/AI8r5DZGTcqXt/5M9pEywPy0mjgwCogbe0R8MIL98PLqT6Bt3fIN1v8bjN +XySH04N2WJ/YU4SXdeI8QUy+ET4cTV0U9xC49T7knpYK9I1zGHeYvd+E2AJCYnLU +841qxZAr0O/5Oi0jgiYoltQi+u7ejQaqJJfp2+XYsgHhKpMBdJbyhNuE8KIZLhgU +8kFRejEHBgYJq9X6OyQ6ZRQxsQ18GrUrFq0kf2qZWwIDAQABo1kwVzAUBgNVHREE +DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB +MB0GA1UdDgQWBBRD48TR9xtQIhaDMHuqbXqH9lsgfzANBgkqhkiG9w0BAQsFAAOC +AQEAAAGRN5NEzE2eKpn1clBbHpFB3sOnxRQ5efVafz0U0XSCqyLlYU9IibGdk3Yk +huaslxUFGtPPlzPbRjHYP3ST5pZpCOuH7Ze/8AOaeNu+DFxILjD04wAGV881JPSB +Sw3wrxk0hWMMwWTCTnsaDIeLOejhf86CB1gH3UdvZzcxGIanXtyb86wcnkj107+B +5Pv6Q8J0Thn2gMjisVqybdqrQi7tAtDURefyoPuRxgK2k6kgTBSMPXxRqWvq6Aem +ozxYrT7/QV0gKXjCNTmhFjEOOIGdN8h/Rmih7/TbcPpaEHcsVwG2w8eRKvJ2xrIk +dHL+Xn35P1jTwxwRq1vfLhEIkg== +-----END CERTIFICATE----- diff --git a/certs/localhost.key b/certs/localhost.key new file mode 100644 index 0000000..8ae2cfe --- /dev/null +++ b/certs/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDgRQ2iQkrldVXW +xroTeIY+pAHAKfkrTZfiS1nmGS2Dxujd2lyB2uZ0/ooQHW8/BwQ4GYwRqwTVsj+h +wKLAcYL/Jj4L7P5PxhLDlZQEJJCA8SPFzE238AjyvkNkZNype3/kz2kTLA/LSaOD +AKiBt7RHwwgv3w8upPoG3d8g3W/xuM1fJIfTg3ZYn9hThJd14jxBTL4RPhxNXRT3 +ELj1PuSelgr0jXMYd5i934TYAkJictTzjWrFkCvQ7/k6LSOCJiiW1CL67t6NBqok +l+nb5diyAeEqkwF0lvKE24TwohkuGBTyQVF6MQcGBgmr1fo7JDplFDGxDXwatSsW +rSR/aplbAgMBAAECggEAAsPXLzDyC3Iu5L7+fE74GL2c5+mckNQcE0YqjZOx4/YH +2PRgP5mbTcX1nc0/Gd/URXzLJUyeeqP/9NaTKxw7KTonea4qVIF0qcSmVoa84VoX +vtCOBL6I3bVKz2oO7mf/Y6rK+NmKOE9oHK1dZnwFVP0qlKyDW2fdTUhe/+C7CJfg +x8KXNQ66cky5VaM+/Oqqaxs866XI9sjUr234YRarwgIwoP1hVrVfOaKJounbqHtS +HCwyWlvk1JkmGNEnPIH8HlkTPfh1Q1ifK3t8qKFfnj6ITarbZildsPJD9P/eN8E5 +oujw0eUi1YOkWRI91ecCTVqvFNh35nRFF9fDchRygQKBgQDxCWqaPDfmQIQxRoaX +z2UkXZZfQ2I/CdmVT1TrgpMndyzYI1KC93VdDCDZlWMCEOyfN2qDg/bSbQFCWlUJ +m+fgZd2ovrtyx7AL4P808mVgR0+ZcW6+48ZI5TKs/UI+m/WBy+QMedPcObhx26CA +MqdyLw3nAU5u9M1beQ+F34XGQwKBgQDuMSxQjc+FoT38exDPcLyrmvMxiJDl9F7q +BzWGxp2HtgBxLRvI0E7tb0kJ2ZgZjq3xdbPYgYVUKkaSxDNU7/63nE6vw+ZbW42V +XBnt55c0GixnV9LQLv0Qz9Tp7RfdOZlDQ/azoLHY2XWOt08rac3TgHwD3YNALrkQ +NrDa30tLCQKBgFsCTC7qN80HZSJZ163wT+cYMxPLFIhqxq3ao1y9E6TeGZ+OTrRG +jRjR4IFnJ1f7XeyL9vqrVAGFyOjtxJf5NucCb1wskAg5n54MmS+7qk1c/5AXRVJs +HE0fxS+N/Ho5VsxoWLXhNf48CQlsfMCK37B8Vcp4Ms4wPm2gWx0YFaGTAoGBAN6i +9cHhm0xTV4YMXb0XqjJYZeIxvQZDsQfcbyqnsQztkGI5AJRmKLAD6egcC/AvjeR4 +2P6QqdfuoAKFA1nr7VEf9+iQGlvgKmmmKdJWOt2HbWO3EiRnF0HEkUWJyFmOgfP+ +rbRein2fXSNlsclpXurHWKOgRBMU2QQPqqUaO91JAoGBAM5EcSu1uZkMs2urRKJY +SDO0N13Dm1rY3t+nInrBk6j4AxEuwdF5k1lAevmEPRNKXSqh0JjInGPQij+Z3Brp +kLCubqZKTRmXkhMkI8p++d0aW5voSo4E1Fr4mWrK/WakMHMVcwVRyqTmmXYyY9v/ +FMNjTyw8P7HAaUuhdazp/pzM +-----END PRIVATE KEY----- diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..b81ba45 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,10 @@ +services: + web: + platform: linux/arm64/v8 + image: firstsoftwarrior/game_shop + build: + context: app + target: builder + container_name: firstsoftwarrior-game_shop + ports: + - '8090:8090'