309 lines
10 KiB
Markdown
309 lines
10 KiB
Markdown
# Профиль и аватар пользователя
|
||
|
||
Эндпоинты управления профилем, загрузкой аватаров и поиска пользователей.
|
||
|
||
## Быстрый старт
|
||
|
||
```bash
|
||
# 1. Получить профиль
|
||
curl http://localhost:8000/api/v1/users/me \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# 2. Обновить ФИО и команду
|
||
curl -X PATCH http://localhost:8000/api/v1/users/me \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"name_user":"Новое имя","team_id":"uuid-team"}'
|
||
|
||
# 3. Загрузить аватар
|
||
curl -X POST http://localhost:8000/api/v1/users/me/avatar \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-F "file=@/path/to/photo.jpg"
|
||
|
||
# 4. Удалить аватар
|
||
curl -X DELETE http://localhost:8000/api/v1/users/me/avatar \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# 5. Сменить пароль
|
||
curl -X POST http://localhost:8000/api/v1/users/me/password \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"current_password":"oldPassword123","new_password":"newSecurePassword456"}'
|
||
|
||
# 6. Поиск пользователей (для пикера участников)
|
||
curl 'http://localhost:8000/api/v1/users?q=john' \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# 7. Получить справочник команд
|
||
curl http://localhost:8000/api/v1/teams \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
```
|
||
|
||
## Эндпоинты
|
||
|
||
### GET /api/v1/users/me
|
||
|
||
**Получить профиль текущего аутентифицированного пользователя.**
|
||
|
||
**Требует auth:** JWT Bearer token
|
||
|
||
**Response (200 OK):**
|
||
```json
|
||
{
|
||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"email": "user@example.com",
|
||
"name_user": "Иван Петров",
|
||
"team_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
||
"team_name": "Backend",
|
||
"avatar_path": "avatars/550e8400-e29b-41d4-a716-446655440000.jpg",
|
||
"avatar_url": "/media/avatars/550e8400-e29b-41d4-a716-446655440000.jpg",
|
||
"created_at": "2026-07-16T12:00:00Z"
|
||
}
|
||
```
|
||
|
||
**Поля:**
|
||
- `email` — адрес электронной почты (read-only)
|
||
- `name_user` — отображаемое имя (редактируемое)
|
||
- `team_id` — UUID команды (опционально, редактируемое)
|
||
- `team_name` — имя команды (null если не привязан)
|
||
- `avatar_path` — путь к файлу аватара (null если нет)
|
||
- `avatar_url` — готовый URL для отображения; при отсутствии аватара фронтенд показывает заглушку с инициалами
|
||
|
||
---
|
||
|
||
### PATCH /api/v1/users/me
|
||
|
||
**Изменить ФИО и/или команду текущего пользователя; email — read-only.**
|
||
|
||
**Требует auth:** JWT Bearer token
|
||
|
||
**Request body:** (все поля опциональны)
|
||
```json
|
||
{
|
||
"name_user": "Новое имя",
|
||
"team_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
||
}
|
||
```
|
||
|
||
**Поля:**
|
||
- `name_user` (str, 1..255) — новое ФИО
|
||
- `team_id` (UUID или null) — переназначить команду; явное `null` снимает привязку; отсутствие поля команду не трогает
|
||
|
||
**Response (200 OK):** обновленный профиль (как GET /me)
|
||
|
||
**Коды ошибок:**
|
||
- `404` — team_not_found (если передан несуществующий `team_id`)
|
||
- `422` — Unprocessable Entity (валидация: name_user пуст и т.д.)
|
||
|
||
---
|
||
|
||
### POST /api/v1/users/me/avatar
|
||
|
||
**Загрузить аватар текущего пользователя.**
|
||
|
||
**Требует auth:** JWT Bearer token
|
||
|
||
**Request body:** `multipart/form-data`
|
||
- `file` — файл изображения (обязателен)
|
||
|
||
**Требования:**
|
||
- **Формат:** JPEG, PNG или WebP (проверяется по magic bytes, не по расширению)
|
||
- **Максимальный размер:** 2 МБ
|
||
- Старый аватар автоматически удаляется при загрузке нового
|
||
|
||
**Response (200 OK):** обновленный профиль (как GET /me, с новым `avatar_url`)
|
||
|
||
**Коды ошибок:**
|
||
- `413` — avatar_too_large (файл больше 2 МБ)
|
||
- `415` — avatar_invalid_type (не JPEG/PNG/WebP; проверяется по magic bytes)
|
||
- `422` — Unprocessable Entity (невалидный multipart и т.д.)
|
||
|
||
**Примеры:**
|
||
```bash
|
||
# JPEG
|
||
curl -X POST http://localhost:8000/api/v1/users/me/avatar \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-F "file=@photo.jpg"
|
||
|
||
# PNG
|
||
curl -X POST http://localhost:8000/api/v1/users/me/avatar \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-F "file=@photo.png"
|
||
|
||
# WebP
|
||
curl -X POST http://localhost:8000/api/v1/users/me/avatar \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-F "file=@photo.webp"
|
||
```
|
||
|
||
---
|
||
|
||
### DELETE /api/v1/users/me/avatar
|
||
|
||
**Удалить аватар текущего пользователя.**
|
||
|
||
**Требует auth:** JWT Bearer token
|
||
|
||
**Response (204 No Content)** — без тела
|
||
|
||
**Примечание:** После удаления `avatar_path` становится `null`, фронтенд отображает заглушку с инициалами.
|
||
|
||
---
|
||
|
||
### POST /api/v1/users/me/password
|
||
|
||
**Сменить пароль текущего пользователя.**
|
||
|
||
**Требует auth:** JWT Bearer token
|
||
|
||
**Request body:**
|
||
```json
|
||
{
|
||
"current_password": "oldPassword123",
|
||
"new_password": "newSecurePassword456"
|
||
}
|
||
```
|
||
|
||
**Поля:**
|
||
- `current_password` (str) — текущий пароль (обязательно, для верификации)
|
||
- `new_password` (str, ≥8) — новый пароль (политика как при регистрации: минимум 8 символов)
|
||
|
||
**Response (204 No Content)** — без тела
|
||
|
||
**Коды ошибок:**
|
||
- `400` — invalid_current_password (текущий пароль не совпадает)
|
||
- `422` — Unprocessable Entity (новый пароль короче 8 символов)
|
||
|
||
**Примечание:** Refresh-сессии сознательно **не отзываются** (см. ADR-005 `docs/architecture/adr/005-password-reset-deferred.md`) — пользователь останется залогинен на других устройствах. Массовый отзыв всех сессий появится вместе со сбросом пароля по email (v0.1.0).
|
||
|
||
---
|
||
|
||
### GET /api/v1/users
|
||
|
||
**Поиск пользователей для пикера участников конференции.**
|
||
|
||
**Требует auth:** JWT Bearer token
|
||
|
||
**Query параметры:**
|
||
- `q` (str, опционально) — текст для поиска (по имени и email)
|
||
|
||
**Поведение:**
|
||
- Без `q` — возвращает полный список (с ограничением ~20 результатов)
|
||
- С `q` — полнотекстовый поиск (также ~20 результатов)
|
||
- Поиск регистронезависим
|
||
|
||
**Response (200 OK):**
|
||
```json
|
||
[
|
||
{
|
||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||
"email": "ivan@example.com",
|
||
"name_user": "Иван Петров",
|
||
"avatar_url": "/media/avatars/550e8400-e29b-41d4-a716-446655440000.jpg"
|
||
},
|
||
{
|
||
"id": "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
|
||
"email": "maria@example.com",
|
||
"name_user": "Мария Сидорова",
|
||
"avatar_url": null
|
||
}
|
||
]
|
||
```
|
||
|
||
**Примеры:**
|
||
```bash
|
||
# Полный список
|
||
curl http://localhost:8000/api/v1/users \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# Поиск по имени
|
||
curl 'http://localhost:8000/api/v1/users?q=ivan' \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
|
||
# Поиск по email
|
||
curl 'http://localhost:8000/api/v1/users?q=example.com' \
|
||
-H "Authorization: Bearer $TOKEN"
|
||
```
|
||
|
||
---
|
||
|
||
## Аватары
|
||
|
||
### Хранение
|
||
|
||
Аватары хранятся в каталоге `MEDIA_ROOT/avatars/`:
|
||
|
||
```
|
||
MEDIA_ROOT/
|
||
└── avatars/
|
||
├── 550e8400-e29b-41d4-a716-446655440000.jpg
|
||
├── 6ba7b811-9dad-11d1-80b4-00c04fd430c8.png
|
||
└── ...
|
||
```
|
||
|
||
**Адресация:**
|
||
- Backend сохраняет: `user.avatar_path = "avatars/{user_id}.{ext}"`
|
||
- Frontend отображает: `avatar_url = "/media/avatars/{user_id}.{ext}"`
|
||
- Nginx раздаёт напрямую (location `/media/` → alias `MEDIA_ROOT`)
|
||
|
||
### Валидация
|
||
|
||
**Magic bytes (не расширение):**
|
||
- JPEG: `FF D8 FF`
|
||
- PNG: `89 50 4E 47`
|
||
- WebP: `52 49 46 46 ... 57 45 42 50`
|
||
|
||
**Размер:** max 2 МБ (413 Payload Too Large)
|
||
|
||
### Заглушка (дефолт)
|
||
|
||
Если `avatar_path` = `null`:
|
||
- Фронтенд отображает цветную заглушку с инициалами (2 буквы ФИО)
|
||
- Цвет выбирается детерминированно по хэшу `user_id` (всегда одинаковый для пользователя)
|
||
|
||
---
|
||
|
||
## Типы данных
|
||
|
||
### UserProfileOut
|
||
```json
|
||
{
|
||
"id": "uuid",
|
||
"email": "string",
|
||
"name_user": "string",
|
||
"team_id": "uuid or null",
|
||
"team_name": "string or null",
|
||
"avatar_path": "string or null",
|
||
"avatar_url": "string or null",
|
||
"created_at": "datetime"
|
||
}
|
||
```
|
||
|
||
### UserListItemOut
|
||
```json
|
||
{
|
||
"id": "uuid",
|
||
"email": "string",
|
||
"name_user": "string",
|
||
"avatar_url": "string or null"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Примечания
|
||
|
||
- **Email read-only:** адрес электронной почты менять нельзя (требует переверификации, которая не реализована в v0.0.1)
|
||
- **Таймзона:** все `created_at` сохраняются в UTC (ISO 8601)
|
||
- **Команда опциональна:** пользователь может работать без привязки к команде (`team_id` = null)
|
||
- **Безопасность:** пароли никогда не передаются в API; для смены пароля используйте `POST /api/v1/users/me/password`. Сброс пароля по email появится в v0.1.0 (см. ADR-005)
|
||
|
||
---
|
||
|
||
## Ссылки
|
||
|
||
- [Справочник команд](./teams.md) — GET /api/v1/teams
|
||
- [Администраторский API](./admin.md) — GET/PATCH /api/v1/admin/users/{id}, POST /api/v1/admin/users/{id}/avatar
|
||
- [Аутентификация](./auth.md) — регистрация, логин
|
||
- [Конференции](./conferences.md) — параметр `participants` при создании/редактировании
|