Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions backend/backlog_app/api/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from uuid import UUID

from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
Expand Down Expand Up @@ -90,13 +89,18 @@ async def update_movie(


async def partial_update_movie(
db: AsyncSession, movie_id: int, movie_in: MovieUpdate
db: AsyncSession,
movie_id: int,
movie_in: MovieUpdate,
user: User,
) -> Movie | None:
result = await db.execute(select(Movie).where(Movie.id == movie_id))
movie = result.scalars().first()
if not movie:
raise HTTPException(status_code=404)

check_movie_ownership(movie, user)

for field, value in movie_in.model_dump(exclude_unset=True).items():
setattr(movie, field, value)

Expand Down
4 changes: 2 additions & 2 deletions backend/backlog_app/api/view/movie_view.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Annotated, List
from typing import Annotated

from fastapi import APIRouter, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession
Expand Down Expand Up @@ -63,7 +63,7 @@ async def partial_update_movie(
db: Annotated[AsyncSession, Depends(get_async_session)],
user: Annotated[User, Depends(current_active_user)],
):
return await crud.partial_update_movie(db, movie_id, movie_update)
return await crud.partial_update_movie(db, movie_id, movie_update, user)


@router.delete(
Expand Down
113 changes: 110 additions & 3 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,112 @@
# Vue 3 + Vite
# Backlog Frontend

This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Vue 3 + TypeScript фронтенд для Backlog API.

Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
## Стек

- **Vue 3** (Composition API + `<script setup>`)
- **Vite** — сборщик
- **Vue Router 4** — маршрутизация с guards
- **Pinia** — state management
- **Axios** — HTTP-клиент с интерцепторами
- **Tailwind CSS** — стилизация
- **TypeScript** — типизация

## Установка и запуск

```bash
# Установить зависимости
npm install

# Запустить dev-сервер
npm run dev

# Собрать для продакшена
npm run build
```

По умолчанию dev-сервер запустится на http://localhost:5173

API-прокси: все запросы `/api/*` проксируются на `http://localhost:8000`
Это можно изменить в `vite.config.ts`.

## Структура проекта

```
src/
├── api/
│ ├── axios.ts # настройка axios, токен + 401-редирект
│ ├── auth.ts # методы: login, register, verify, reset
│ └── movies.ts # CRUD фильмов
├── stores/
│ ├── auth.ts # пользователь, токен (localStorage), isAuthenticated
│ └── movies.ts # список фильмов, CRUD-методы
├── router/
│ └── index.ts # маршруты + guards (requiresAuth, guestOnly)
├── views/
│ ├── LandingView.vue # лэндинг
│ ├── auth/
│ │ ├── LoginView.vue
│ │ ├── RegisterView.vue
│ │ ├── ForgotPasswordView.vue
│ │ ├── ResetPasswordView.vue
│ │ ├── EmailSentView.vue # "письмо отправлено"
│ │ └── EmailVerifiedView.vue # "email подтверждён" (авто-верификация по токену)
│ └── movies/
│ ├── MovieListView.vue
│ └── MovieDetailView.vue
└── components/
├── layout/
│ ├── AppHeader.vue
│ ├── AppFooter.vue
│ └── AuthLayout.vue
└── ui/
├── BaseInput.vue # input с label, error, password-toggle
├── AlertMessage.vue # info/success/error алерт
├── MovieCard.vue # карточка фильма
└── AddMovieModal.vue # модал добавления/редактирования
```

## Маршруты

| Путь | Страница | Доступ |
|------|----------|--------|
| `/` | Лэндинг | публичный |
| `/login` | Вход | только гости |
| `/register` | Регистрация | только гости |
| `/forgot-password` | Восстановление пароля | только гости |
| `/reset-password?token=...` | Новый пароль | только гости |
| `/email-sent?email=...` | Письмо отправлено | только гости |
| `/email-verified?token=...` | Подтверждение email | публичный |
| `/movies` | Список фильмов | авторизованный |
| `/movies/:id` | Детальная страница | авторизованный |

## Обработка ошибок API

### Auth
- `LOGIN_BAD_CREDENTIALS` → «Неверный email или пароль»
- `LOGIN_USER_NOT_VERIFIED` → предупреждение + кнопка повторной отправки
- `REGISTER_USER_ALREADY_EXISTS` → ошибка поля email
- `REGISTER_INVALID_PASSWORD` → ошибка поля пароля
- `RESET_PASSWORD_BAD_TOKEN` → страница с сообщением об ошибке
- `VERIFY_USER_ALREADY_VERIFIED` → отдельное состояние

### Token
Токен хранится в `localStorage` под ключом `access_token`.
Axios автоматически добавляет его в заголовок `Authorization: Bearer`.
При 401-ответе токен очищается и пользователь редиректится на `/login`.

## Конфигурация API

Измените `target` в `vite.config.ts` если ваш бэкенд работает на другом адресе:

```ts
server: {
proxy: {
'/api': {
target: 'http://localhost:8000', // ← ваш бэкенд
changeOrigin: true,
},
},
},
```
31 changes: 15 additions & 16 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext x='50%25' y='50%25' dominant-baseline='central' text-anchor='middle' font-size='90'%3E🎬%3C/text%3E%3C/svg%3E"
>

<title>Backlog Movies</title>
</head>
<body class="h-full m-0 p-0">
<div id="app" class="h-full"></div>
<script type="module" src="/src/main.js"></script>
</body>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Backlog — ваш личный список фильмов для просмотра" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<title>Backlog — Список фильмов</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Loading
Loading