[rewrite] Major changes to code - switched to postgres, added initial backend docker files
This commit is contained in:
parent
c7e20fc935
commit
71e916586e
51
.env-example
Normal file
51
.env-example
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Port for the app to run on
|
||||||
|
# PORT=31714
|
||||||
|
|
||||||
|
# Secret key used for cryptographic operations. Must be a long, random string.
|
||||||
|
SECRET_KEY=
|
||||||
|
|
||||||
|
# Token expiration time in minutes. Default is 8 days (60 min * 24 hours * 8 days).
|
||||||
|
# ACCESS_TOKEN_EXPIRE_MINUTES=11520
|
||||||
|
|
||||||
|
# The environment in which the application is running.
|
||||||
|
# Options: local, staging, production
|
||||||
|
ENVIRONMENT=local
|
||||||
|
|
||||||
|
# The frontend host that interacts with the backend.
|
||||||
|
FRONTEND_HOST=
|
||||||
|
|
||||||
|
# CORS origins allowed to access the backend.
|
||||||
|
# Multiple values should be comma-separated, e.g., "http://localhost,http://example.com"
|
||||||
|
BACKEND_CORS_ORIGINS=
|
||||||
|
|
||||||
|
# MySQL database configuration
|
||||||
|
POSTGRES_SERVER=
|
||||||
|
POSTGRES_PORT=3306
|
||||||
|
POSTGRES_USER=
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
POSTGRES_DB=
|
||||||
|
|
||||||
|
# SMTP configuration for sending emails
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_TLS=True # Use TLS for email security
|
||||||
|
SMTP_SSL=False # Set to True if using SSL instead of TLS
|
||||||
|
|
||||||
|
# Email sender information
|
||||||
|
EMAILS_FROM_EMAIL=
|
||||||
|
EMAILS_FROM_NAME=
|
||||||
|
|
||||||
|
# Expiration time for password reset tokens (in hours)
|
||||||
|
# EMAIL_RESET_TOKEN_EXPIRE_HOURS=48
|
||||||
|
|
||||||
|
# A test user email used for automated email testing
|
||||||
|
EMAIL_TEST_USER=test@example.com
|
||||||
|
|
||||||
|
# Superuser credentials for the first administrator
|
||||||
|
FIRST_SUPERUSER=
|
||||||
|
FIRST_SUPERUSER_PASSWORD=
|
||||||
|
|
||||||
|
DOCKER_IMAGE_BACKEND=backend
|
||||||
|
DOCKER_IMAGE_FRONTEND=frontend
|
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@ -8,7 +8,13 @@
|
|||||||
"cwd": "${workspaceFolder}/backend",
|
"cwd": "${workspaceFolder}/backend",
|
||||||
"module": "fastapi",
|
"module": "fastapi",
|
||||||
"args": ["dev", "${cwd}/backend/app/main.py"],
|
"args": ["dev", "${cwd}/backend/app/main.py"],
|
||||||
"console": "internalConsole"
|
"console": "internalConsole",
|
||||||
|
"serverReadyAction":{
|
||||||
|
"action": "openExternally",
|
||||||
|
"killOnServerStop": false,
|
||||||
|
"pattern": "Application startup complete.",
|
||||||
|
"uriFormat": "http://localhost:8000/docs"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Frontend: Dev",
|
"name": "Frontend: Dev",
|
||||||
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -2,9 +2,9 @@
|
|||||||
// #region Backend settings
|
// #region Backend settings
|
||||||
"files.exclude" : { "**/__pycache__/**": true }, // Hide __pycache__ directories
|
"files.exclude" : { "**/__pycache__/**": true }, // Hide __pycache__ directories
|
||||||
"mypy-type-checker.args" : ["--config-file='backend/mypy.ini'"], // Override mypy config
|
"mypy-type-checker.args" : ["--config-file='backend/mypy.ini'"], // Override mypy config
|
||||||
"python.defaultInterpreterPath": "./backend/.venv/bin/python", // Use venv by default
|
"python.defaultInterpreterPath" : "./backend/.venv/bin/python", // Use venv by default
|
||||||
"python.analysis.extraPaths" : ["./backend"], // Pylint - fix for import analysis
|
"python.analysis.extraPaths" : ["./backend"], // Pylint - fix for import analysis
|
||||||
"pylint.cwd": "${workspaceFolder}/backend",
|
"pylint.cwd" : "${workspaceFolder}/backend",
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Frontend settings
|
// #region Frontend settings
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
# TODO Fill me up
|
|
||||||
|
|
||||||
HOST=
|
|
||||||
PORT=
|
|
||||||
|
|
||||||
MYSQL_USER=
|
|
||||||
MYSQL_DATABASE=
|
|
||||||
MYSQL_HOST=
|
|
||||||
MYSQL_PORT=
|
|
||||||
MYSQL_PASSWORD=
|
|
||||||
|
|
||||||
REDIS_HOST=
|
|
||||||
REDIS_PORT=
|
|
||||||
|
|
||||||
JWT_SECRET_KEY=
|
|
||||||
|
|
||||||
MAIL_SERVER=
|
|
||||||
MAIL_PORT=
|
|
||||||
MAIL_USERNAME=
|
|
||||||
MAIL_PASSWORD=
|
|
||||||
MAIL_USE_TLS=
|
|
||||||
MAIL_DEFAULT_SENDER=
|
|
25
backend/Dockerfile
Normal file
25
backend/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Base Image
|
||||||
|
FROM python:3.13
|
||||||
|
|
||||||
|
RUN pip install poetry
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app/
|
||||||
|
|
||||||
|
# Copy dependency files first to leverage caching
|
||||||
|
COPY pyproject.toml poetry.lock /app/
|
||||||
|
|
||||||
|
# Copy the rest of the application
|
||||||
|
COPY ./app /app/app
|
||||||
|
|
||||||
|
# Ensure dependencies are installed correctly
|
||||||
|
RUN poetry install --no-interaction --no-ansi --without dev
|
||||||
|
|
||||||
|
# Expose port for FastAPI
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Command to run the app
|
||||||
|
CMD ["poetry", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
|
10
backend/app/api/__init__.py
Normal file
10
backend/app/api/__init__.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.api.routes import cart_routes, shop_routes, user_routes, utils_routes
|
||||||
|
|
||||||
|
api_router = APIRouter()
|
||||||
|
|
||||||
|
api_router.include_router(cart_routes.router)
|
||||||
|
api_router.include_router(shop_routes.router)
|
||||||
|
api_router.include_router(user_routes.router)
|
||||||
|
api_router.include_router(utils_routes.router)
|
48
backend/app/api/dependencies.py
Normal file
48
backend/app/api/dependencies.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from jwt.exceptions import InvalidTokenError
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core import security
|
||||||
|
|
||||||
|
from app.database.manager import get_session
|
||||||
|
from app.database.models.user_model import User
|
||||||
|
|
||||||
|
from app.schemas.user_schemas import TokenPayload
|
||||||
|
|
||||||
|
|
||||||
|
reusable_oauth2 = OAuth2PasswordBearer(
|
||||||
|
tokenUrl="/login/access-token"
|
||||||
|
)
|
||||||
|
|
||||||
|
SessionDep = Annotated[Session, Depends(get_session)]
|
||||||
|
TokenDep = Annotated[str, Depends(reusable_oauth2)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user(session: SessionDep, token: TokenDep) -> User:
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
|
||||||
|
)
|
||||||
|
token_data = TokenPayload(**payload)
|
||||||
|
except (InvalidTokenError, ValidationError):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
)
|
||||||
|
user = session.get(User, {"uuid": token_data.sub})
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
if not user.is_active:
|
||||||
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
CurrentUser = Annotated[User, Depends(get_current_user)]
|
37
backend/app/api/routes/login_routes.py
Normal file
37
backend/app/api/routes/login_routes.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
|
||||||
|
from app.api.dependencies import SessionDep
|
||||||
|
|
||||||
|
from app.schemas.user_schemas import Token
|
||||||
|
|
||||||
|
from app.core import security
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
router = APIRouter(tags=["login"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login/access-token")
|
||||||
|
def login_access_token(
|
||||||
|
session: SessionDep, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||||
|
) -> Token:
|
||||||
|
"""
|
||||||
|
OAuth2 compatible token login, get an access token for future requests
|
||||||
|
"""
|
||||||
|
user = None
|
||||||
|
# user = crud.authenticate(
|
||||||
|
# session=session, email=form_data.username, password=form_data.password
|
||||||
|
# )
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||||
|
elif not user:
|
||||||
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
return Token(
|
||||||
|
access_token=security.create_access_token(
|
||||||
|
user.id, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
)
|
@ -31,6 +31,7 @@ async def login(login_data: UserLoginSchema):
|
|||||||
# )
|
# )
|
||||||
# return Token(access_token=access_token, token_type="bearer")
|
# return Token(access_token=access_token, token_type="bearer")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/logout", summary="User logout")
|
@router.delete("/logout", summary="User logout")
|
||||||
async def logout():
|
async def logout():
|
||||||
raise NotImplementedError("logout() needs to be implemented.")
|
raise NotImplementedError("logout() needs to be implemented.")
|
||||||
@ -38,8 +39,11 @@ async def logout():
|
|||||||
|
|
||||||
@router.post("/register", summary="Register new user")
|
@router.post("/register", summary="Register new user")
|
||||||
async def register(user_data: UserRegisterSchema):
|
async def register(user_data: UserRegisterSchema):
|
||||||
create_user(user_data)
|
try:
|
||||||
return {"message": "User registered successfully"}
|
create_user(user_data)
|
||||||
|
return {"message": "User registered successfully"}
|
||||||
|
except BaseException:
|
||||||
|
return {"message": "An error occured"}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/update", summary="Update user details")
|
@router.put("/update", summary="Update user details")
|
20
backend/app/api/routes/utils_routes.py
Normal file
20
backend/app/api/routes/utils_routes.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from sqlmodel import select
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from app.api.dependencies import SessionDep
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/utils", tags=["utils"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health-check/")
|
||||||
|
async def health_check() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@router.get("/test-db/")
|
||||||
|
async def test_db(session: SessionDep) -> bool:
|
||||||
|
try:
|
||||||
|
with session:
|
||||||
|
session.exec(select(1))
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
@ -1,3 +0,0 @@
|
|||||||
from .config import *
|
|
||||||
|
|
||||||
__all__ = [*config.__all__]
|
|
@ -1,205 +1,107 @@
|
|||||||
import os
|
import secrets
|
||||||
|
import warnings
|
||||||
import logging
|
import logging
|
||||||
import re
|
from typing import Annotated, Any, Literal
|
||||||
from typing import Any, Type, Optional, Callable
|
|
||||||
|
|
||||||
import dotenv
|
from pydantic import (
|
||||||
|
AnyUrl,
|
||||||
from .exceptions import ConfigError
|
BeforeValidator,
|
||||||
|
EmailStr,
|
||||||
|
PostgresDsn,
|
||||||
|
computed_field,
|
||||||
|
model_validator,
|
||||||
|
)
|
||||||
|
from pydantic_core import MultiHostUrl
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
||||||
class EnvConfigField():
|
def parse_cors(v: Any) -> list[str] | str:
|
||||||
"""
|
if isinstance(v, str) and not v.startswith("["):
|
||||||
Represents a single config field to be parsed from the environment.
|
return [i.strip() for i in v.split(",")]
|
||||||
|
elif isinstance(v, list | str):
|
||||||
|
return v
|
||||||
|
raise ValueError(v)
|
||||||
|
|
||||||
:param env_name: Name of the environment variable
|
|
||||||
:param data_type: Expected data type (int, str, etc.)
|
|
||||||
:param default: Default value if not set in environment
|
|
||||||
:param required: Whether the variable is required
|
|
||||||
:param choices: List of allowed values (for enums)
|
|
||||||
:param min_value: Minimum value (for integers)
|
|
||||||
:param max_value: Maximum value (for integers)
|
|
||||||
:param regex: Regex pattern (for string validation)
|
|
||||||
:param parser: Custom parser function for complex validation
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
class Settings(BaseSettings):
|
||||||
self,
|
model_config = SettingsConfigDict(
|
||||||
env_name: str,
|
env_file="../.env",
|
||||||
data_type: Type[Any] = str,
|
env_ignore_empty=True,
|
||||||
default: Any = None,
|
extra="ignore",
|
||||||
required: bool = False,
|
)
|
||||||
choices: Optional[list[Any]] = None,
|
PORT: int = 8000
|
||||||
min_value: Optional[int] = None,
|
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||||
max_value: Optional[int] = None,
|
|
||||||
regex: Optional[str] = None,
|
|
||||||
parser: Optional[Callable[[str], Any]] = None,
|
|
||||||
):
|
|
||||||
self.env_value = None
|
|
||||||
self.env_name = env_name
|
|
||||||
self.data_type = data_type
|
|
||||||
self.default = default
|
|
||||||
self.required = required
|
|
||||||
self.choices = choices
|
|
||||||
self.min_value = min_value
|
|
||||||
self.max_value = max_value
|
|
||||||
self.regex = re.compile(regex) if regex else None
|
|
||||||
self.parser = parser
|
|
||||||
|
|
||||||
def load(self):
|
VERBOSITY: Literal[*logging.getLevelNamesMapping().keys()] = "DEBUG"
|
||||||
"""Loads the value from the environment and validates it."""
|
|
||||||
raw_value = os.environ.get(self.env_name)
|
|
||||||
|
|
||||||
if self.required and (raw_value is None or raw_value == ""):
|
# 60 minutes * 24 hours * 8 days = 8 days
|
||||||
raise ConfigError(
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
||||||
"Field %(env_name)s is required, but was not provided in the environment / .env",
|
FRONTEND_HOST: str
|
||||||
env_name=self.env_name,
|
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
|
||||||
)
|
|
||||||
|
|
||||||
self.env_value = self.validate(raw_value)
|
BACKEND_CORS_ORIGINS: Annotated[
|
||||||
|
list[AnyUrl] | str, BeforeValidator(parse_cors)
|
||||||
def validate(self, value: Any) -> Any:
|
] = []
|
||||||
"""Validates the value based on provided constraints."""
|
|
||||||
|
|
||||||
# Use the default value if available and skip validation
|
|
||||||
if (value is None or value == "") and self.default is not None:
|
|
||||||
return self.default
|
|
||||||
|
|
||||||
# Use custom parser if provided
|
|
||||||
if self.parser:
|
|
||||||
value = self.parser(value)
|
|
||||||
|
|
||||||
# Convert value to expected data type
|
|
||||||
if self.data_type == str:
|
|
||||||
value = self.__parse_to_str(value)
|
|
||||||
elif self.data_type == int:
|
|
||||||
value = self.__parse_to_int(value)
|
|
||||||
elif self.data_type == bool:
|
|
||||||
value = self.__parse_to_bool(value)
|
|
||||||
|
|
||||||
# Validate allowed choices
|
|
||||||
if self.choices and value not in self.choices:
|
|
||||||
raise ConfigError(
|
|
||||||
"Value for %(env_name)s - %(value)s is not one of the following allowed values: %(allowed_values)s",
|
|
||||||
env_name=self.env_name,
|
|
||||||
value=value,
|
|
||||||
allowed_values=", ".join(map(str, self.choices)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def all_cors_origins(self) -> list[str]:
|
||||||
return self.env_value
|
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [self.FRONTEND_HOST]
|
||||||
|
|
||||||
@value.setter
|
POSTGRES_SERVER: str
|
||||||
def value(self, new):
|
POSTGRES_PORT: int = 5432
|
||||||
raise TypeError("Cannot modify")
|
POSTGRES_USER: str
|
||||||
|
POSTGRES_PASSWORD: str = ""
|
||||||
|
POSTGRES_DB: str = ""
|
||||||
|
|
||||||
def __parse_to_str(self, value) -> str:
|
@computed_field
|
||||||
"""Parses and validates string values."""
|
@property
|
||||||
value = str(value)
|
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
|
||||||
|
return MultiHostUrl.build(
|
||||||
|
scheme='postgresql+psycopg2',
|
||||||
|
username=self.POSTGRES_USER,
|
||||||
|
password=self.POSTGRES_PASSWORD,
|
||||||
|
host=self.POSTGRES_SERVER,
|
||||||
|
port=self.POSTGRES_PORT,
|
||||||
|
path=self.POSTGRES_DB,
|
||||||
|
)
|
||||||
|
|
||||||
if self.regex and not self.regex.match(value):
|
SMTP_TLS: bool = True
|
||||||
raise ConfigError(
|
SMTP_SSL: bool = False
|
||||||
"Value for %(env_name)s - %(value)s does not match the expected pattern: %(regex)s",
|
SMTP_PORT: int = 587
|
||||||
env_name=self.env_name,
|
SMTP_HOST: str | None = None
|
||||||
value=value,
|
SMTP_USER: str | None = None
|
||||||
regex=self.regex.pattern,
|
SMTP_PASSWORD: str | None = None
|
||||||
)
|
EMAILS_FROM_EMAIL: EmailStr | None = None
|
||||||
|
EMAILS_FROM_NAME: EmailStr | None = None
|
||||||
|
|
||||||
return value
|
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
|
||||||
|
|
||||||
def __parse_to_int(self, value) -> int:
|
@computed_field
|
||||||
"""Parses and validates integer values."""
|
@property
|
||||||
if not str(value).isdigit():
|
def emails_enabled(self) -> bool:
|
||||||
raise ConfigError(
|
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)
|
||||||
"Value for %(env_name)s - %(value)s must be a numeric integer",
|
|
||||||
env_name=self.env_name,
|
|
||||||
value=value,
|
|
||||||
)
|
|
||||||
|
|
||||||
value = int(value)
|
EMAIL_TEST_USER: EmailStr = "test@example.com"
|
||||||
|
FIRST_SUPERUSER: EmailStr
|
||||||
|
FIRST_SUPERUSER_PASSWORD: str
|
||||||
|
|
||||||
if self.min_value is not None and value < self.min_value:
|
def _check_default_secret(self, var_name: str, value: str | None) -> None:
|
||||||
raise ConfigError(
|
if value == "changethis":
|
||||||
"Value for %(env_name)s - %(value)d is too small. Must be greater than or equal to %(min_value)d",
|
message = f'The value of {var_name} is "changethis", '"for security, please change it, at least for deployments."
|
||||||
env_name=self.env_name,
|
if self.ENVIRONMENT == "local":
|
||||||
value=value,
|
warnings.warn(message, stacklevel=1)
|
||||||
min_value=self.min_value,
|
else:
|
||||||
)
|
raise ValueError(message)
|
||||||
|
|
||||||
if self.max_value is not None and value > self.max_value:
|
@model_validator(mode="after")
|
||||||
raise ConfigError(
|
def _enforce_non_default_secrets(self) -> Self:
|
||||||
"Value for %(env_name)s - %(value)d is too large. Must be smaller than or equal to %(max_value)d",
|
self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
|
||||||
env_name=self.env_name,
|
self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD)
|
||||||
value=value,
|
self._check_default_secret("FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD)
|
||||||
max_value=self.max_value,
|
|
||||||
)
|
|
||||||
|
|
||||||
return value
|
return self
|
||||||
|
|
||||||
def __parse_to_bool(self, value):
|
|
||||||
value = str(value).lower()
|
|
||||||
|
|
||||||
true_values = ["1", "true", "yes"]
|
|
||||||
false_values = ["0", "false", "no"]
|
|
||||||
|
|
||||||
if value in true_values:
|
|
||||||
return True
|
|
||||||
if value in false_values:
|
|
||||||
return False
|
|
||||||
|
|
||||||
raise ConfigError("Value for %(env_name)s - %(value)s cannot be converted to a true / false value",
|
|
||||||
env_name=self.env_name,
|
|
||||||
value=value)
|
|
||||||
|
|
||||||
|
|
||||||
class EnvConfig():
|
settings = Settings()
|
||||||
|
|
||||||
_instance = None
|
|
||||||
_initialized = False
|
|
||||||
|
|
||||||
def __new__(cls):
|
|
||||||
print("Called __new__")
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if EnvConfig._initialized:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
self.fields = {}
|
|
||||||
|
|
||||||
EnvConfig._initialized = True
|
|
||||||
|
|
||||||
def add_field(self, name: str, field: EnvConfigField):
|
|
||||||
if name in self.fields:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.fields[name] = field
|
|
||||||
|
|
||||||
def load_config(self):
|
|
||||||
dotenv.load_dotenv()
|
|
||||||
|
|
||||||
for field in self.fields.values():
|
|
||||||
try:
|
|
||||||
field.load()
|
|
||||||
except ConfigError as e:
|
|
||||||
self.logger.error("Configuration error: %s", e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def __getitem__(self, key: str):
|
|
||||||
return self.fields[key].value
|
|
||||||
|
|
||||||
def __contains__(self, key: str) -> bool:
|
|
||||||
return key in self.fields
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {attr: getattr(self, attr) for attr in self.fields}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["EnvConfig"]
|
|
||||||
|
13
backend/app/core/errors.py
Normal file
13
backend/app/core/errors.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class SwagShopError(Exception):
|
||||||
|
"""
|
||||||
|
Generic error to be raised throughout the entire app.
|
||||||
|
All other custom errors extend this class
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryError(SwagShopError):
|
||||||
|
"""Generic error occuring in a repository"""
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceError(SwagShopError):
|
||||||
|
"""Generic error occuring in a service"""
|
@ -1,12 +0,0 @@
|
|||||||
class SwagShopError(Exception):
|
|
||||||
def __init__(self, message):
|
|
||||||
super().__init__(message)
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
class ConfigError(SwagShopError):
|
|
||||||
def __init__(self, message, **kwargs):
|
|
||||||
formatted_message = message % kwargs
|
|
||||||
super().__init__(formatted_message)
|
|
||||||
self.message = formatted_message
|
|
||||||
|
|
||||||
__all__ = ["SwagShopError", "ConfigError"]
|
|
@ -5,7 +5,7 @@ import jwt
|
|||||||
|
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
from app.core.config import EnvConfig
|
from app.core.config import settings
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
@ -14,11 +14,9 @@ ALGORITHM = "HS256"
|
|||||||
|
|
||||||
|
|
||||||
def create_access_token(subject: str | Any, expires_delta: timedelta) -> str:
|
def create_access_token(subject: str | Any, expires_delta: timedelta) -> str:
|
||||||
config = EnvConfig()
|
|
||||||
|
|
||||||
expire = datetime.now(timezone.utc) + expires_delta
|
expire = datetime.now(timezone.utc) + expires_delta
|
||||||
to_encode = {"exp": expire, "sub": str(subject)}
|
to_encode = {"exp": expire, "sub": str(subject)}
|
||||||
encoded_jwt = jwt.encode(to_encode, config["secret_key"], algorithm=ALGORITHM)
|
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,63 +1,43 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Generator
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Generator
|
||||||
|
|
||||||
from sqlalchemy.orm import sessionmaker, Session
|
from sqlalchemy.exc import DatabaseError as SqlAlchemyError
|
||||||
from sqlalchemy import create_engine, text
|
from sqlmodel import Session, create_engine, select, SQLModel
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
from app.database.exceptions import DatabaseError
|
from app.database.exceptions import DatabaseError
|
||||||
|
|
||||||
|
import app.database.models
|
||||||
|
|
||||||
class DatabaseManager():
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_instance: 'DatabaseManager' = None
|
logger.info("Creating engine")
|
||||||
|
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||||
def __new__(cls, mysql_user: str, mysql_password: str, mysql_host: str, mysql_port: int, mysql_db_name: str):
|
SQLModel.metadata.create_all(engine)
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, mysql_user: str, mysql_password: str, mysql_host: str, mysql_port: int, mysql_db_name: str) -> None:
|
def test_connection():
|
||||||
self.logger = logging.getLogger(__name__)
|
logger.debug("Testing database connection")
|
||||||
self.logger.info("Reading database config")
|
try:
|
||||||
self.engine = create_engine('mysql+mysqlconnector://%s:%s@%s:%s/%s' % (
|
with Session(engine) as session:
|
||||||
mysql_user,
|
session.exec(select(1))
|
||||||
mysql_password,
|
logger.debug("Database connection successful")
|
||||||
mysql_host,
|
except SqlAlchemyError as e:
|
||||||
str(mysql_port),
|
logger.critical("Database connection failed: %s", e)
|
||||||
mysql_db_name),
|
raise DatabaseError("Database connection failed", DatabaseError.CONNECTION_ERROR) from e
|
||||||
pool_pre_ping=True)
|
|
||||||
self.test_connection()
|
|
||||||
self.Session = sessionmaker(bind=self.engine) # NOSONAR
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
|
||||||
self.logger.debug("Closing connection")
|
|
||||||
self.engine.dispose()
|
|
||||||
|
|
||||||
def test_connection(self):
|
|
||||||
self.logger.debug("Testing database connection")
|
|
||||||
try:
|
|
||||||
with self.engine.connect() as connection:
|
|
||||||
connection.execute(text("select 1"))
|
|
||||||
self.logger.debug("Database connection successful")
|
|
||||||
except DatabaseError as e:
|
|
||||||
self.logger.critical("Database connection failed: %s", e)
|
|
||||||
raise DatabaseError("Database connection failed", DatabaseError.CONNECTION_ERROR) from e
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
def cleanup() -> None:
|
||||||
@contextmanager
|
logger.debug("Closing connection")
|
||||||
def get_session(cls) -> Generator[Session, None, None]:
|
engine.dispose()
|
||||||
session = cls._instance.Session()
|
|
||||||
try:
|
|
||||||
yield session
|
|
||||||
except Exception as e:
|
|
||||||
session.rollback()
|
|
||||||
cls._instance.logger.error("Transaction failed: %s", e)
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["DatabaseManager"]
|
@contextmanager
|
||||||
|
def get_session() -> Generator[Session, None, None]:
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
test_connection()
|
||||||
|
6
backend/app/database/models/__init__.py
Normal file
6
backend/app/database/models/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from . import user_model, shop_model
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
*user_model.__all__,
|
||||||
|
*shop_model.__all__
|
||||||
|
]
|
@ -3,6 +3,7 @@ from sqlalchemy.sql import func
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from .base_model import Base
|
from .base_model import Base
|
||||||
|
|
||||||
|
|
||||||
class Purchase(Base):
|
class Purchase(Base):
|
||||||
__tablename__ = "purchase"
|
__tablename__ = "purchase"
|
||||||
|
|
@ -3,6 +3,7 @@ from sqlalchemy.orm import relationship
|
|||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
from .base_model import Base
|
from .base_model import Base
|
||||||
|
|
||||||
|
|
||||||
class Wishlist(Base):
|
class Wishlist(Base):
|
||||||
__tablename__ = "wishlist"
|
__tablename__ = "wishlist"
|
||||||
|
|
69
backend/app/database/models/shop_model.py
Normal file
69
backend/app/database/models/shop_model.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
from enum import Enum as PyEnum
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
from sqlalchemy import Column, CheckConstraint
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
|
from sqlmodel import SQLModel, Field, Relationship, Enum
|
||||||
|
from pydantic import BaseModel, constr
|
||||||
|
|
||||||
|
|
||||||
|
class ShopStatus(PyEnum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
SUSPENDED = "suspended"
|
||||||
|
|
||||||
|
|
||||||
|
class ShopLinkEntry(BaseModel):
|
||||||
|
name: constr(strip_whitespace=True, min_length=1)
|
||||||
|
url: constr(strip_whitespace=True, min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ShopLinks(BaseModel):
|
||||||
|
links: List[ShopLinkEntry]
|
||||||
|
|
||||||
|
|
||||||
|
class ShopBusinessHourEntry(BaseModel):
|
||||||
|
day: constr(strip_whitespace=True, min_length=1)
|
||||||
|
open_time: time
|
||||||
|
close_time: time
|
||||||
|
|
||||||
|
|
||||||
|
class ShopBusinessHours(BaseModel):
|
||||||
|
hours: List[ShopBusinessHourEntry]
|
||||||
|
|
||||||
|
|
||||||
|
class Shop(SQLModel, table=True):
|
||||||
|
__tablename__ = 'shop'
|
||||||
|
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
owner_id: int = Field(foreign_key='user.id', nullable=False)
|
||||||
|
name: str = Field(max_length=100, nullable=False, unique=True)
|
||||||
|
description: str = Field(max_length=500, nullable=False)
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
||||||
|
status: ShopStatus = Field(sa_column=Column(Enum(ShopStatus)))
|
||||||
|
logo: Optional[str] = Field(max_length=100)
|
||||||
|
contact_email: str = Field(max_length=128, nullable=False, unique=True)
|
||||||
|
phone_number: str = Field(max_length=15, nullable=False)
|
||||||
|
address: str = Field(nullable=False)
|
||||||
|
currency: str = Field(max_length=3, nullable=False)
|
||||||
|
|
||||||
|
business_hours: ShopBusinessHours = Field(
|
||||||
|
sa_column=Column(JSONB, nullable=False, default=lambda: {})
|
||||||
|
)
|
||||||
|
links: ShopLinks = Field(
|
||||||
|
sa_column=Column(JSONB, nullable=False, default=lambda: {})
|
||||||
|
)
|
||||||
|
|
||||||
|
owner: Optional["User"] = Relationship(back_populates='owned_shops')
|
||||||
|
registered_users: List["User"] = Relationship(back_populates='registered_shop')
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint("business_hours ? 'hours'", name="check_business_hours_keys"),
|
||||||
|
CheckConstraint("links ? 'links'", name="check_links_keys"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Shop", "ShopBusinessHours", "ShopLinks", "ShopStatus"]
|
59
backend/app/database/models/user_model.py
Normal file
59
backend/app/database/models/user_model.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from typing import Optional, List
|
||||||
|
from uuid import UUID
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlmodel import SQLModel, Field, Relationship
|
||||||
|
|
||||||
|
|
||||||
|
class User(SQLModel, table=True):
|
||||||
|
__tablename__ = 'user'
|
||||||
|
|
||||||
|
id: int = Field(primary_key=True)
|
||||||
|
uuid: UUID = Field(nullable=False, unique=True)
|
||||||
|
user_role_id: int = Field(foreign_key="user_role.id", nullable=False)
|
||||||
|
shop_id: Optional[int] = Field(foreign_key="shop.id")
|
||||||
|
username: str = Field(max_length=64, nullable=False, unique=True)
|
||||||
|
email: str = Field(max_length=128, nullable=False, unique=True)
|
||||||
|
password: str = Field(max_length=60, nullable=False)
|
||||||
|
first_name: Optional[str] = Field(max_length=64)
|
||||||
|
last_name: Optional[str] = Field(max_length=64)
|
||||||
|
phone_number: str = Field(max_length=15, nullable=False)
|
||||||
|
profile_picture: Optional[str] = Field(max_length=100)
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
|
||||||
|
last_login: Optional[datetime] = Field(default_factory=datetime.utcnow)
|
||||||
|
|
||||||
|
owned_shops: List["Shop"] = Relationship(back_populates="owner")
|
||||||
|
registered_shop: List["Shop"] = Relationship(back_populates="registered_users")
|
||||||
|
|
||||||
|
role: Optional["UserRole"] = Relationship(back_populates="users")
|
||||||
|
preferences: Optional["UserPreferences"] = Relationship(back_populates="user")
|
||||||
|
statistics: Optional["UserStatistics"] = Relationship(back_populates="user_statistics")
|
||||||
|
|
||||||
|
|
||||||
|
class UserPreferences(SQLModel, table=True):
|
||||||
|
__tablename__ = 'user_preferences'
|
||||||
|
|
||||||
|
user_id: int = Field(foreign_key="user.id", primary_key=True)
|
||||||
|
|
||||||
|
user: Optional["User"] = Relationship(back_populates="preferences")
|
||||||
|
|
||||||
|
|
||||||
|
class UserRole(SQLModel, table=True):
|
||||||
|
__tablename__ = 'user_role'
|
||||||
|
|
||||||
|
id: int = Field(primary_key=True)
|
||||||
|
name: str = Field(nullable=False, unique=True, max_length=50)
|
||||||
|
|
||||||
|
users = Relationship(back_populates="role")
|
||||||
|
|
||||||
|
|
||||||
|
class UserStatistics(SQLModel, table=True):
|
||||||
|
__tablename__ = "user_statistics"
|
||||||
|
|
||||||
|
user_id: int = Field(foreign_key="user.id", primary_key=True)
|
||||||
|
total_spend: Optional[float] = Field(default=None)
|
||||||
|
|
||||||
|
user: Optional["User"] = Relationship(back_populates="statistics")
|
||||||
|
|
||||||
|
__all__ = ["User", "UserPreferences", "UserRole"]
|
@ -14,6 +14,7 @@ class LoginRequest(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
async def get_jsession_id(request: Request):
|
async def get_jsession_id(request: Request):
|
||||||
jsessionid = (
|
jsessionid = (
|
||||||
request.headers.get("JSESSIONID")
|
request.headers.get("JSESSIONID")
|
||||||
@ -26,11 +27,11 @@ async def get_jsession_id(request: Request):
|
|||||||
|
|
||||||
return jsessionid
|
return jsessionid
|
||||||
|
|
||||||
|
|
||||||
async def get_credentials(credentials: LoginRequest = Body(...)):
|
async def get_credentials(credentials: LoginRequest = Body(...)):
|
||||||
return credentials.dict()
|
return credentials.dict()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def get_query_token(token: str):
|
async def get_query_token(token: str):
|
||||||
if token != "jessica":
|
if token != "jessica":
|
||||||
raise HTTPException(status_code=400, detail="No Jessica token provided")
|
raise HTTPException(status_code=400, detail="No Jessica token provided")
|
||||||
|
@ -1,46 +1,33 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from app.routes.cart_routes import router as cart_router
|
from fastapi.routing import APIRoute
|
||||||
from app.routes.user_routes import router as user_router
|
|
||||||
from app.routes.shop_routes import router as shop_router
|
|
||||||
|
|
||||||
from app.core.config import EnvConfig, EnvConfigField
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.database.manager import DatabaseManager
|
from app.utils import logger
|
||||||
|
|
||||||
|
from app.api import api_router
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
config = EnvConfig()
|
logger.setup_logger()
|
||||||
|
|
||||||
config.add_field("mysql_user", EnvConfigField("MYSQL_USER", required=True))
|
def custom_generate_unique_id(route: APIRoute) -> str:
|
||||||
config.add_field("mysql_database", EnvConfigField("MYSQL_DATABASE", required=True))
|
return f"{route.tags[0]}-{route.name}"
|
||||||
config.add_field("mysql_host", EnvConfigField("MYSQL_HOST", required=True))
|
|
||||||
config.add_field("mysql_port", EnvConfigField("MYSQL_PORT", default=3306, data_type=int, required=False))
|
|
||||||
config.add_field("mysql_password", EnvConfigField("MYSQL_PASSWORD", required=True))
|
|
||||||
|
|
||||||
config.load_config()
|
|
||||||
|
|
||||||
|
|
||||||
config2 = EnvConfig()
|
|
||||||
|
|
||||||
|
|
||||||
DatabaseManager(
|
|
||||||
mysql_user=config["mysql_user"],
|
|
||||||
mysql_password=config["mysql_password"],
|
|
||||||
mysql_host=config["mysql_host"],
|
|
||||||
mysql_port=config["mysql_port"],
|
|
||||||
mysql_db_name=config["mysql_database"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="SWAG Shop",
|
title="SWAG Shop",
|
||||||
version="0.0.1"
|
version="0.0.1",
|
||||||
|
generate_unique_id_function=custom_generate_unique_id
|
||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(user_router)
|
|
||||||
app.include_router(cart_router)
|
|
||||||
app.include_router(shop_router)
|
|
||||||
|
|
||||||
|
if settings.all_cors_origins:
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.all_cors_origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/")
|
app.include_router(api_router)
|
||||||
async def root():
|
|
||||||
return {"message": "Hello World"}
|
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
from sqlalchemy import ForeignKey, Column, Integer, JSON, TIMESTAMP, String, Enum, Text
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
|
|
||||||
from .base_model import Base
|
|
||||||
|
|
||||||
|
|
||||||
class Shop(Base):
|
|
||||||
__tablename__ = 'shop'
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
owner_id = Column(Integer, ForeignKey('user.id'), nullable=False)
|
|
||||||
name = Column(String(100), unique=True, nullable=False)
|
|
||||||
description = Column(String(500), nullable=False)
|
|
||||||
created_at = Column(TIMESTAMP, default='CURRENT_TIMESTAMP')
|
|
||||||
updated_at = Column(TIMESTAMP, default='CURRENT_TIMESTAMP')
|
|
||||||
status = Column(Enum('active', 'inactive', 'suspended'), nullable=False)
|
|
||||||
logo = Column(String(100))
|
|
||||||
contact_email = Column(String(128), unique=True, nullable=False)
|
|
||||||
phone_number = Column(String(15), nullable=False)
|
|
||||||
address = Column(Text, nullable=False)
|
|
||||||
currency = Column(String(3), nullable=False)
|
|
||||||
business_hours = Column(JSON, nullable=False)
|
|
||||||
links = Column(JSON, nullable=False)
|
|
||||||
|
|
||||||
owner = relationship('User', back_populates='owned_shops')
|
|
||||||
registered_users = relationship('User', back_populates='registered_shop')
|
|
||||||
# products = relationship('Product', back_populates='shop')
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Shop"]
|
|
@ -1,32 +0,0 @@
|
|||||||
from sqlalchemy import Column, String, Integer, ForeignKey, TIMESTAMP
|
|
||||||
from sqlalchemy.dialects.mysql import INTEGER
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from .base_model import Base
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
|
||||||
__tablename__ = 'user'
|
|
||||||
|
|
||||||
id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True)
|
|
||||||
user_role_id = Column(INTEGER(unsigned=True), ForeignKey('user_role.id'), nullable=False)
|
|
||||||
shop_id = Column(Integer, ForeignKey('shop.id'), nullable=True)
|
|
||||||
username = Column(String(64), nullable=False, unique=True)
|
|
||||||
email = Column(String(128), nullable=False, unique=True)
|
|
||||||
password = Column(String(60), nullable=False)
|
|
||||||
first_name = Column(String(64), nullable=True)
|
|
||||||
last_name = Column(String(64), nullable=True)
|
|
||||||
phone_number = Column(String(15), nullable=False)
|
|
||||||
profile_picture = Column(String(100), nullable=True)
|
|
||||||
created_at = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP")
|
|
||||||
updated_at = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
|
|
||||||
last_login = Column(TIMESTAMP, nullable=True, server_default="CURRENT_TIMESTAMP")
|
|
||||||
|
|
||||||
owned_shops = relationship("Shop", back_populates="owner")
|
|
||||||
registered_shop = relationship("Shop", back_populates="registered_users")
|
|
||||||
|
|
||||||
role = relationship("UserRole", back_populates="users")
|
|
||||||
preferences = relationship("UserPreferences", uselist=False, back_populates="user")
|
|
||||||
statistics = relationship("UserStatistics", uselist=False, back_populates="user_statistics")
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["User"]
|
|
@ -1,11 +0,0 @@
|
|||||||
from sqlalchemy import Column, ForeignKey
|
|
||||||
from sqlalchemy.dialects.mysql import INTEGER
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from .base_model import Base
|
|
||||||
|
|
||||||
class UserPreferences(Base):
|
|
||||||
__tablename__ = 'user_preferences'
|
|
||||||
|
|
||||||
user_id = Column(INTEGER(unsigned=True), ForeignKey('user.id'), primary_key=True)
|
|
||||||
|
|
||||||
user = relationship("User", back_populates="preferences")
|
|
@ -1,16 +0,0 @@
|
|||||||
from sqlalchemy import Column, String
|
|
||||||
from sqlalchemy.dialects.mysql import INTEGER
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from .base_model import Base
|
|
||||||
|
|
||||||
|
|
||||||
class UserRole(Base):
|
|
||||||
__tablename__ = 'user_role'
|
|
||||||
|
|
||||||
id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True)
|
|
||||||
name = Column(String(45), nullable=False, unique=True)
|
|
||||||
|
|
||||||
users = relationship("User", back_populates="role")
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["UserRole"]
|
|
@ -1,12 +0,0 @@
|
|||||||
from sqlalchemy import Column, Float, ForeignKey
|
|
||||||
from sqlalchemy.dialects.mysql import INTEGER
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
from .base_model import Base
|
|
||||||
|
|
||||||
class UserStatistics(Base):
|
|
||||||
__tablename__ = "user_statistics"
|
|
||||||
|
|
||||||
user_id = Column(INTEGER(unsigned=True), ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
|
|
||||||
total_spend = Column(Float, nullable=True)
|
|
||||||
|
|
||||||
user = relationship("User", back_populates="user_statistics", foreign_keys=[user_id])
|
|
@ -1,7 +1,8 @@
|
|||||||
from pydantic import BaseModel, EmailStr, Field
|
from sqlmodel import Field as SqlModelField, SQLModel
|
||||||
|
from pydantic import EmailStr, Field
|
||||||
|
|
||||||
|
|
||||||
class UserRegisterSchema(BaseModel):
|
class UserRegisterSchema(SQLModel):
|
||||||
username: str = Field(..., min_length=3, max_length=64)
|
username: str = Field(..., min_length=3, max_length=64)
|
||||||
email: EmailStr = Field(...)
|
email: EmailStr = Field(...)
|
||||||
phone_number: str = Field(..., min_length=2, max_length=16, pattern=r'^\+[1-9]\d{1,14}$')
|
phone_number: str = Field(..., min_length=2, max_length=16, pattern=r'^\+[1-9]\d{1,14}$')
|
||||||
@ -9,9 +10,19 @@ class UserRegisterSchema(BaseModel):
|
|||||||
shop_id: int = 0
|
shop_id: int = 0
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|
||||||
class UserLoginSchema(BaseModel):
|
|
||||||
|
class UserLoginSchema(SQLModel):
|
||||||
shop_id: int = 0
|
shop_id: int = 0
|
||||||
username: str = Field(..., min_length=3, max_length=64)
|
username: str = Field(..., min_length=3, max_length=64)
|
||||||
password: str = Field(..., min_length=6, max_length=128)
|
password: str = Field(..., min_length=6, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
|
class Token(SQLModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
|
||||||
|
|
||||||
|
class TokenPayload(SQLModel):
|
||||||
|
sub: str | None = None
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import imghdr
|
import imghdr
|
||||||
|
|
||||||
|
|
||||||
def is_base64_jpg(decoded_string) -> bool:
|
def is_base64_jpg(decoded_string) -> bool:
|
||||||
try:
|
try:
|
||||||
image_type = imghdr.what(None, decoded_string)
|
image_type = imghdr.what(None, decoded_string)
|
||||||
|
@ -5,6 +5,7 @@ import app.messages.api_errors as errors
|
|||||||
|
|
||||||
from app.db import product_db
|
from app.db import product_db
|
||||||
|
|
||||||
|
|
||||||
def product_list(page: int):
|
def product_list(page: int):
|
||||||
try:
|
try:
|
||||||
result_products = product_db.fetch_products(page)
|
result_products = product_db.fetch_products(page)
|
||||||
|
@ -11,6 +11,7 @@ from app.mail.mail import send_mail
|
|||||||
|
|
||||||
from app.messages.mail_responses.user_email import USER_EMAIL_SUCCESSFULLY_DELETED_ACCOUNT
|
from app.messages.mail_responses.user_email import USER_EMAIL_SUCCESSFULLY_DELETED_ACCOUNT
|
||||||
|
|
||||||
|
|
||||||
def delete_user(user_id: str) -> Tuple[Union[dict, str], int]:
|
def delete_user(user_id: str) -> Tuple[Union[dict, str], int]:
|
||||||
"""
|
"""
|
||||||
Deletes a user account.
|
Deletes a user account.
|
||||||
|
@ -13,7 +13,6 @@ from app.models.user_model import User
|
|||||||
from app.messages.mail_responses.user_email import USER_EMAIL_SUCCESSFULLY_LOGGED_IN
|
from app.messages.mail_responses.user_email import USER_EMAIL_SUCCESSFULLY_LOGGED_IN
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def login(username: str, password: str) -> Tuple[Union[dict, str], int]:
|
def login(username: str, password: str) -> Tuple[Union[dict, str], int]:
|
||||||
"""
|
"""
|
||||||
Authenticates a user with the provided username and password.
|
Authenticates a user with the provided username and password.
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
from app.database.manager import DatabaseManager
|
from app.database.models.user_model import User
|
||||||
from app.models.user_model import User
|
|
||||||
|
|
||||||
from app.schemas.user_schemas import UserRegisterSchema
|
from app.schemas.user_schemas import UserRegisterSchema
|
||||||
|
|
||||||
|
from app.core.security import get_password_hash
|
||||||
|
|
||||||
|
|
||||||
def create_user(user_data: UserRegisterSchema):
|
def create_user(user_data: UserRegisterSchema):
|
||||||
print("Creating user")
|
print("Creating account")
|
||||||
return
|
|
||||||
with DatabaseManager.get_session() as session:
|
with DatabaseManager.get_session() as session:
|
||||||
user = User(
|
user = User(
|
||||||
username=user_data.username,
|
username=user_data.username,
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError
|
|
||||||
from app.database.exceptions import DatabaseError
|
|
||||||
|
|
||||||
|
|
||||||
def handle_database_errors(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
try:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
except SqlAlchemyDatabaseError as e:
|
|
||||||
raise DatabaseError(str(e), -1) from e
|
|
||||||
return wrapper
|
|
13
backend/app/utils/logger.py
Normal file
13
backend/app/utils/logger.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
def setup_logger():
|
||||||
|
verbosity = settings.VERBOSITY
|
||||||
|
if verbosity == "DEBUG":
|
||||||
|
log_format = "[ %(name)s / %(levelname)s ] - %(filename)s:%(lineno)d - %(message)s"
|
||||||
|
else:
|
||||||
|
log_format = "[ %(levelname)s ] - %(message)s"
|
||||||
|
|
||||||
|
logging.basicConfig(level=verbosity, format=log_format, stream=sys.stdout)
|
6
backend/app/utils/models.py
Normal file
6
backend/app/utils/models.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from uuid import uuid5, UUID, NAMESPACE_DNS
|
||||||
|
|
||||||
|
def generate_user_uuid5(email: str, shop_id: Optional[int]) -> UUID:
|
||||||
|
unique_string = f"{email}-{shop_id}" if shop_id else email
|
||||||
|
return uuid5(NAMESPACE_DNS, unique_string)
|
27
backend/app/utils/propagate.py
Normal file
27
backend/app/utils/propagate.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError
|
||||||
|
|
||||||
|
from app.core.errors import RepositoryError, ServiceError
|
||||||
|
|
||||||
|
|
||||||
|
def propagate_db_error_to_service(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except (SqlAlchemyDatabaseError, RepositoryError) as e:
|
||||||
|
raise ServiceError(str(e)) from e
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def propagate_service_errors_to_http_errors(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except ServiceError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
return wrapper
|
228
backend/poetry.lock
generated
228
backend/poetry.lock
generated
@ -43,6 +43,20 @@ files = [
|
|||||||
{file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"},
|
{file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autopep8"
|
||||||
|
version = "2.3.2"
|
||||||
|
description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128"},
|
||||||
|
{file = "autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pycodestyle = ">=2.12.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "4.2.1"
|
version = "4.2.1"
|
||||||
@ -92,17 +106,6 @@ files = [
|
|||||||
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfgv"
|
|
||||||
version = "3.4.0"
|
|
||||||
description = "Validate configuration and produce human readable error messages."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
|
|
||||||
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.8"
|
version = "8.1.8"
|
||||||
@ -143,17 +146,6 @@ files = [
|
|||||||
graph = ["objgraph (>=1.7.2)"]
|
graph = ["objgraph (>=1.7.2)"]
|
||||||
profile = ["gprof2dot (>=2022.7.29)"]
|
profile = ["gprof2dot (>=2022.7.29)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "distlib"
|
|
||||||
version = "0.3.9"
|
|
||||||
description = "Distribution utilities"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
files = [
|
|
||||||
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
|
|
||||||
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dnspython"
|
name = "dnspython"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@ -234,22 +226,6 @@ uvicorn = {version = ">=0.15.0", extras = ["standard"]}
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
standard = ["uvicorn[standard] (>=0.15.0)"]
|
standard = ["uvicorn[standard] (>=0.15.0)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "filelock"
|
|
||||||
version = "3.16.1"
|
|
||||||
description = "A platform independent file lock."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
|
|
||||||
{file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"]
|
|
||||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
|
|
||||||
typing = ["typing-extensions (>=4.12.2)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.1.1"
|
version = "3.1.1"
|
||||||
@ -447,20 +423,6 @@ http2 = ["h2 (>=3,<5)"]
|
|||||||
socks = ["socksio (==1.*)"]
|
socks = ["socksio (==1.*)"]
|
||||||
zstd = ["zstandard (>=0.18.0)"]
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "identify"
|
|
||||||
version = "2.6.5"
|
|
||||||
description = "File identification library for Python"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.9"
|
|
||||||
files = [
|
|
||||||
{file = "identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566"},
|
|
||||||
{file = "identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
license = ["ukkonen"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.10"
|
version = "3.10"
|
||||||
@ -633,17 +595,6 @@ files = [
|
|||||||
{file = "mysql-connector-2.2.9.tar.gz", hash = "sha256:1733e6ce52a049243de3264f1fbc22a852cb35458c4ad739ba88189285efdf32"},
|
{file = "mysql-connector-2.2.9.tar.gz", hash = "sha256:1733e6ce52a049243de3264f1fbc22a852cb35458c4ad739ba88189285efdf32"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nodeenv"
|
|
||||||
version = "1.9.1"
|
|
||||||
description = "Node.js virtual environment builder"
|
|
||||||
optional = false
|
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
|
||||||
files = [
|
|
||||||
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
|
|
||||||
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "passlib"
|
name = "passlib"
|
||||||
version = "1.7.4"
|
version = "1.7.4"
|
||||||
@ -681,22 +632,92 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-
|
|||||||
type = ["mypy (>=1.11.2)"]
|
type = ["mypy (>=1.11.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pre-commit"
|
name = "psycopg2-binary"
|
||||||
version = "4.0.1"
|
version = "2.9.10"
|
||||||
description = "A framework for managing and maintaining multi-language pre-commit hooks."
|
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"},
|
{file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"},
|
||||||
{file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"},
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"},
|
||||||
|
{file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[[package]]
|
||||||
cfgv = ">=2.0.0"
|
name = "pycodestyle"
|
||||||
identify = ">=1.0.0"
|
version = "2.12.1"
|
||||||
nodeenv = ">=0.11.1"
|
description = "Python style guide checker"
|
||||||
pyyaml = ">=5.1"
|
optional = false
|
||||||
virtualenv = ">=20.10.0"
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"},
|
||||||
|
{file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
@ -830,6 +851,26 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-settings"
|
||||||
|
version = "2.8.1"
|
||||||
|
description = "Settings management using Pydantic"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"},
|
||||||
|
{file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pydantic = ">=2.7.0"
|
||||||
|
python-dotenv = ">=0.21.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
|
||||||
|
toml = ["tomli (>=2.0.1)"]
|
||||||
|
yaml = ["pyyaml (>=6.0.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.1"
|
version = "2.19.1"
|
||||||
@ -1123,6 +1164,21 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
|
|||||||
pymysql = ["pymysql"]
|
pymysql = ["pymysql"]
|
||||||
sqlcipher = ["sqlcipher3_binary"]
|
sqlcipher = ["sqlcipher3_binary"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlmodel"
|
||||||
|
version = "0.0.24"
|
||||||
|
description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193"},
|
||||||
|
{file = "sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pydantic = ">=1.10.13,<3.0.0"
|
||||||
|
SQLAlchemy = ">=2.0.14,<2.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.41.3"
|
version = "0.41.3"
|
||||||
@ -1255,26 +1311,6 @@ dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"]
|
|||||||
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
||||||
test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
|
test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "virtualenv"
|
|
||||||
version = "20.29.1"
|
|
||||||
description = "Virtual Python Environment builder"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"},
|
|
||||||
{file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
distlib = ">=0.3.7,<1"
|
|
||||||
filelock = ">=3.12.2,<4"
|
|
||||||
platformdirs = ">=3.9.1,<5"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
|
||||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watchfiles"
|
name = "watchfiles"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@ -1439,4 +1475,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "94e26cada399002f42bf182c10ffeabb2a71a62c600beeaed229cf4b16c03504"
|
content-hash = "6c9b9d5fc1e4617313748228ffbb73bc8d8ad82b5ccecbe68e8be0f7f13842bd"
|
||||||
|
@ -14,12 +14,21 @@ python-dotenv = "^1.0.1"
|
|||||||
mysql-connector = "^2.2.9"
|
mysql-connector = "^2.2.9"
|
||||||
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
|
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
|
||||||
pyjwt = "^2.10.1"
|
pyjwt = "^2.10.1"
|
||||||
|
pydantic-settings = "^2.8.1"
|
||||||
|
sqlmodel = "^0.0.24"
|
||||||
|
psycopg2-binary = "^2.9.10"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pre-commit = "^4.0.1"
|
|
||||||
pylint = "^3.3.4"
|
pylint = "^3.3.4"
|
||||||
|
autopep8 = "^2.3.2"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.autopep8]
|
||||||
|
max_line_length = 200
|
||||||
|
ignore = "E501"
|
||||||
|
in_place = true
|
||||||
|
recursive = true
|
68
docker-compose.yml
Normal file
68
docker-compose.yml
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
services:
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:12
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
timeout: 10s
|
||||||
|
volumes:
|
||||||
|
- app-db-data:/var/lib/postgresql/data/pgdata
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD?Variable not set}
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER?Variable not set}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB?Variable not set}
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
- ADMINER_DESIGN=pepa-linha-dark
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: '${DOCKER_IMAGE_BACKEND?Variable not set}:${TAG-latest}'
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- FRONTEND_HOST=${FRONTEND_HOST?Variable not set}
|
||||||
|
- ENVIRONMENT=${ENVIRONMENT}
|
||||||
|
- BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS}
|
||||||
|
- SECRET_KEY=${SECRET_KEY?Variable not set}
|
||||||
|
- FIRST_SUPERUSER=${FIRST_SUPERUSER?Variable not set}
|
||||||
|
- FIRST_SUPERUSER_PASSWORD=${FIRST_SUPERUSER_PASSWORD?Variable not set}
|
||||||
|
- SMTP_HOST=${SMTP_HOST}
|
||||||
|
- SMTP_USER=${SMTP_USER}
|
||||||
|
- SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||||
|
- EMAILS_FROM_EMAIL=${EMAILS_FROM_EMAIL}
|
||||||
|
- MYSQL_SERVER=db
|
||||||
|
- MYSQL_PORT=${MYSQL_PORT}
|
||||||
|
- MYSQL_DB=${MYSQL_DB}
|
||||||
|
- MYSQL_USER=${MYSQL_USER?Variable not set}
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/utils/health-check/"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
app-db-data:
|
@ -8,9 +8,17 @@ export const registerUser = async (userData: {
|
|||||||
phone_number: string;
|
phone_number: string;
|
||||||
password: string;
|
password: string;
|
||||||
}) => {
|
}) => {
|
||||||
const response = await axios.post(`${API_BASE_URL}/user/register`, {
|
const response = await axios.post(
|
||||||
...userData,
|
`${API_BASE_URL}/user/register`,
|
||||||
shop_id: 0 // 0 signifies a free account
|
{
|
||||||
});
|
...userData,
|
||||||
|
shop_id: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user