diff --git a/.gitignore b/.gitignore index c3002d8..e432cec 100644 --- a/.gitignore +++ b/.gitignore @@ -149,6 +149,7 @@ activemq-data/ # Environments .env +.env.production .envrc .venv env/ diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..0a53e44 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,89 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..5881597 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.13-slim + +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +COPY --from=ghcr.io/astral-sh/uv:0.9.29 /uv /uvx /bin/ + +WORKDIR /app + +COPY ./pyproject.toml ./ +COPY ./uv.lock ./ +COPY ./backlog_app ./backlog_app + +RUN uv sync + +EXPOSE 8000 + +CMD ["uv", "run", "fastapi", "run", "backlog_app/main.py", "--port", "8000"] diff --git a/backend/backlog_app/alembic/env.py b/backend/backlog_app/alembic/env.py index 1e16910..1e671d8 100644 --- a/backend/backlog_app/alembic/env.py +++ b/backend/backlog_app/alembic/env.py @@ -8,11 +8,12 @@ from logging.config import fileConfig from alembic import context -from config import settings -from models.base import Base from sqlalchemy import pool from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine +from backlog_app.config import settings +from backlog_app.models import Base + config = context.config if config.config_file_name is not None: diff --git a/backend/backlog_app/api/crud.py b/backend/backlog_app/api/crud.py index f120bc7..d708c6c 100644 --- a/backend/backlog_app/api/crud.py +++ b/backend/backlog_app/api/crud.py @@ -1,13 +1,14 @@ import logging from fastapi import HTTPException -from models import User -from models.movie import Movie -from schemas.movie import MovieCreate, MovieRead, MovieUpdate from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from sqlalchemy.orm import selectinload +from backlog_app.models import User +from backlog_app.models.movie import Movie +from backlog_app.schemas.movie import MovieCreate, MovieRead, MovieUpdate + logger = logging.getLogger(__name__) diff --git a/backend/backlog_app/api/view/auth_view.py b/backend/backlog_app/api/view/auth_view.py index 6a7b441..3ff52a9 100644 --- a/backend/backlog_app/api/view/auth_view.py +++ b/backend/backlog_app/api/view/auth_view.py @@ -1,7 +1,10 @@ -from dependencies.authentification.backend import authentication_backend -from dependencies.authentification.fastapi_users_routers import fastapi_users from fastapi import APIRouter -from schemas.user import UserCreate, UserRead + +from backlog_app.dependencies.authentification.backend import authentication_backend +from backlog_app.dependencies.authentification.fastapi_users_routers import ( + fastapi_users, +) +from backlog_app.schemas.user import UserCreate, UserRead router = APIRouter( prefix="/auth", diff --git a/backend/backlog_app/api/view/movie_view.py b/backend/backlog_app/api/view/movie_view.py index cfb834a..8d6debb 100644 --- a/backend/backlog_app/api/view/movie_view.py +++ b/backend/backlog_app/api/view/movie_view.py @@ -1,12 +1,15 @@ from typing import Annotated, List -from api import crud -from dependencies.authentification.fastapi_users_routers import current_active_user from fastapi import APIRouter, Depends -from models.users import User -from schemas.movie import MovieCreate, MovieRead, MovieUpdate from sqlalchemy.ext.asyncio import AsyncSession -from storages.database import get_async_session + +from backlog_app.api import crud +from backlog_app.dependencies.authentification.fastapi_users_routers import ( + current_active_user, +) +from backlog_app.models.users import User +from backlog_app.schemas.movie import MovieCreate, MovieRead, MovieUpdate +from backlog_app.storages.database import get_async_session router = APIRouter(prefix="/movies", tags=["Movies"]) diff --git a/backend/backlog_app/api/view/users_view.py b/backend/backlog_app/api/view/users_view.py index a50d52e..bdd0fa3 100644 --- a/backend/backlog_app/api/view/users_view.py +++ b/backend/backlog_app/api/view/users_view.py @@ -1,6 +1,9 @@ -from dependencies.authentification.fastapi_users_routers import fastapi_users from fastapi import APIRouter -from schemas.user import UserRead, UserUpdate + +from backlog_app.dependencies.authentification.fastapi_users_routers import ( + fastapi_users, +) +from backlog_app.schemas.user import UserRead, UserUpdate router = APIRouter(prefix="/users", tags=["Users"]) diff --git a/backend/backlog_app/app_lifespan.py b/backend/backlog_app/app_lifespan.py index b5d4893..1f812bb 100644 --- a/backend/backlog_app/app_lifespan.py +++ b/backend/backlog_app/app_lifespan.py @@ -2,8 +2,8 @@ from contextlib import asynccontextmanager from fastapi import FastAPI -from storages.database import engine +from .storages.database import engine from .taskiq_broker import broker diff --git a/backend/backlog_app/dependencies/authentification/access_tokens.py b/backend/backlog_app/dependencies/authentification/access_tokens.py index 01cdc67..576f1bd 100644 --- a/backend/backlog_app/dependencies/authentification/access_tokens.py +++ b/backend/backlog_app/dependencies/authentification/access_tokens.py @@ -1,8 +1,9 @@ from typing import TYPE_CHECKING, Annotated from fastapi import Depends -from models import AccessToken -from storages.database import get_async_session + +from backlog_app.models import AccessToken +from backlog_app.storages.database import get_async_session if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession diff --git a/backend/backlog_app/dependencies/authentification/backend.py b/backend/backlog_app/dependencies/authentification/backend.py index 7f932ce..2f81b26 100644 --- a/backend/backlog_app/dependencies/authentification/backend.py +++ b/backend/backlog_app/dependencies/authentification/backend.py @@ -1,5 +1,6 @@ from fastapi_users.authentication import AuthenticationBackend -from servicies.authentification import bearer_transport + +from backlog_app.servicies.authentification import bearer_transport from .strategy import get_database_strategy diff --git a/backend/backlog_app/dependencies/authentification/fastapi_users_routers.py b/backend/backlog_app/dependencies/authentification/fastapi_users_routers.py index 73501f8..4fef678 100644 --- a/backend/backlog_app/dependencies/authentification/fastapi_users_routers.py +++ b/backend/backlog_app/dependencies/authentification/fastapi_users_routers.py @@ -1,7 +1,8 @@ import uuid from fastapi_users import FastAPIUsers -from models import User + +from backlog_app.models import User from .backend import authentication_backend from .user_manager import get_user_manager diff --git a/backend/backlog_app/dependencies/authentification/strategy.py b/backend/backlog_app/dependencies/authentification/strategy.py index 92d7481..c5c5c0f 100644 --- a/backend/backlog_app/dependencies/authentification/strategy.py +++ b/backend/backlog_app/dependencies/authentification/strategy.py @@ -1,9 +1,10 @@ from typing import TYPE_CHECKING, Annotated -from config import settings from fastapi import Depends from fastapi_users.authentication.strategy.db import DatabaseStrategy +from backlog_app.config import settings + from .access_tokens import get_access_token_db if TYPE_CHECKING: diff --git a/backend/backlog_app/dependencies/authentification/user_manager.py b/backend/backlog_app/dependencies/authentification/user_manager.py index 0c11d19..c7330f6 100644 --- a/backend/backlog_app/dependencies/authentification/user_manager.py +++ b/backend/backlog_app/dependencies/authentification/user_manager.py @@ -1,7 +1,8 @@ from typing import TYPE_CHECKING, Annotated from fastapi import Depends -from servicies.authentification import UserManager + +from backlog_app.servicies.authentification import UserManager from .users import get_user_db diff --git a/backend/backlog_app/dependencies/authentification/users.py b/backend/backlog_app/dependencies/authentification/users.py index 43790bb..7439e98 100644 --- a/backend/backlog_app/dependencies/authentification/users.py +++ b/backend/backlog_app/dependencies/authentification/users.py @@ -1,8 +1,9 @@ from typing import TYPE_CHECKING, Annotated from fastapi import Depends -from models import User -from storages.database import get_async_session + +from backlog_app.models import User +from backlog_app.storages.database import get_async_session if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession diff --git a/backend/backlog_app/jinja2_templates.py b/backend/backlog_app/jinja2_templates.py index f5c2d6d..3f72495 100644 --- a/backend/backlog_app/jinja2_templates.py +++ b/backend/backlog_app/jinja2_templates.py @@ -1,6 +1,7 @@ __all__ = ("templates",) -from config import BASE_DIR from fastapi.templating import Jinja2Templates +from backlog_app.config import BASE_DIR + templates = Jinja2Templates(directory=BASE_DIR / "templates") diff --git a/backend/backlog_app/main.py b/backend/backlog_app/main.py index a50a9e9..e9cdc94 100644 --- a/backend/backlog_app/main.py +++ b/backend/backlog_app/main.py @@ -2,13 +2,13 @@ from datetime import datetime import uvicorn -from api import router as api_router -from api.view.main_view import router as main_router -from config import settings from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from backend.backlog_app.app_lifespan import lifespan +from backlog_app.api import router as api_router +from backlog_app.api.view.main_view import router as main_router +from backlog_app.app_lifespan import lifespan +from backlog_app.config import settings logging.basicConfig( format=settings.logging.log_format, diff --git a/backend/backlog_app/models/movie.py b/backend/backlog_app/models/movie.py index fbc4a62..a872340 100644 --- a/backend/backlog_app/models/movie.py +++ b/backend/backlog_app/models/movie.py @@ -2,11 +2,12 @@ from datetime import datetime from typing import TYPE_CHECKING -from models.base import Base from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship +from .base import Base + if TYPE_CHECKING: from .users import User diff --git a/backend/backlog_app/models/users.py b/backend/backlog_app/models/users.py index 2dee427..37a2fb4 100644 --- a/backend/backlog_app/models/users.py +++ b/backend/backlog_app/models/users.py @@ -4,9 +4,10 @@ SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase, ) -from models.base import Base from sqlalchemy.orm import Mapped, relationship +from .base import Base + if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession diff --git a/backend/backlog_app/prestart.sh b/backend/backlog_app/prestart.sh new file mode 100644 index 0000000..b0cd115 --- /dev/null +++ b/backend/backlog_app/prestart.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e +set -x + +echo "Run apply migrations.." +alembic upgrade head +echo "Migrations applied!" + +exec "$@" diff --git a/backend/backlog_app/servicies/authentification/user_manager.py b/backend/backlog_app/servicies/authentification/user_manager.py index 956b3de..289e584 100644 --- a/backend/backlog_app/servicies/authentification/user_manager.py +++ b/backend/backlog_app/servicies/authentification/user_manager.py @@ -2,10 +2,11 @@ import uuid from typing import TYPE_CHECKING, Optional -from config import settings from fastapi_users import BaseUserManager, UUIDIDMixin -from models import User -from tasks.email_task import send_email_confirmed, send_verification_email + +from backlog_app.config import settings +from backlog_app.models import User +from backlog_app.tasks.email_task import send_email_confirmed, send_verification_email if TYPE_CHECKING: from fastapi import Request diff --git a/backend/backlog_app/storages/database.py b/backend/backlog_app/storages/database.py index 1a1d4ac..865d404 100644 --- a/backend/backlog_app/storages/database.py +++ b/backend/backlog_app/storages/database.py @@ -1,6 +1,7 @@ -from config import settings from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from backlog_app.config import settings + engine = create_async_engine( settings.db.connection.database_url_asyncpg, echo=False, diff --git a/backend/backlog_app/tasks/email_task.py b/backend/backlog_app/tasks/email_task.py index 9379088..e38b8b9 100644 --- a/backend/backlog_app/tasks/email_task.py +++ b/backend/backlog_app/tasks/email_task.py @@ -1,8 +1,8 @@ from textwrap import dedent -from jinja2_templates import templates -from servicies.mailing import send_email -from taskiq_broker import broker +from backlog_app.jinja2_templates import templates +from backlog_app.servicies.mailing import send_email +from backlog_app.taskiq_broker import broker @broker.task diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..9bf719b --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,24 @@ +services: + + pg: + image: postgres:18 + env_file: + - .env + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data/pgdata + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + pgdata: diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3ea9ab7 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,76 @@ +services: + + backend: + container_name: fastapi_app + build: + context: backend + dockerfile: Dockerfile + env_file: + - ./backend/backlog_app/.env + expose: + - "8000" + depends_on: + pg: + condition: service_healthy + restart: unless-stopped + networks: + - app-network + + frontend: + container_name: vite_vue_frontend + build: + context: frontend + dockerfile: Dockerfile + args: + VITE_API_URL: /api + expose: + - "80" + depends_on: + backend: + condition: service_started + restart: unless-stopped + networks: + - app-network + + pg: + image: postgres:18 + env_file: + - .env + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATA: /var/lib/postgresql/data/pgdata + expose: + - "5432" + volumes: + - pgdata:/var/lib/postgresql/data/pgdata + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + networks: + - app-network + + nginx: + image: nginx:alpine + container_name: nginx_proxy + ports: + - "80:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + depends_on: + - backend + - frontend + restart: unless-stopped + networks: + - app-network + +volumes: + pgdata: + +networks: + app-network: + driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..6877a3d --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,25 @@ +# Stage 1: Build +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +ARG VITE_API_URL +ENV VITE_API_URL=$VITE_API_URL + +RUN npm run build + +# Stage 2: Production +FROM nginx:alpine + +COPY --from=build /app/dist /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..6d95b8c --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js index 46d02f1..9baddba 100644 --- a/frontend/src/services/auth.js +++ b/frontend/src/services/auth.js @@ -1,6 +1,6 @@ import axios from 'axios' -const API_URL = 'http://127.0.0.1:8000/api' +const API_URL = import.meta.env.VITE_API_URL; class AuthService { constructor() { diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..a39d59b --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,38 @@ +events { + worker_connections 1024; +} + +http { + upstream backend { + server backend:8000; + } + + upstream frontend { + server frontend:80; + } + + server { + listen 80; + server_name _; + + allow 127.0.0.1; # Change IP + deny all; + + location / { + proxy_pass http://frontend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + } + } +}