commit e54e57ca3d94d9674a8f6a254087aab00945f148 Author: saddydead1 Date: Mon Jul 14 18:32:55 2025 +0300 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86c2271 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/client/.venv +/server/.venv +.key +.auth_data \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4441c81 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a000a19 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sonoma-app.iml b/.idea/sonoma-app.iml new file mode 100644 index 0000000..2793d72 --- /dev/null +++ b/.idea/sonoma-app.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..bbe9e04 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "associatedIndex": 4 +} + + + + { + "keyToString": { + "Docker.server/docker-compose.yaml.postgres: Compose Deployment.executor": "Run", + "Docker.server/docker-compose.yaml: Compose Deployment.executor": "Run", + "ModuleVcsDetector.initialDetectionPerformed": "true", + "Python.client run.executor": "Run", + "Python.main.executor": "Run", + "Python.start client.executor": "Run", + "Python.start main server.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.git.unshallow": "true", + "git-widget-placeholder": "main", + "last_opened_file_path": "/Users/saddy/Documents/projects/sonoma-app/server/main-server", + "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1751252361654 + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e465190 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Sonoma App \ No newline at end of file diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..b0c602a --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,163 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +*.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Flet +storage/ \ No newline at end of file diff --git a/client/poetry.toml b/client/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/client/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/client/pyproject.toml b/client/pyproject.toml new file mode 100644 index 0000000..631380a --- /dev/null +++ b/client/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "sonoma-app-client" +version = "0.1.0" +description = "" +requires-python = ">=3.12" +authors = [ + { name = "SaddyDEAD", email = "saddydead@sonoma.su" } +] +dependencies = [ + "flet==0.28.3", + "cryptography", + "httpx (>=0.28.1,<0.29.0)" +] + +[tool.flet] +org = "su.sonoma" + +product = "SApp" + +company = "Sonoma" + +copyright = "Copyright (C) 2025 by Sonoma Org." + +[tool.flet.app] +path = "src" + +[tool.uv] +dev-dependencies = [ + "flet[all]==0.28.3", +] + +[tool.poetry] +package-mode = false + +[tool.poetry.group.dev.dependencies] +flet = {extras = ["all"], version = "0.28.3"} \ No newline at end of file diff --git a/client/src/assets/sonoma.png b/client/src/assets/sonoma.png new file mode 100644 index 0000000..41e92b9 Binary files /dev/null and b/client/src/assets/sonoma.png differ diff --git a/client/src/auth/login.py b/client/src/auth/login.py new file mode 100644 index 0000000..209e106 --- /dev/null +++ b/client/src/auth/login.py @@ -0,0 +1,28 @@ +from cryptography.fernet import Fernet +import os +import httpx + +KEY_FILE = ".key" +REMEMBER_FILE = ".auth_data" + +def generate_key(): + if not os.path.exists(KEY_FILE): + key = Fernet.generate_key() + with open(KEY_FILE, "wb") as f: + f.write(key) + + +async def load_fernet(): + with open(KEY_FILE, "rb") as f: + key = f.read() + return Fernet(key) + + +async def login(name: str, password: str): + async with httpx.AsyncClient() as client: + r = await client.post(f'http://127.0.0.1:7535/login?name={name}&password={password}') + + if r.is_success: + return True + else: + return False \ No newline at end of file diff --git a/client/src/main.py b/client/src/main.py new file mode 100644 index 0000000..05c0ddf --- /dev/null +++ b/client/src/main.py @@ -0,0 +1,102 @@ +import flet as ft +from auth.login import * + +USERNAME = "admin" +PASSWORD = "1234" + + +def generate_key(): + if not os.path.exists(KEY_FILE): + key = Fernet.generate_key() + with open(KEY_FILE, "wb") as f: + f.write(key) + + +async def load_fernet(): + with open(KEY_FILE, "rb") as f: + key = f.read() + return Fernet(key) + + +async def main(page: ft.Page): + generate_key() + fernet = await load_fernet() + + page.title = "SClient" + page.window_width = 400 + page.window_height = 300 + page.vertical_alignment = ft.MainAxisAlignment.CENTER + + login_input = ft.TextField(label="Логин", autofocus=True) + password_input = ft.TextField(label="Пароль", password=True, can_reveal_password=True) + remember_checkbox = ft.Checkbox(label="Запомнить меня") + status_text = ft.Text(color=ft.Colors.RED) + + if os.path.exists(REMEMBER_FILE): + try: + with open(REMEMBER_FILE, "rb") as f: + lines = f.readlines() + if len(lines) == 2: + saved_login = lines[0].decode().strip() + encrypted_pw = lines[1].strip() + decrypted_pw = fernet.decrypt(encrypted_pw).decode() + + login_input.value = saved_login + password_input.value = decrypted_pw + remember_checkbox.value = True + except Exception as e: + print(f"[ОШИБКА] Не удалось прочитать данные: {e}") + + async def login_click(e): + if await login(login_input.value, password_input.value): + if remember_checkbox.value: + encrypted_pw = fernet.encrypt(password_input.value.encode()) + with open(REMEMBER_FILE, "wb") as f: + f.write(f"{login_input.value}\n".encode()) + f.write(encrypted_pw) + else: + if os.path.exists(REMEMBER_FILE): + os.remove(REMEMBER_FILE) + + page.clean() + page.add( + ft.Column( + [ + ft.Text(f"Добро пожаловать, {USERNAME}!", size=24), + ft.ElevatedButton("Выйти", on_click=lambda e: page.go("/")) + ], + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ) + ) + else: + status_text.value = "Неверный логин или пароль" + page.update() + + page.add( + ft.Container( + content=ft.Column( + controls=[ + ft.Image( + src="sonoma.png", + width=50, + height=50, + fit=ft.ImageFit.CONTAIN + ), + ft.Text("SClient", size=24, weight=ft.FontWeight.BOLD), + login_input, + password_input, + remember_checkbox, + ft.ElevatedButton("Войти", on_click=login_click), + status_text + ], + width=300, + alignment=ft.MainAxisAlignment.CENTER, + horizontal_alignment=ft.CrossAxisAlignment.CENTER + ), + alignment=ft.alignment.center, + expand=True + ) + ) + +ft.app(main) diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml new file mode 100644 index 0000000..161c28f --- /dev/null +++ b/server/docker-compose.yaml @@ -0,0 +1,20 @@ +version: '3.9' + +services: + postgres: + image: postgres:latest + container_name: postgres_container + environment: + POSTGRES_USER: ADMIN + POSTGRES_PASSWORD: 123123 + POSTGRES_DB: sonoma-db + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data/pgdata + restart: unless-stopped + tty: true + stdin_open: true + +volumes: + pgdata: \ No newline at end of file diff --git a/server/main-server/.gitignore b/server/main-server/.gitignore new file mode 100644 index 0000000..8dd4cbf --- /dev/null +++ b/server/main-server/.gitignore @@ -0,0 +1,164 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.idea + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +*.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Flet +storage/ \ No newline at end of file diff --git a/server/main-server/poetry.toml b/server/main-server/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/server/main-server/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/server/main-server/pyproject.toml b/server/main-server/pyproject.toml new file mode 100644 index 0000000..4294b1c --- /dev/null +++ b/server/main-server/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "sclient-server-main" +version = "0.1.0" +description = "" +authors = [ + {name = "SaddyDEAD",email = "saddydead@sonoma.su"} +] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi (>=0.115.14,<0.116.0)", + "asyncpg (>=0.30.0,<0.31.0)", + "uvicorn (>=0.35.0,<0.36.0)", + "fastapi-users[sqlalchemy] (>=14.0.1,<15.0.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/server/main-server/src/auth/transport.py b/server/main-server/src/auth/transport.py new file mode 100644 index 0000000..5333fd6 --- /dev/null +++ b/server/main-server/src/auth/transport.py @@ -0,0 +1,27 @@ +import uuid +from fastapi_users import FastAPIUsers, models +from fastapi_users.authentication import ( + AuthenticationBackend, + BearerTransport, + JWTStrategy, +) +from fastapi_users.jwt import SecretType + +from src.database.db import Database +from src.database.user import User + +class Transport: + def __init__(self, secret: SecretType, db: Database): + self.secret = secret + self.db = db + self.bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") + self.auth_backend = AuthenticationBackend( + name="jwt", + transport=self.bearer_transport, + get_strategy=self.get_jwt_strategy, + ) + self.fastapi_users = FastAPIUsers[User, uuid.UUID](self.db.get_user_manager, [self.auth_backend]) + self.current_active_user = self.fastapi_users.current_user(active=True) + + def get_jwt_strategy(self) -> JWTStrategy[models.UP, models.ID]: + return JWTStrategy(secret=self.secret, lifetime_seconds=3600) diff --git a/server/main-server/src/auth/user_manager.py b/server/main-server/src/auth/user_manager.py new file mode 100644 index 0000000..9e79f8a --- /dev/null +++ b/server/main-server/src/auth/user_manager.py @@ -0,0 +1,30 @@ +import uuid +from typing import Optional +from fastapi import Request +from fastapi_users import UUIDIDMixin, BaseUserManager +from fastapi_users.jwt import SecretType + +from src.database.user import User + +SECRET: SecretType + +class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): + + def __init__(self, secret: SecretType): + super().__init__() + + reset_password_token_secret = SECRET + verification_token_secret = SECRET + + async def on_after_register(self, user: User, request: Optional[Request] = None): + print(f"User {user.id} has registered.") + + async def on_after_forgot_password( + self, user: User, token: str, request: Optional[Request] = None + ): + print(f"User {user.id} has forgot their password. Reset token: {token}") + + async def on_after_request_verify( + self, user: User, token: str, request: Optional[Request] = None + ): + print(f"Verification requested for user {user.id}. Verification token: {token}") \ No newline at end of file diff --git a/server/main-server/src/database/db.py b/server/main-server/src/database/db.py new file mode 100644 index 0000000..fa85ddb --- /dev/null +++ b/server/main-server/src/database/db.py @@ -0,0 +1,40 @@ +from collections.abc import AsyncGenerator +from fastapi import Depends +from fastapi_users.db import SQLAlchemyUserDatabase +from fastapi_users.jwt import SecretType +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +from src.auth.user_manager import UserManager +from src.database.user import User, Base + + +class Database: + def __init__( + self, + db_user: str, + db_pass: str, + db_host: str, + db_port: int, + db_name: str, + secret: SecretType + ): + self.DATABASE_URL = f'postgresql+asyncpg://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}' + self.engine = create_async_engine(self.DATABASE_URL) + self.async_session_maker = async_sessionmaker(self.engine, expire_on_commit=False) + self.secret = secret + + async def create_db_and_tables(self): + async with self.engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + + async def get_async_session(self) -> AsyncGenerator[AsyncSession, None]: + async with self.async_session_maker() as session: + yield session + + + async def get_user_db(self, session: AsyncSession = Depends(get_async_session)): + yield SQLAlchemyUserDatabase(session, User) + + async def get_user_manager(self, user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): + yield UserManager(self.secret, user_db) diff --git a/server/main-server/src/database/role.py b/server/main-server/src/database/role.py new file mode 100644 index 0000000..42da439 --- /dev/null +++ b/server/main-server/src/database/role.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class Role(Enum): + ADMIN = 'Admin' + USER = 'User' diff --git a/server/main-server/src/database/schemas.py b/server/main-server/src/database/schemas.py new file mode 100644 index 0000000..d715622 --- /dev/null +++ b/server/main-server/src/database/schemas.py @@ -0,0 +1,15 @@ +import uuid + +from fastapi_users import schemas + + +class UserRead(schemas.BaseUser[uuid.UUID]): + pass + + +class UserCreate(schemas.BaseUserCreate): + pass + + +class UserUpdate(schemas.BaseUserUpdate): + pass \ No newline at end of file diff --git a/server/main-server/src/database/user.py b/server/main-server/src/database/user.py new file mode 100644 index 0000000..f99eabd --- /dev/null +++ b/server/main-server/src/database/user.py @@ -0,0 +1,13 @@ +from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID +from sqlalchemy.orm import DeclarativeBase, Mapped +from src.database.role import Role + + +class Base(DeclarativeBase): + pass + +class User(SQLAlchemyBaseUserTableUUID, Base): + username: Mapped[str] + role: Mapped[Role] + vpn_server_access: Mapped[bool] + main_server_access: Mapped[bool] \ No newline at end of file diff --git a/server/main-server/src/main.py b/server/main-server/src/main.py new file mode 100644 index 0000000..1f9ac5f --- /dev/null +++ b/server/main-server/src/main.py @@ -0,0 +1,91 @@ +import asyncio +from fastapi import Depends, FastAPI, HTTPException, Request +from fastapi.responses import RedirectResponse +from database.db import Database +import uvicorn +from src.database.schemas import * +from src.auth.transport import Transport +from src.database.user import User + +app = FastAPI(title='sclient-main-server') + +### Settings +# TODO: Create .env + +PORT = 7535 +ADMIN_NAME = 'admin' +ADMIN_PASSWORD = 'admin' +DATABASE_USER = 'ADMIN' +DATABASE_PASS = '123123' +DATABASE_HOST = '127.0.0.1' +DATABASE_PORT = 5432 +DATABASE_NAME = 'sonoma-db' +SECRET = 'SECRET' + +### + +db = Database( + DATABASE_USER, + DATABASE_PASS, + DATABASE_HOST, + DATABASE_PORT, + DATABASE_NAME, + SECRET + ) + +transport = Transport(SECRET, db) + +class App: + def init(self, loop) -> None: + config = uvicorn.Config( + app, + loop=loop, + host='0.0.0.0', + port=PORT + ) + server = uvicorn.Server(config) + loop.run_until_complete(server.serve()) + + @app.get('/') + async def docs(self: Request): + return RedirectResponse(f'{self.url}docs') + + @app.get("/authenticated-route") + async def authenticated_route(user: User = Depends(transport.current_active_user)): + return {"message": f"Hello {user.email}!"} + +def main(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + server = App() + + app.include_router( + transport.fastapi_users.get_auth_router(transport.auth_backend), prefix="/auth/jwt", tags=["auth"] + ) + app.include_router( + transport.fastapi_users.get_register_router(UserRead, UserCreate), + prefix="/auth", + tags=["auth"], + ) + app.include_router( + transport.fastapi_users.get_reset_password_router(), + prefix="/auth", + tags=["auth"], + ) + app.include_router( + transport.fastapi_users.get_verify_router(UserRead), + prefix="/auth", + tags=["auth"], + ) + app.include_router( + transport.fastapi_users.get_users_router(UserRead, UserUpdate), + prefix="/users", + tags=["users"], + ) + + loop.run_until_complete(db.create_db_and_tables()) + server.init(loop) + +if __name__ == '__main__': + main() \ No newline at end of file