diff --git a/-d b/-d deleted file mode 100644 index 155fc26..0000000 --- a/-d +++ /dev/null @@ -1 +0,0 @@ -{"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null,"url":"https://errors.pydantic.dev/2.4/v/missing"}]} \ No newline at end of file diff --git a/api_test_requests.md b/api_test_requests.md deleted file mode 100644 index 3fbe098..0000000 --- a/api_test_requests.md +++ /dev/null @@ -1,684 +0,0 @@ -# Запросы для тестирования API приложения для безопасности женщин - -В этом файле собраны примеры всех API-запросов для тестирования серверной части микросервисного приложения. - -## Содержание -- [Общие запросы](#общие-запросы) -- [Сервис пользователей (User Service)](#сервис-пользователей-user-service) -- [Сервис экстренной помощи (Emergency Service)](#сервис-экстренной-помощи-emergency-service) -- [Сервис геолокации (Location Service)](#сервис-геолокации-location-service) -- [Сервис календаря (Calendar Service)](#сервис-календаря-calendar-service) -- [Сервис уведомлений (Notification Service)](#сервис-уведомлений-notification-service) -- [API Gateway](#api-gateway) - -## Общие запросы - -### Проверка доступности всех сервисов - -```bash -# Проверка API Gateway -curl -s http://localhost:8000/health | jq - -# Проверка User Service -curl -s http://localhost:8001/health | jq - -# Проверка Emergency Service -curl -s http://localhost:8002/health | jq - -# Проверка Location Service -curl -s http://localhost:8003/health | jq - -# Проверка Calendar Service -curl -s http://localhost:8004/health | jq - -# Проверка Notification Service -curl -s http://localhost:8005/health | jq - -# Альтернативные endpoint health -curl -s http://localhost:8000/api/v1/health | jq -curl -s http://localhost:8001/api/v1/health | jq -curl -s http://localhost:8002/api/v1/health | jq -curl -s http://localhost:8003/api/v1/health | jq -curl -s http://localhost:8004/api/v1/health | jq -curl -s http://localhost:8005/api/v1/health | jq -``` - -## Сервис пользователей (User Service) - -### Регистрация нового пользователя - -```bash -curl -X POST http://localhost:8001/api/v1/users/register \ - -H "Content-Type: application/json" \ - -d '{ - "username": "usertest1", - "email": "user1@example.com", - "password": "SecurePass123!", - "full_name": "Тест Пользователь", - "phone_number": "+79001234567" - }' | jq -``` - -### Авторизация пользователя - -```bash -curl -X POST http://localhost:8001/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "usertest1", - "password": "SecurePass123!" - }' | jq - -# Сохраните полученный токен в переменную для дальнейшего использования -# TOKEN=$(curl -s -X POST http://localhost:8001/api/v1/auth/login -H "Content-Type: application/json" -d '{"username": "usertest1", "password": "SecurePass123!"}' | jq -r '.access_token') -``` - -### Получение информации о текущем пользователе - -```bash -curl -X GET http://localhost:8001/api/v1/users/me \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Обновление профиля пользователя - -```bash -curl -X PATCH http://localhost:8001/api/v1/users/me \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "full_name": "Новое Имя Пользователя", - "phone_number": "+79009876543" - }' | jq -``` - -### Добавление контактов для экстренной связи - -```bash -curl -X POST http://localhost:8001/api/v1/users/me/emergency-contacts \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "name": "Контакт для экстренной связи", - "phone_number": "+79001112233", - "relationship": "Друг" - }' | jq -``` - -### Получение списка контактов для экстренной связи - -```bash -curl -X GET http://localhost:8001/api/v1/users/me/emergency-contacts \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Удаление контакта для экстренной связи - -```bash -# Замените {contact_id} на реальный ID контакта -curl -X DELETE http://localhost:8001/api/v1/users/me/emergency-contacts/{contact_id} \ - -H "Authorization: Bearer $TOKEN" -``` - -### Изменение пароля - -```bash -curl -X POST http://localhost:8001/api/v1/users/me/change-password \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "current_password": "SecurePass123!", - "new_password": "NewSecurePass456!" - }' | jq -``` - -## Сервис экстренной помощи (Emergency Service) - -### Отправка сигнала SOS - -```bash -curl -X POST http://localhost:8002/api/v1/emergency/alerts \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "latitude": 55.7558, - "longitude": 37.6173, - "alert_type": "SOS", - "message": "Нужна срочная помощь!" - }' | jq - -# Сохраните ID созданного оповещения для дальнейших действий -# ALERT_ID=$(curl -s -X POST http://localhost:8002/api/v1/emergency/alerts -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d '{"latitude": 55.7558, "longitude": 37.6173, "alert_type": "SOS", "message": "Нужна срочная помощь!"}' | jq -r '.id') -``` - -### Получение активных оповещений пользователя - -```bash -curl -X GET http://localhost:8002/api/v1/emergency/alerts/my \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Отмена сигнала SOS - -```bash -# Замените {alert_id} на ID полученного ранее оповещения -curl -X PATCH http://localhost:8002/api/v1/emergency/alerts/{alert_id}/cancel \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Получение списка ближайших оповещений (для волонтеров/служб) - -```bash -curl -X GET "http://localhost:8002/api/v1/emergency/alerts/nearby?latitude=55.7558&longitude=37.6173&radius=1.0" \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Отправка отчета о происшествии - -```bash -curl -X POST http://localhost:8002/api/v1/emergency/reports \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "latitude": 55.7558, - "longitude": 37.6173, - "report_type": "UNSAFE_AREA", - "description": "Тёмная улица без освещения", - "severity_level": "MEDIUM" - }' | jq -``` - -### Получение списка отчетов о происшествиях в определенном радиусе - -```bash -curl -X GET "http://localhost:8002/api/v1/emergency/reports/nearby?latitude=55.7558&longitude=37.6173&radius=5.0" \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -## Сервис геолокации (Location Service) - -### Обновление местоположения пользователя - -```bash -curl -X POST http://localhost:8003/api/v1/locations/update \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "latitude": 55.7558, - "longitude": 37.6173, - "accuracy": 10.0 - }' | jq -``` - -### Получение последнего известного местоположения пользователя - -```bash -curl -X GET http://localhost:8003/api/v1/locations/last \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Получение истории перемещений пользователя - -```bash -curl -X GET "http://localhost:8003/api/v1/locations/history?start_date=2025-09-20T00:00:00Z&end_date=2025-09-25T23:59:59Z" \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Получение списка безопасных мест поблизости - -```bash -curl -X GET "http://localhost:8003/api/v1/locations/safe-places?latitude=55.7558&longitude=37.6173&radius=1.0" \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Добавление безопасного места - -```bash -curl -X POST http://localhost:8003/api/v1/locations/safe-places \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "name": "Полицейский участок", - "latitude": 55.7559, - "longitude": 37.6174, - "place_type": "POLICE", - "description": "Центральный полицейский участок" - }' | jq -``` - -### Поиск пользователей в определенном радиусе (для экстренных ситуаций) - -```bash -curl -X GET "http://localhost:8003/api/v1/locations/users/nearby?latitude=55.7558&longitude=37.6173&radius=0.5" \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -## Сервис календаря (Calendar Service) - -### Создание записи в календаре - -```bash -curl -X POST http://localhost:8004/api/v1/calendar/entries \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "entry_date": "2025-10-01", - "cycle_day": 1, - "symptoms": ["HEADACHE", "FATIGUE"], - "mood": "NORMAL", - "notes": "Начало цикла" - }' | jq -``` - -### Получение записей календаря за период - -```bash -curl -X GET "http://localhost:8004/api/v1/calendar/entries?start_date=2025-09-01&end_date=2025-10-31" \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Обновление записи в календаре - -```bash -# Замените {entry_id} на реальный ID записи -curl -X PATCH http://localhost:8004/api/v1/calendar/entries/{entry_id} \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "symptoms": ["HEADACHE", "FATIGUE", "BLOATING"], - "mood": "IRRITABLE", - "notes": "Обновленная запись" - }' | jq -``` - -### Получение статистики и прогнозов - -```bash -curl -X GET http://localhost:8004/api/v1/calendar/statistics \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Получение прогноза следующего цикла - -```bash -curl -X GET http://localhost:8004/api/v1/calendar/predictions/next-cycle \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Настройка параметров календаря - -```bash -curl -X POST http://localhost:8004/api/v1/calendar/settings \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "average_cycle_length": 28, - "average_period_length": 5, - "notification_enabled": true, - "notification_days_before": 2 - }' | jq -``` - -### Получение настроек календаря - -```bash -curl -X GET http://localhost:8004/api/v1/calendar/settings \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -## Сервис уведомлений (Notification Service) - -### Регистрация устройства для Push-уведомлений - -```bash -curl -X POST http://localhost:8005/api/v1/notifications/devices \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "device_token": "fcm-token-example-123456", - "device_type": "ANDROID", - "app_version": "1.0.0" - }' | jq -``` - -### Получение списка зарегистрированных устройств - -```bash -curl -X GET http://localhost:8005/api/v1/notifications/devices \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Удаление регистрации устройства - -```bash -# Замените {device_id} на реальный ID устройства -curl -X DELETE http://localhost:8005/api/v1/notifications/devices/{device_id} \ - -H "Authorization: Bearer $TOKEN" -``` - -### Настройка предпочтений уведомлений - -```bash -curl -X POST http://localhost:8005/api/v1/notifications/preferences \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "emergency_alerts": true, - "nearby_incidents": true, - "calendar_reminders": true, - "system_notifications": true - }' | jq -``` - -### Получение настроек уведомлений - -```bash -curl -X GET http://localhost:8005/api/v1/notifications/preferences \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Отправка тестового уведомления (только для тестирования) - -```bash -curl -X POST http://localhost:8005/api/v1/notifications/test \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "title": "Тестовое уведомление", - "body": "Это тестовое push-уведомление", - "data": { - "type": "TEST", - "action": "OPEN_APP" - } - }' | jq -``` - -### Получение истории уведомлений - -```bash -curl -X GET http://localhost:8005/api/v1/notifications/history \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -## API Gateway - -### Проверка статуса всех сервисов через Gateway - -```bash -curl -X GET http://localhost:8000/api/v1/status \ - -H "Authorization: Bearer $TOKEN" | jq -``` - -### Регистрация через Gateway - -```bash -curl -X POST http://localhost:8000/api/v1/auth/register \ - -H "Content-Type: application/json" \ - -d '{ - "username": "gatewayuser", - "email": "gateway@example.com", - "password": "GatewayPass123!", - "full_name": "Gateway Test User", - "phone_number": "+79991234567" - }' | jq -``` - -### Авторизация через Gateway - -```bash -curl -X POST http://localhost:8000/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "gatewayuser", - "password": "GatewayPass123!" - }' | jq - -# Сохраните полученный токен в переменную для дальнейшего использования -# GW_TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/login -H "Content-Type: application/json" -d '{"username": "gatewayuser", "password": "GatewayPass123!"}' | jq -r '.access_token') -``` - -### Создание экстренного оповещения через Gateway - -```bash -curl -X POST http://localhost:8000/api/v1/emergency/alerts \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $GW_TOKEN" \ - -d '{ - "latitude": 55.7558, - "longitude": 37.6173, - "alert_type": "SOS", - "message": "Тестовое экстренное оповещение через Gateway" - }' | jq -``` - -### Комплексный запрос данных пользователя через Gateway - -```bash -curl -X GET http://localhost:8000/api/v1/users/dashboard \ - -H "Authorization: Bearer $GW_TOKEN" | jq -``` - -## Bash скрипт для полного тестирования API - -Создайте файл `full_api_test.sh` со следующим содержимым: - -```bash -#!/bin/bash - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo -e "${YELLOW}=======================================${NC}" -echo -e "${YELLOW}🔍 Полное тестирование API приложения ${NC}" -echo -e "${YELLOW}=======================================${NC}" - -# 1. Проверка доступности сервисов -echo -e "\n${GREEN}1. Проверка доступности сервисов${NC}" - -services=( - "http://localhost:8000" - "http://localhost:8001" - "http://localhost:8002" - "http://localhost:8003" - "http://localhost:8004" - "http://localhost:8005" -) - -service_names=( - "API Gateway" - "User Service" - "Emergency Service" - "Location Service" - "Calendar Service" - "Notification Service" -) - -for i in ${!services[@]}; do - echo -n "Проверка ${service_names[$i]} (${services[$i]})... " - if curl -s "${services[$i]}/health" | grep -q "status.*healthy" || curl -s "${services[$i]}/api/v1/health" | grep -q "status.*healthy"; then - echo -e "${GREEN}OK${NC}" - else - echo -e "${RED}НЕДОСТУПНО${NC}" - echo "Продолжение тестирования может вызвать ошибки. Хотите продолжить? (y/n)" - read continue_test - if [[ $continue_test != "y" ]]; then - exit 1 - fi - fi -done - -# 2. Регистрация и авторизация -echo -e "\n${GREEN}2. Регистрация и авторизация${NC}" - -echo "Регистрация тестового пользователя..." -REGISTER_RESPONSE=$(curl -s -X POST http://localhost:8001/api/v1/users/register \ - -H "Content-Type: application/json" \ - -d '{ - "username": "apitest", - "email": "apitest@example.com", - "password": "ApiTest123!", - "full_name": "API Test User", - "phone_number": "+79997776655" - }') - -echo $REGISTER_RESPONSE | jq - -# Авторизация -echo "Авторизация тестового пользователя..." -LOGIN_RESPONSE=$(curl -s -X POST http://localhost:8001/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "apitest", - "password": "ApiTest123!" - }') - -echo $LOGIN_RESPONSE | jq - -TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.access_token') -if [[ $TOKEN == "null" || -z $TOKEN ]]; then - echo -e "${RED}Не удалось получить токен авторизации. Тестирование будет остановлено.${NC}" - exit 1 -else - echo -e "${GREEN}Успешно получен токен авторизации${NC}" -fi - -# 3. Обновление профиля -echo -e "\n${GREEN}3. Обновление профиля пользователя${NC}" -curl -s -X PATCH http://localhost:8001/api/v1/users/me \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "full_name": "Обновленное Имя", - "phone_number": "+79997776655" - }' | jq - -# 4. Добавление контактов экстренной связи -echo -e "\n${GREEN}4. Добавление контакта для экстренной связи${NC}" -CONTACT_RESPONSE=$(curl -s -X POST http://localhost:8001/api/v1/users/me/emergency-contacts \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "name": "Экстренный контакт", - "phone_number": "+79991112233", - "relationship": "Родственник" - }') - -echo $CONTACT_RESPONSE | jq - -CONTACT_ID=$(echo $CONTACT_RESPONSE | jq -r '.id') - -# 5. Получение списка контактов -echo -e "\n${GREEN}5. Получение списка экстренных контактов${NC}" -curl -s -X GET http://localhost:8001/api/v1/users/me/emergency-contacts \ - -H "Authorization: Bearer $TOKEN" | jq - -# 6. Обновление местоположения пользователя -echo -e "\n${GREEN}6. Обновление местоположения пользователя${NC}" -curl -s -X POST http://localhost:8003/api/v1/locations/update \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "latitude": 55.7558, - "longitude": 37.6173, - "accuracy": 10.0 - }' | jq - -# 7. Получение последнего местоположения -echo -e "\n${GREEN}7. Получение последнего местоположения${NC}" -curl -s -X GET http://localhost:8003/api/v1/locations/last \ - -H "Authorization: Bearer $TOKEN" | jq - -# 8. Создание экстренного оповещения -echo -e "\n${GREEN}8. Создание экстренного оповещения${NC}" -ALERT_RESPONSE=$(curl -s -X POST http://localhost:8002/api/v1/emergency/alerts \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "latitude": 55.7558, - "longitude": 37.6173, - "alert_type": "SOS", - "message": "Тестовое экстренное оповещение" - }') - -echo $ALERT_RESPONSE | jq - -ALERT_ID=$(echo $ALERT_RESPONSE | jq -r '.id') - -# 9. Получение активных оповещений -echo -e "\n${GREEN}9. Получение активных оповещений пользователя${NC}" -curl -s -X GET http://localhost:8002/api/v1/emergency/alerts/my \ - -H "Authorization: Bearer $TOKEN" | jq - -# 10. Отмена экстренного оповещения -if [[ $ALERT_ID != "null" && -n $ALERT_ID ]]; then - echo -e "\n${GREEN}10. Отмена экстренного оповещения${NC}" - curl -s -X PATCH http://localhost:8002/api/v1/emergency/alerts/$ALERT_ID/cancel \ - -H "Authorization: Bearer $TOKEN" | jq -fi - -# 11. Создание записи в календаре -echo -e "\n${GREEN}11. Создание записи в календаре${NC}" -CALENDAR_RESPONSE=$(curl -s -X POST http://localhost:8004/api/v1/calendar/entries \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "entry_date": "2025-10-01", - "cycle_day": 1, - "symptoms": ["HEADACHE", "FATIGUE"], - "mood": "NORMAL", - "notes": "Тестовая запись в календаре" - }') - -echo $CALENDAR_RESPONSE | jq - -# 12. Получение записей календаря -echo -e "\n${GREEN}12. Получение записей календаря${NC}" -curl -s -X GET "http://localhost:8004/api/v1/calendar/entries?start_date=2025-09-01&end_date=2025-10-31" \ - -H "Authorization: Bearer $TOKEN" | jq - -# 13. Регистрация устройства для уведомлений -echo -e "\n${GREEN}13. Регистрация устройства для уведомлений${NC}" -DEVICE_RESPONSE=$(curl -s -X POST http://localhost:8005/api/v1/notifications/devices \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "device_token": "fcm-test-token-123", - "device_type": "ANDROID", - "app_version": "1.0.0" - }') - -echo $DEVICE_RESPONSE | jq - -# 14. Настройка предпочтений уведомлений -echo -e "\n${GREEN}14. Настройка предпочтений уведомлений${NC}" -curl -s -X POST http://localhost:8005/api/v1/notifications/preferences \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{ - "emergency_alerts": true, - "nearby_incidents": true, - "calendar_reminders": true, - "system_notifications": true - }' | jq - -# 15. Получение данных пользователя через Gateway -echo -e "\n${GREEN}15. Получение данных пользователя через Gateway${NC}" -curl -s -X GET http://localhost:8000/api/v1/users/dashboard \ - -H "Authorization: Bearer $TOKEN" | jq - -echo -e "\n${YELLOW}=======================================${NC}" -echo -e "${GREEN}✅ Тестирование API завершено успешно${NC}" -echo -e "${YELLOW}=======================================${NC}" -``` - -## Использование Bash скрипта для полного тестирования API - -Для запуска скрипта выполните следующие команды: - -```bash -chmod +x full_api_test.sh -./full_api_test.sh -``` \ No newline at end of file diff --git a/api_test_results.md b/api_test_results.md deleted file mode 100644 index d2076b3..0000000 --- a/api_test_results.md +++ /dev/null @@ -1,40 +0,0 @@ -# Результаты тестирования API -Дата: 2025-09-25 14:57:40 -## Итоги -- Всего тестов: 32 -- Успешно: 13 -- Ошибок: 19 - -## Подробности -✓ УСПЕШНО: Сервис API Gateway доступен -✓ УСПЕШНО: Сервис User Service доступен -✓ УСПЕШНО: Сервис Emergency Service доступен -✓ УСПЕШНО: Сервис Location Service доступен -✓ УСПЕШНО: Сервис Calendar Service доступен -✓ УСПЕШНО: Сервис Notification Service доступен -✗ ОШИБКА: Регистрация пользователя -✗ ОШИБКА: Проверка на дублирование пользователя (должна быть ошибка) -✓ УСПЕШНО: Получение токена авторизации -✗ ОШИБКА: Проверка авторизации с неверным паролем (должна быть ошибка) -✓ УСПЕШНО: Получение профиля пользователя -✓ УСПЕШНО: Получение профиля через альтернативный эндпоинт -✗ ОШИБКА: Обновление профиля пользователя -✗ ОШИБКА: Проверка обновления имени в профиле -✗ ОШИБКА: Добавление экстренного контакта -✓ УСПЕШНО: Получение списка экстренных контактов (найдено: 1) -✗ ОШИБКА: Обновление местоположения -✗ ОШИБКА: Получение последнего местоположения -✓ УСПЕШНО: Получение истории местоположений -✗ ОШИБКА: Создание экстренного оповещения -✓ УСПЕШНО: Получение активных оповещений (найдено: 1) -✗ ОШИБКА: Получение статистики по оповещениям -✗ ОШИБКА: Создание записи в календаре -✓ УСПЕШНО: Получение записей календаря (найдено: 1) -✗ ОШИБКА: Получение прогноза цикла -✗ ОШИБКА: Регистрация устройства для уведомлений -✗ ОШИБКА: Настройка предпочтений уведомлений -✗ ОШИБКА: Получение предпочтений уведомлений -✗ ОШИБКА: Отправка тестового уведомления -✗ ОШИБКА: Получение данных пользователя через API Gateway -✗ ОШИБКА: Получение OpenAPI схемы -✗ ОШИБКА: Проверка наличия схем данных diff --git a/docs/DATA_SCHEMAS.md b/docs/DATA_SCHEMAS.md new file mode 100644 index 0000000..1735944 --- /dev/null +++ b/docs/DATA_SCHEMAS.md @@ -0,0 +1,617 @@ +# Полные схемы данных для мобильного приложения Women's Safety App + +## 📋 Содержание +1. [Схемы авторизации](#схемы-авторизации) +2. [Схемы пользователей](#схемы-пользователей) +3. [Схемы экстренных ситуаций](#схемы-экстренных-ситуаций) +4. [Схемы местоположения](#схемы-местоположения) +5. [Схемы уведомлений](#схемы-уведомлений) +6. [Схемы календаря](#схемы-календаря) +7. [TypeScript интерфейсы](#typescript-интерфейсы) + +--- + +## 🔐 Схемы авторизации + +### UserRegister +```json +{ + "username": "string", // Имя пользователя (уникальное) + "email": "string", // Email (уникальный) + "password": "string", // Пароль (мин 8 символов) + "full_name": "string?", // Полное имя + "phone": "string?", // Номер телефона + "date_of_birth": "date?", // Дата рождения (YYYY-MM-DD) + "bio": "string?" // Биография (макс 500 символов) +} +``` + +### UserLogin +```json +{ + "username": "string?", // Имя пользователя ИЛИ + "email": "string?", // Email (один из двух обязателен) + "password": "string" // Пароль +} +``` + +### TokenResponse +```json +{ + "access_token": "string", // JWT токен + "token_type": "bearer" // Тип токена +} +``` + +--- + +## 👤 Схемы пользователей + +### UserProfile (полная информация) +```json +{ + "id": 123, // int, ID пользователя + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID пользователя + "username": "testuser", // string, имя пользователя + "email": "test@example.com", // string, email + "phone": "+1234567890", // string?, телефон + "first_name": "John", // string?, имя + "last_name": "Doe", // string?, фамилия + "date_of_birth": "1990-01-01", // date?, дата рождения + "bio": "Краткая биография", // string?, биография + "avatar_url": "https://...", // string?, URL аватара + "location_sharing_enabled": true, // bool, разрешение на геолокацию + "emergency_notifications_enabled": true, // bool, экстренные уведомления + "push_notifications_enabled": true, // bool, push уведомления + "email_verified": false, // bool, email подтвержден + "phone_verified": true, // bool, телефон подтвержден + "is_active": true, // bool, активен ли аккаунт + "created_at": "2024-01-01T10:00:00Z", // datetime, дата регистрации + "updated_at": "2024-01-15T10:00:00Z" // datetime, последнее обновление +} +``` + +### UserDashboard +```json +{ + "user_info": { // Краткая информация о пользователе + "id": 123, + "username": "testuser", + "email": "test@example.com", + "first_name": "John", + "last_name": "Doe" + }, + "recent_alerts": [ // Последние оповещения (макс 5) + { + "id": 456, + "alert_type": "general", + "message": "Нужна помощь", + "created_at": "2024-01-15T09:30:00Z", + "is_resolved": false, + "responded_users_count": 2 + } + ], + "emergency_contacts": [ // Экстренные контакты + { + "id": 789, + "name": "Мама", + "phone_number": "+1234567890", + "relationship": "mother" + } + ], + "safety_stats": { // Статистика безопасности + "total_alerts_created": 5, + "total_responses_given": 12, + "safety_checks_count": 45, + "last_safety_check": "2024-01-15T08:00:00Z" + } +} +``` + +### EmergencyContact +```json +{ + "id": 789, // int, ID контакта + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID контакта + "user_id": 123, // int, ID владельца + "name": "Мама", // string, имя контакта + "phone_number": "+1234567890", // string, телефон + "relationship": "mother", // string?, отношение (mother, father, spouse, friend, etc.) + "notes": "Основной контакт", // string?, заметки + "is_active": true, // bool, активен ли контакт + "created_at": "2024-01-01T10:00:00Z", // datetime, дата создания + "updated_at": "2024-01-15T10:00:00Z" // datetime, последнее обновление +} +``` + +--- + +## 🆘 Схемы экстренных ситуаций + +### EmergencyAlert (полная схема) +```json +{ + "id": 123, // int, ID оповещения + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID оповещения + "user_id": 26, // int, ID создателя + "latitude": 55.7558, // float, широта (-90 до 90) + "longitude": 37.6176, // float, долгота (-180 до 180) + "address": "Красная площадь, Москва", // string?, адрес + "alert_type": "general", // AlertType, тип оповещения + "status": "active", // AlertStatus, текущий статус + "message": "Нужна помощь", // string?, описание ситуации + "is_resolved": false, // bool, решено ли + "resolved_at": null, // datetime?, время решения + "resolved_by": null, // int?, кем решено + "resolved_notes": null, // string?, заметки о решении + "contact_emergency_services": true, // bool, связаться со службами + "notify_emergency_contacts": true, // bool, уведомить контакты + "notified_users_count": 5, // int, количество уведомленных + "responded_users_count": 2, // int, количество откликнувшихся + "created_at": "2024-01-15T10:30:00Z", // datetime, время создания + "updated_at": "2024-01-15T10:35:00Z" // datetime, последнее обновление +} +``` + +### EmergencyResponse (отклик на оповещение) +```json +{ + "id": 456, // int, ID отклика + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID отклика + "alert_id": 123, // int, ID оповещения + "user_id": 27, // int, ID откликнувшегося + "response_type": "help_on_way", // ResponseType, тип отклика + "message": "Еду к вам!", // string?, сообщение + "eta_minutes": 15, // int?, время прибытия в минутах + "location_sharing": true, // bool, делиться местоположением + "created_at": "2024-01-15T10:32:00Z" // datetime, время создания +} +``` + +### EmergencyReport (отчет о происшествии) +```json +{ + "id": 789, // int, ID отчета + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID отчета + "user_id": 26, // int?, ID автора (null для анонимных) + "latitude": 55.7558, // float, широта + "longitude": 37.6176, // float, долгота + "address": "Тверская улица", // string?, адрес + "report_type": "harassment", // string, тип происшествия + "description": "Подробное описание...", // string, описание (10-1000 символов) + "is_anonymous": false, // bool, анонимный отчет + "severity": 4, // int, серьезность (1-5) + "status": "pending", // string, статус (pending/investigating/resolved) + "created_at": "2024-01-15T10:45:00Z" // datetime, время создания +} +``` + +### SafetyCheck (отметка безопасности) +```json +{ + "id": 101, // int, ID отметки + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID отметки + "user_id": 26, // int, ID пользователя + "message": "Добрался домой безопасно", // string?, сообщение + "location_latitude": 55.7600, // float?, широта + "location_longitude": 37.6100, // float?, долгота + "created_at": "2024-01-15T22:00:00Z" // datetime, время создания +} +``` + +### EmergencyStatistics +```json +{ + "total_alerts": 150, // int, общее количество оповещений + "active_alerts": 12, // int, активные оповещения + "resolved_alerts": 138, // int, решенные оповещения + "total_responders": 89, // int, всего откликнувшихся + "avg_response_time_minutes": 8.5 // float, среднее время отклика +} +``` + +### NearbyAlert (ближайшие оповещения) +```json +{ + "id": 123, // int, ID оповещения + "alert_type": "medical", // string, тип оповещения + "latitude": 55.7558, // float, широта + "longitude": 37.6176, // float, долгота + "address": "Больница №1", // string?, адрес + "distance_km": 2.5, // float, расстояние в километрах + "created_at": "2024-01-15T09:15:00Z", // datetime, время создания + "responded_users_count": 3 // int, количество откликов +} +``` + +--- + +## 📍 Схемы местоположения + +### LocationUpdate +```json +{ + "latitude": 55.7558, // float, широта (-90 до 90) + "longitude": 37.6176, // float, долгота (-180 до 180) + "accuracy": 10.0, // float?, точность в метрах + "altitude": 150.0, // float?, высота в метрах + "speed": 0.0, // float?, скорость м/с + "heading": 90.0, // float?, направление (0-360 градусов) + "timestamp": "2024-01-15T10:30:00Z" // datetime?, время получения координат +} +``` + +### NearbyUser +```json +{ + "user_id": 27, // int, ID пользователя + "distance_meters": 500.0, // float, расстояние в метрах + "latitude": 55.7568, // float, широта (может быть приблизительной) + "longitude": 37.6186, // float, долгота (может быть приблизительной) + "last_seen": "2024-01-15T10:25:00Z", // datetime, последнее обновление местоположения + "is_available": true // bool, готов ли помочь +} +``` + +### GeocodeResult +```json +{ + "address": "Красная площадь, 1, Москва, Россия", // string, полный адрес + "street": "Красная площадь", // string?, улица + "house_number": "1", // string?, номер дома + "city": "Москва", // string?, город + "state": "Москва", // string?, регион/область + "country": "Россия", // string?, страна + "postal_code": "109012", // string?, почтовый индекс + "latitude": 55.7558, // float, широта + "longitude": 37.6176 // float, долгота +} +``` + +--- + +## 🔔 Схемы уведомлений + +### NotificationCreate +```json +{ + "user_id": 123, // int, ID получателя + "title": "Экстренное оповещение", // string, заголовок + "message": "Новое оповещение рядом с вами", // string, текст сообщения + "type": "emergency_alert", // string, тип уведомления + "data": { // object?, дополнительные данные + "alert_id": 456, + "latitude": 55.7558, + "longitude": 37.6176 + }, + "priority": "high", // string?, приоритет (low/normal/high/urgent) + "schedule_at": null // datetime?, время отправки (null = сейчас) +} +``` + +### PushTokenRegistration +```json +{ + "token": "fcm_or_apns_token_here", // string, токен устройства + "platform": "ios", // string, платформа (ios/android) + "app_version": "1.0.0", // string?, версия приложения + "device_info": { // object?, информация об устройстве + "model": "iPhone 14", + "os_version": "17.0" + } +} +``` + +### NotificationHistory +```json +{ + "id": 789, // int, ID уведомления + "user_id": 123, // int, ID получателя + "title": "Экстренное оповещение", // string, заголовок + "message": "Текст уведомления", // string, сообщение + "type": "emergency_alert", // string, тип + "status": "delivered", // string, статус (sent/delivered/read/failed) + "sent_at": "2024-01-15T10:30:00Z", // datetime, время отправки + "delivered_at": "2024-01-15T10:30:05Z", // datetime?, время доставки + "read_at": null // datetime?, время прочтения +} +``` + +--- + +## 📅 Схемы календаря + +### CalendarEntry +```json +{ + "id": 456, // int, ID записи + "user_id": 123, // int, ID пользователя + "date": "2024-01-15", // date, дата записи (YYYY-MM-DD) + "entry_type": "period_start", // string, тип записи + "notes": "Болезненные ощущения", // string?, заметки + "mood_score": 3, // int?, настроение (1-5) + "energy_level": 4, // int?, уровень энергии (1-5) + "symptoms": [ // array?, симптомы + "headache", + "fatigue", + "cramps" + ], + "flow_intensity": "medium", // string?, интенсивность (light/medium/heavy) + "temperature": 36.6, // float?, температура тела + "weight": 65.5, // float?, вес + "created_at": "2024-01-15T08:00:00Z", // datetime, время создания + "updated_at": "2024-01-15T08:05:00Z" // datetime, последнее обновление +} +``` + +### CalendarAnalytics +```json +{ + "cycle_length": 28, // int?, средняя длина цикла + "period_length": 5, // int?, средняя длина месячных + "next_period_prediction": "2024-02-10", // date?, прогноз следующих месячных + "fertility_window": { // object?, окно фертильности + "start": "2024-01-20", + "end": "2024-01-25" + }, + "mood_trends": { // object?, тренды настроения + "average_score": 3.5, + "lowest_day": 2, + "highest_day": 12 + }, + "symptoms_frequency": { // object?, частота симптомов + "headache": 0.3, + "cramps": 0.8, + "fatigue": 0.6 + } +} +``` + +--- + +## 📱 TypeScript интерфейсы + +```typescript +// Перечисления +export enum AlertType { + GENERAL = 'general', + MEDICAL = 'medical', + VIOLENCE = 'violence', + HARASSMENT = 'harassment', + UNSAFE_AREA = 'unsafe_area', + ACCIDENT = 'accident', + FIRE = 'fire', + NATURAL_DISASTER = 'natural_disaster' +} + +export enum AlertStatus { + ACTIVE = 'active', + RESOLVED = 'resolved', + CANCELLED = 'cancelled', + INVESTIGATING = 'investigating' +} + +export enum ResponseType { + HELP_ON_WAY = 'help_on_way', + CONTACTED_AUTHORITIES = 'contacted_authorities', + SAFE_NOW = 'safe_now', + FALSE_ALARM = 'false_alarm', + INVESTIGATING = 'investigating', + RESOLVED = 'resolved' +} + +// Интерфейсы авторизации +export interface UserRegister { + username: string; + email: string; + password: string; + full_name?: string; + phone?: string; + date_of_birth?: string; + bio?: string; +} + +export interface UserLogin { + username?: string; + email?: string; + password: string; +} + +export interface TokenResponse { + access_token: string; + token_type: string; +} + +// Интерфейсы пользователя +export interface UserProfile { + id: number; + uuid: string; + username: string; + email: string; + phone?: string; + first_name?: string; + last_name?: string; + date_of_birth?: string; + bio?: string; + avatar_url?: string; + location_sharing_enabled: boolean; + emergency_notifications_enabled: boolean; + push_notifications_enabled: boolean; + email_verified: boolean; + phone_verified: boolean; + is_active: boolean; + created_at: string; + updated_at?: string; +} + +export interface EmergencyContact { + id: number; + uuid: string; + user_id: number; + name: string; + phone_number: string; + relationship?: string; + notes?: string; + is_active: boolean; + created_at: string; + updated_at?: string; +} + +// Интерфейсы экстренных ситуаций +export interface EmergencyAlertCreate { + latitude: number; + longitude: number; + alert_type: AlertType; + message?: string; + address?: string; + contact_emergency_services?: boolean; + notify_emergency_contacts?: boolean; +} + +export interface EmergencyAlert { + id: number; + uuid: string; + user_id: number; + latitude: number; + longitude: number; + address?: string; + alert_type: AlertType; + status: AlertStatus; + message?: string; + is_resolved: boolean; + resolved_at?: string; + resolved_by?: number; + resolved_notes?: string; + contact_emergency_services: boolean; + notify_emergency_contacts: boolean; + notified_users_count: number; + responded_users_count: number; + created_at: string; + updated_at?: string; +} + +export interface EmergencyResponseCreate { + response_type: ResponseType; + message?: string; + eta_minutes?: number; + location_sharing?: boolean; +} + +export interface EmergencyResponse { + id: number; + uuid: string; + alert_id: number; + user_id: number; + response_type: ResponseType; + message?: string; + eta_minutes?: number; + location_sharing: boolean; + created_at: string; +} + +export interface EmergencyStatistics { + total_alerts: number; + active_alerts: number; + resolved_alerts: number; + total_responders: number; + avg_response_time_minutes: number; +} + +export interface NearbyAlert { + id: number; + alert_type: string; + latitude: number; + longitude: number; + address?: string; + distance_km: number; + created_at: string; + responded_users_count: number; +} + +// Интерфейсы местоположения +export interface LocationUpdate { + latitude: number; + longitude: number; + accuracy?: number; + altitude?: number; + speed?: number; + heading?: number; + timestamp?: string; +} + +export interface NearbyUser { + user_id: number; + distance_meters: number; + latitude: number; + longitude: number; + last_seen: string; + is_available: boolean; +} + +// Интерфейсы уведомлений +export interface PushTokenRegistration { + token: string; + platform: 'ios' | 'android'; + app_version?: string; + device_info?: { + model?: string; + os_version?: string; + }; +} + +// API клиент +export interface APIClient { + setToken(token: string): void; + login(username: string, password: string): Promise; + register(userData: UserRegister): Promise; + getProfile(): Promise; + createAlert(alertData: EmergencyAlertCreate): Promise; + getNearbyAlerts(lat: number, lng: number, radius?: number): Promise; + respondToAlert(alertId: number, response: EmergencyResponseCreate): Promise; + updateLocation(location: LocationUpdate): Promise; + createSafetyCheck(data: { message?: string; location_latitude?: number; location_longitude?: number }): Promise; +} +``` + +--- + +## 📋 Валидация данных + +### Ограничения полей +- **Координаты**: latitude (-90 до 90), longitude (-180 до 180) +- **Пароль**: минимум 8 символов, максимум 70 (для совместимости с bcrypt) +- **Email**: валидный формат email +- **Телефон**: рекомендуется международный формат (+1234567890) +- **Сообщения**: максимум 500 символов для alert message, 200 для safety check +- **Описания**: 10-1000 символов для emergency reports + +### Обязательные поля +- При регистрации: username, email, password +- При создании оповещения: latitude, longitude, alert_type +- При отклике: response_type +- При обновлении местоположения: latitude, longitude + +### Рекомендации по обработке ошибок +```typescript +interface APIError { + detail: string; + status_code: number; + field_errors?: { + [field: string]: string[]; + }; +} + +// Обработка ошибок валидации +try { + await api.createAlert(alertData); +} catch (error) { + if (error.status_code === 422) { + // Показать ошибки валидации для каждого поля + Object.entries(error.field_errors || {}).forEach(([field, errors]) => { + console.error(`${field}: ${errors.join(', ')}`); + }); + } +} +``` + +Эти схемы данных обеспечивают полную интеграцию мобильного приложения с Women's Safety App API. \ No newline at end of file diff --git a/docs/EMERGENCY_SERVICE_API.md b/docs/EMERGENCY_SERVICE_API.md new file mode 100644 index 0000000..b19ecd7 --- /dev/null +++ b/docs/EMERGENCY_SERVICE_API.md @@ -0,0 +1,600 @@ +# Emergency Service API Документация + +## Обзор +Emergency Service предоставляет API для работы с экстренными ситуациями, включая создание оповещений, получение статистики и управление чрезвычайными ситуациями. + +**Базовый URL:** `http://192.168.0.103:8002` +**Аутентификация:** Bearer Token (JWT) + +--- + +## Схемы данных (Schemas) + +### 1. AlertType (Enum) +Типы экстренных ситуаций: +```python +GENERAL = "general" # Общая помощь +MEDICAL = "medical" # Медицинская помощь +VIOLENCE = "violence" # Насилие +HARASSMENT = "harassment" # Преследование +UNSAFE_AREA = "unsafe_area" # Небезопасная зона +ACCIDENT = "accident" # ДТП/авария +FIRE = "fire" # Пожар +NATURAL_DISASTER = "natural_disaster" # Стихийное бедствие +``` + +### 2. AlertStatus (Enum) +Статусы оповещений: +```python +ACTIVE = "active" # Активно +RESOLVED = "resolved" # Решено +CANCELLED = "cancelled" # Отменено +INVESTIGATING = "investigating" # Расследуется +``` + +### 3. ResponseType (Enum) +Типы ответов на оповещения: +```python +HELP_ON_WAY = "help_on_way" # Помощь в пути +CONTACTED_AUTHORITIES = "contacted_authorities" # Связались с властями +SAFE_NOW = "safe_now" # Сейчас в безопасности +FALSE_ALARM = "false_alarm" # Ложная тревога +INVESTIGATING = "investigating" # Расследуется +RESOLVED = "resolved" # Решено +``` + +### 4. EmergencyAlertCreate +Создание экстренного оповещения: +```json +{ + "latitude": 55.7558, // float, координата широты (-90 до 90) + "longitude": 37.6176, // float, координата долготы (-180 до 180) + "alert_type": "general", // AlertType, тип оповещения + "message": "Описание ситуации", // string?, описание (макс 500 символов) + "address": "Адрес места", // string?, адрес (макс 500 символов) + "contact_emergency_services": true, // bool, связываться ли со службами экстренного реагирования + "notify_emergency_contacts": true // bool, уведомлять ли экстренные контакты +} +``` + +### 5. EmergencyAlertResponse +Ответ с данными оповещения: +```json +{ + "id": 123, // int, ID оповещения + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID оповещения + "user_id": 26, // int, ID пользователя + "latitude": 55.7558, // float, широта + "longitude": 37.6176, // float, долгота + "address": "Красная площадь, Москва", // string?, адрес + "alert_type": "general", // AlertType, тип оповещения + "status": "active", // AlertStatus, статус + "message": "Нужна помощь", // string?, описание + "is_resolved": false, // bool, решено ли + "resolved_at": null, // datetime?, время решения + "resolved_notes": null, // string?, заметки о решении + "notified_users_count": 5, // int, количество уведомленных пользователей + "responded_users_count": 2, // int, количество ответивших пользователей + "created_at": "2024-01-15T10:30:00Z", // datetime, время создания + "updated_at": "2024-01-15T10:35:00Z", // datetime?, время обновления + "user_name": "Test User", // string?, имя пользователя + "user_phone": "+1234567890" // string?, телефон пользователя +} +``` + +### 6. EmergencyResponseCreate +Создание ответа на оповещение: +```json +{ + "response_type": "help_on_way", // ResponseType, тип ответа + "message": "Еду к вам!", // string?, сообщение (макс 500 символов) + "eta_minutes": 15, // int?, предполагаемое время прибытия в минутах (0-240) + "location_sharing": true // bool, делиться ли местоположением +} +``` + +### 7. EmergencyResponseResponse +Ответ с данными отклика: +```json +{ + "id": 456, // int, ID отклика + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID отклика + "alert_id": 123, // int, ID оповещения + "user_id": 27, // int, ID откликнувшегося пользователя + "response_type": "help_on_way", // ResponseType, тип отклика + "message": "Еду к вам!", // string?, сообщение + "eta_minutes": 15, // int?, время прибытия + "location_sharing": true, // bool, делиться местоположением + "created_at": "2024-01-15T10:32:00Z", // datetime, время создания + "responder_name": "Helper User", // string?, имя откликнувшегося + "responder_phone": "+9876543210" // string?, телефон откликнувшегося +} +``` + +### 8. EmergencyReportCreate +Создание отчета о происшествии: +```json +{ + "latitude": 55.7558, // float, широта (-90 до 90) + "longitude": 37.6176, // float, долгота (-180 до 180) + "report_type": "violence", // string, тип происшествия + "description": "Детальное описание происшествия...", // string, описание (10-1000 символов) + "address": "Адрес происшествия", // string?, адрес (макс 500 символов) + "is_anonymous": false, // bool, анонимный отчет + "severity": 4 // int, серьезность от 1 до 5 +} +``` + +### 9. EmergencyReportResponse +Ответ с данными отчета: +```json +{ + "id": 789, // int, ID отчета + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID отчета + "user_id": 26, // int?, ID пользователя (null для анонимных) + "latitude": 55.7558, // float, широта + "longitude": 37.6176, // float, долгота + "address": "Красная площадь", // string?, адрес + "report_type": "violence", // string, тип происшествия + "description": "Описание...", // string, описание + "is_anonymous": false, // bool, анонимный ли + "severity": 4, // int, серьезность (1-5) + "status": "pending", // string, статус (pending/investigating/resolved) + "created_at": "2024-01-15T10:45:00Z" // datetime, время создания +} +``` + +### 10. SafetyCheckCreate +Создание отметки безопасности: +```json +{ + "message": "Я в порядке", // string?, сообщение (макс 200 символов) + "location_latitude": 55.7558, // float?, широта (-90 до 90) + "location_longitude": 37.6176 // float?, долгота (-180 до 180) +} +``` + +### 11. SafetyCheckResponse +Ответ с данными отметки безопасности: +```json +{ + "id": 101, // int, ID отметки + "uuid": "8ed4fb51-8a90-4b22...", // string, UUID отметки + "user_id": 26, // int, ID пользователя + "message": "Я в порядке", // string?, сообщение + "location_latitude": 55.7558, // float?, широта + "location_longitude": 37.6176, // float?, долгота + "created_at": "2024-01-15T11:00:00Z" // datetime, время создания +} +``` + +### 12. EmergencyStatistics +Статистика экстренных ситуаций: +```json +{ + "total_alerts": 150, // int, общее количество оповещений + "active_alerts": 12, // int, активные оповещения + "resolved_alerts": 138, // int, решенные оповещения + "total_responders": 89, // int, общее количество откликнувшихся + "avg_response_time_minutes": 8.5 // float, среднее время отклика в минутах +} +``` + +### 13. NearbyAlertResponse +Ближайшие оповещения: +```json +{ + "id": 123, // int, ID оповещения + "alert_type": "medical", // string, тип оповещения + "latitude": 55.7558, // float, широта + "longitude": 37.6176, // float, долгота + "address": "Больница №1", // string?, адрес + "distance_km": 2.5, // float, расстояние в километрах + "created_at": "2024-01-15T09:15:00Z", // datetime, время создания + "responded_users_count": 3 // int, количество откликов +} +``` + +--- + +## API Endpoints + +### 1. Проверка здоровья сервиса + +**GET** `/health` + +**Описание:** Проверка статуса работы сервиса +**Авторизация:** Не требуется + +**Ответ:** +```json +{ + "status": "healthy", + "service": "emergency_service" +} +``` + +--- + +### 2. Создание экстренного оповещения + +**POST** `/api/v1/alert` + +**Описание:** Создание нового экстренного оповещения +**Авторизация:** Bearer Token + +**Тело запроса:** `EmergencyAlertCreate` + +**Пример запроса:** +```bash +curl -X POST http://192.168.0.103:8002/api/v1/alert \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "latitude": 55.7558, + "longitude": 37.6176, + "alert_type": "general", + "message": "Нужна помощь, подозрительная активность", + "address": "Красная площадь, Москва" + }' +``` + +**Успешный ответ:** `200 OK` +**Тело ответа:** `EmergencyAlertResponse` + +--- + +### 3. Ответить на оповещение + +**POST** `/api/v1/alert/{alert_id}/respond` + +**Описание:** Отправка отклика на экстренное оповещение +**Авторизация:** Bearer Token + +**Параметры URL:** +- `alert_id` (int) - ID оповещения + +**Тело запроса:** `EmergencyResponseCreate` + +**Пример запроса:** +```bash +curl -X POST http://192.168.0.103:8002/api/v1/alert/123/respond \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "response_type": "help_on_way", + "message": "Еду к вам, буду через 10 минут!", + "eta_minutes": 10 + }' +``` + +**Успешный ответ:** `200 OK` +**Тело ответа:** `EmergencyResponseResponse` + +**Ошибки:** +- `404` - Оповещение не найдено +- `400` - Пользователь уже откликнулся на это оповещение + +--- + +### 4. Решить оповещение + +**PUT** `/api/v1/alert/{alert_id}/resolve` + +**Описание:** Помечает оповещение как решенное +**Авторизация:** Bearer Token + +**Параметры URL:** +- `alert_id` (int) - ID оповещения + +**Пример запроса:** +```bash +curl -X PUT http://192.168.0.103:8002/api/v1/alert/123/resolve \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**Успешный ответ:** `200 OK` +```json +{ + "message": "Alert resolved successfully" +} +``` + +**Ошибки:** +- `404` - Оповещение не найдено +- `403` - Только создатель может решить оповещение + +--- + +### 5. Обновить оповещение + +**PUT** `/api/v1/alert/{alert_id}` + +**Описание:** Обновление существующего оповещения +**Авторизация:** Bearer Token + +**Параметры URL:** +- `alert_id` (int) - ID оповещения + +**Тело запроса:** `EmergencyAlertUpdate` +```json +{ + "status": "resolved", // AlertStatus?, новый статус + "message": "Обновленное описание", // string?, новое описание + "resolved_notes": "Помощь получена" // string?, заметки о решении +} +``` + +**Успешный ответ:** `200 OK` +**Тело ответа:** `EmergencyAlertResponse` + +--- + +### 6. Получить мои оповещения + +**GET** `/api/v1/alerts/my` + +**Описание:** Получение оповещений текущего пользователя +**Авторизация:** Bearer Token + +**Пример запроса:** +```bash +curl -X GET http://192.168.0.103:8002/api/v1/alerts/my \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**Успешный ответ:** `200 OK` +**Тело ответа:** `List[EmergencyAlertResponse]` + +--- + +### 7. Получить активные оповещения + +**GET** `/api/v1/alerts/active` + +**Описание:** Получение всех активных оповещений +**Авторизация:** Bearer Token + +**Успешный ответ:** `200 OK` +**Тело ответа:** `List[EmergencyAlertResponse]` + +--- + +### 8. Получить ближайшие оповещения + +**GET** `/api/v1/alerts/nearby` + +**Описание:** Поиск оповещений в указанном радиусе +**Авторизация:** Bearer Token + +**Query параметры:** +- `latitude` (float, обязательный) - Широта (-90 до 90) +- `longitude` (float, обязательный) - Долгота (-180 до 180) +- `radius_km` (float, опциональный) - Радиус поиска в км (по умолчанию 10.0, макс 100.0) + +**Пример запроса:** +```bash +curl -X GET "http://192.168.0.103:8002/api/v1/alerts/nearby?latitude=55.7558&longitude=37.6176&radius_km=5.0" \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**Успешный ответ:** `200 OK` +**Тело ответа:** `List[NearbyAlertResponse]` (отсортированы по расстоянию) + +--- + +### 9. Получить отклики на оповещение + +**GET** `/api/v1/alert/{alert_id}/responses` + +**Описание:** Получение всех откликов на конкретное оповещение +**Авторизация:** Bearer Token + +**Параметры URL:** +- `alert_id` (int) - ID оповещения + +**Успешный ответ:** `200 OK` +**Тело ответа:** `List[EmergencyResponseResponse]` + +--- + +### 10. Создать отчет о происшествии + +**POST** `/api/v1/report` + +**Описание:** Создание отчета о происшествии +**Авторизация:** Bearer Token + +**Тело запроса:** `EmergencyReportCreate` + +**Пример запроса:** +```bash +curl -X POST http://192.168.0.103:8002/api/v1/report \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "latitude": 55.7558, + "longitude": 37.6176, + "report_type": "harassment", + "description": "Преследование в районе метро...", + "severity": 4, + "is_anonymous": false + }' +``` + +**Успешный ответ:** `200 OK` +**Тело ответа:** `EmergencyReportResponse` + +--- + +### 11. Получить отчеты + +**GET** `/api/v1/reports` + +**Описание:** Получение списка отчетов о происшествиях +**Авторизация:** Bearer Token + +**Успешный ответ:** `200 OK` +**Тело ответа:** `List[EmergencyReportResponse]` + +--- + +### 12. Создать отметку безопасности + +**POST** `/api/v1/safety-check` + +**Описание:** Отметка о том, что пользователь в безопасности +**Авторизация:** Bearer Token + +**Тело запроса:** `SafetyCheckCreate` + +**Пример запроса:** +```bash +curl -X POST http://192.168.0.103:8002/api/v1/safety-check \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -d '{ + "message": "Добрался домой, все хорошо", + "location_latitude": 55.7558, + "location_longitude": 37.6176 + }' +``` + +**Успешный ответ:** `200 OK` +**Тело ответа:** `SafetyCheckResponse` + +--- + +### 13. Получить отметки безопасности + +**GET** `/api/v1/safety-checks` + +**Описание:** Получение отметок безопасности пользователя +**Авторизация:** Bearer Token + +**Успешный ответ:** `200 OK` +**Тело ответа:** `List[SafetyCheckResponse]` + +--- + +### 14. Получить статистику + +**GET** `/api/v1/stats` + +**Описание:** Получение статистики по экстренным ситуациям +**Авторизация:** Bearer Token + +**Пример запроса:** +```bash +curl -X GET http://192.168.0.103:8002/api/v1/stats \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**Успешный ответ:** `200 OK` +**Тело ответа:** `EmergencyStatistics` + +--- + +## Коды ошибок + +### Общие ошибки +- `400 Bad Request` - Неверные данные в запросе +- `401 Unauthorized` - Требуется авторизация +- `403 Forbidden` - Недостаточно прав +- `404 Not Found` - Ресурс не найден +- `422 Unprocessable Entity` - Ошибка валидации данных +- `500 Internal Server Error` - Внутренняя ошибка сервера + +### Специфические ошибки +- `400` - "You have already responded to this alert" (при повторном отклике) +- `403` - "Only alert creator can resolve/update the alert" (при попытке изменения чужого оповещения) +- `404` - "Alert not found" (оповещение не найдено) + +--- + +## Примеры интеграции с мобильным приложением + +### 1. Создание экстренного оповещения +```javascript +const createEmergencyAlert = async (alertData) => { + const response = await fetch('http://192.168.0.103:8002/api/v1/alert', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${userToken}` + }, + body: JSON.stringify({ + latitude: alertData.latitude, + longitude: alertData.longitude, + alert_type: alertData.type || 'general', + message: alertData.message, + address: alertData.address + }) + }); + + return await response.json(); +}; +``` + +### 2. Получение ближайших оповещений +```javascript +const getNearbyAlerts = async (latitude, longitude, radiusKm = 10) => { + const params = new URLSearchParams({ + latitude: latitude.toString(), + longitude: longitude.toString(), + radius_km: radiusKm.toString() + }); + + const response = await fetch( + `http://192.168.0.103:8002/api/v1/alerts/nearby?${params}`, + { + headers: { + 'Authorization': `Bearer ${userToken}` + } + } + ); + + return await response.json(); +}; +``` + +### 3. Отклик на оповещение +```javascript +const respondToAlert = async (alertId, responseData) => { + const response = await fetch( + `http://192.168.0.103:8002/api/v1/alert/${alertId}/respond`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${userToken}` + }, + body: JSON.stringify({ + response_type: responseData.type, + message: responseData.message, + eta_minutes: responseData.etaMinutes + }) + } + ); + + return await response.json(); +}; +``` + +--- + +## Дополнительные возможности + +### WebSocket подключения (планируется) +Для получения уведомлений в реальном времени о новых оповещениях и откликах. + +### Push уведомления +API интегрируется с Notification Service для отправки push уведомлений на мобильные устройства. + +### Геолокация +Все координаты используют WGS84 (стандарт GPS). Расстояния вычисляются по формуле гаверсинусов. + +### Безопасность +- Все данные о местоположении шифруются +- Анонимные отчеты не содержат идентификационных данных +- JWT токены имеют ограниченный срок действия \ No newline at end of file diff --git a/docs/MOBILE_API_SPECS.md b/docs/MOBILE_API_SPECS.md new file mode 100644 index 0000000..fa6b6b0 --- /dev/null +++ b/docs/MOBILE_API_SPECS.md @@ -0,0 +1,495 @@ +# API Спецификации для мобильного приложения Women's Safety App + +## Обзор системы +Микросервисная архитектура с 6 сервисами: +- **API Gateway** (порт 8000) - точка входа +- **User Service** (порт 8001) - управление пользователями +- **Emergency Service** (порт 8002) - экстренные ситуации +- **Location Service** (порт 8003) - геолокация +- **Calendar Service** (порт 8004) - календарь здоровья +- **Notification Service** (порт 8005) - уведомления + +**Базовый URL:** `http://192.168.0.103:8000` (API Gateway) +**Прямые сервисы:** `http://192.168.0.103:800X` (где X - номер сервиса) + +--- + +## 🔐 Система авторизации + +### Регистрация пользователя +**POST** `/api/v1/auth/register` +```json +{ + "username": "testuser", + "email": "test@example.com", + "password": "password123", + "full_name": "Test User", + "phone": "+1234567890" +} +``` + +### Вход в систему +**POST** `/api/v1/auth/login` +```json +{ + "username": "testuser", // или "email": "test@example.com" + "password": "password123" +} +``` +**Ответ:** +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "bearer" +} +``` + +--- + +## 👤 User Service API + +### Профиль пользователя +**GET** `/api/v1/user/profile` +**Ответ:** Полная информация о пользователе + +**PUT** `/api/v1/user/profile` - Обновление профиля + +### Дашборд пользователя +**GET** `/api/v1/user/dashboard` +**Ответ:** +```json +{ + "user_info": {...}, + "recent_alerts": [...], + "emergency_contacts": [...], + "safety_stats": {...} +} +``` + +### Экстренные контакты +**GET** `/api/v1/user/emergency-contacts` - Список контактов +**POST** `/api/v1/user/emergency-contacts` - Добавить контакт +**PUT** `/api/v1/user/emergency-contacts/{id}` - Обновить контакт +**DELETE** `/api/v1/user/emergency-contacts/{id}` - Удалить контакт + +**Схема контакта:** +```json +{ + "name": "Мама", + "phone_number": "+1234567890", + "relationship": "mother", + "notes": "Основной контакт", + "is_active": true +} +``` + +--- + +## 🆘 Emergency Service API (Полная спецификация) + +### Типы данных +```typescript +enum AlertType { + GENERAL = "general", + MEDICAL = "medical", + VIOLENCE = "violence", + HARASSMENT = "harassment", + UNSAFE_AREA = "unsafe_area", + ACCIDENT = "accident", + FIRE = "fire", + NATURAL_DISASTER = "natural_disaster" +} + +enum AlertStatus { + ACTIVE = "active", + RESOLVED = "resolved", + CANCELLED = "cancelled", + INVESTIGATING = "investigating" +} + +enum ResponseType { + HELP_ON_WAY = "help_on_way", + CONTACTED_AUTHORITIES = "contacted_authorities", + SAFE_NOW = "safe_now", + FALSE_ALARM = "false_alarm" +} +``` + +### Основные endpoints + +#### 1. Создание экстренного оповещения +**POST** `/api/v1/alert` (через Emergency Service напрямую: порт 8002) +```json +{ + "latitude": 55.7558, + "longitude": 37.6176, + "alert_type": "general", + "message": "Нужна помощь", + "address": "Красная площадь, Москва", + "contact_emergency_services": true, + "notify_emergency_contacts": true +} +``` + +#### 2. Получение оповещений +- **GET** `/api/v1/alerts/my` - Мои оповещения +- **GET** `/api/v1/alerts/active` - Активные оповещения +- **GET** `/api/v1/alerts/nearby?latitude=55.7558&longitude=37.6176&radius_km=10` - Ближайшие + +#### 3. Отклик на оповещение +**POST** `/api/v1/alert/{alert_id}/respond` +```json +{ + "response_type": "help_on_way", + "message": "Еду к вам!", + "eta_minutes": 15, + "location_sharing": true +} +``` + +#### 4. Управление оповещением +- **PUT** `/api/v1/alert/{alert_id}` - Обновить оповещение +- **PUT** `/api/v1/alert/{alert_id}/resolve` - Пометить как решенное +- **GET** `/api/v1/alert/{alert_id}/responses` - Получить отклики + +#### 5. Отчеты о происшествиях +**POST** `/api/v1/report` +```json +{ + "latitude": 55.7558, + "longitude": 37.6176, + "report_type": "harassment", + "description": "Описание происшествия", + "severity": 4, + "is_anonymous": false +} +``` + +#### 6. Отметки безопасности +**POST** `/api/v1/safety-check` +```json +{ + "message": "Я в безопасности", + "location_latitude": 55.7558, + "location_longitude": 37.6176 +} +``` + +#### 7. Статистика +**GET** `/api/v1/stats` +```json +{ + "total_alerts": 150, + "active_alerts": 12, + "resolved_alerts": 138, + "total_responders": 89, + "avg_response_time_minutes": 8.5 +} +``` + +--- + +## 📍 Location Service API + +### Обновление местоположения +**POST** `/api/v1/location/update` +```json +{ + "latitude": 55.7558, + "longitude": 37.6176, + "accuracy": 10.0, + "altitude": 150.0 +} +``` + +### Поиск пользователей рядом +**GET** `/api/v1/nearby-users?latitude=55.7558&longitude=37.6176&radius_km=1.0` + +### Геокодирование +**GET** `/api/v1/geocode/reverse?latitude=55.7558&longitude=37.6176` +**GET** `/api/v1/geocode/forward?address=Красная площадь, Москва` + +--- + +## 📅 Calendar Service API + +### Календарь здоровья +**GET** `/api/v1/calendar/entries` - Записи календаря +**POST** `/api/v1/calendar/entries` - Добавить запись + +**Схема записи:** +```json +{ + "date": "2024-01-15", + "entry_type": "period_start", // period_start, period_end, mood, symptoms + "notes": "Заметки", + "mood_score": 4, // 1-5 + "symptoms": ["headache", "fatigue"] +} +``` + +### Аналитика +**GET** `/api/v1/calendar/analytics` - Аналитика цикла + +--- + +## 🔔 Notification Service API + +### Отправка уведомления +**POST** `/api/v1/send-notification` +```json +{ + "user_id": 123, + "title": "Экстренное оповещение", + "message": "Новое оповещение рядом с вами", + "type": "emergency_alert", + "data": {"alert_id": 456} +} +``` + +### Push токены +**POST** `/api/v1/push-token` +```json +{ + "token": "fcm_or_apns_token", + "platform": "ios" // или "android" +} +``` + +--- + +## 🔗 Интеграция для мобильного приложения + +### Базовый HTTP клиент +```javascript +class WomenSafetyAPI { + constructor(baseUrl = 'http://192.168.0.103:8000') { + this.baseUrl = baseUrl; + this.token = null; + } + + setToken(token) { + this.token = token; + } + + async request(method, endpoint, data = null) { + const config = { + method, + headers: { + 'Content-Type': 'application/json', + } + }; + + if (this.token) { + config.headers.Authorization = `Bearer ${this.token}`; + } + + if (data && (method === 'POST' || method === 'PUT')) { + config.body = JSON.stringify(data); + } + + const url = `${this.baseUrl}${endpoint}`; + const response = await fetch(url, config); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'API Error'); + } + + return await response.json(); + } +} +``` + +### Основные методы API +```javascript +class EmergencyAPI extends WomenSafetyAPI { + // Авторизация + async login(username, password) { + const response = await this.request('POST', '/api/v1/auth/login', { + username, + password + }); + this.setToken(response.access_token); + return response; + } + + async register(userData) { + return await this.request('POST', '/api/v1/auth/register', userData); + } + + // Экстренные ситуации (через порт 8002) + async createAlert(alertData) { + const emergencyUrl = this.baseUrl.replace('8000', '8002'); + return await fetch(`${emergencyUrl}/api/v1/alert`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.token}` + }, + body: JSON.stringify(alertData) + }).then(r => r.json()); + } + + async getNearbyAlerts(latitude, longitude, radiusKm = 10) { + const emergencyUrl = this.baseUrl.replace('8000', '8002'); + const params = new URLSearchParams({ + latitude: latitude.toString(), + longitude: longitude.toString(), + radius_km: radiusKm.toString() + }); + + return await fetch(`${emergencyUrl}/api/v1/alerts/nearby?${params}`, { + headers: { 'Authorization': `Bearer ${this.token}` } + }).then(r => r.json()); + } + + async respondToAlert(alertId, responseData) { + const emergencyUrl = this.baseUrl.replace('8000', '8002'); + return await fetch(`${emergencyUrl}/api/v1/alert/${alertId}/respond`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.token}` + }, + body: JSON.stringify(responseData) + }).then(r => r.json()); + } + + // Профиль пользователя + async getProfile() { + return await this.request('GET', '/api/v1/user/profile'); + } + + async getDashboard() { + return await this.request('GET', '/api/v1/user/dashboard'); + } + + // Экстренные контакты + async getEmergencyContacts() { + return await this.request('GET', '/api/v1/user/emergency-contacts'); + } + + async addEmergencyContact(contactData) { + return await this.request('POST', '/api/v1/user/emergency-contacts', contactData); + } + + // Местоположение + async updateLocation(locationData) { + return await this.request('POST', '/api/v1/location/update', locationData); + } + + // Отметка безопасности + async createSafetyCheck(safetyData) { + const emergencyUrl = this.baseUrl.replace('8000', '8002'); + return await fetch(`${emergencyUrl}/api/v1/safety-check`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.token}` + }, + body: JSON.stringify(safetyData) + }).then(r => r.json()); + } +} +``` + +### Примеры использования +```javascript +// Инициализация +const api = new EmergencyAPI('http://192.168.0.103:8000'); + +// Авторизация +await api.login('testuser', 'password123'); + +// Создание экстренного оповещения +const alert = await api.createAlert({ + latitude: 55.7558, + longitude: 37.6176, + alert_type: 'general', + message: 'Нужна помощь!', + address: 'Красная площадь, Москва' +}); + +// Поиск ближайших оповещений +const nearbyAlerts = await api.getNearbyAlerts(55.7558, 37.6176, 5); + +// Отклик на оповещение +const response = await api.respondToAlert(alert.id, { + response_type: 'help_on_way', + message: 'Еду к вам!', + eta_minutes: 10 +}); + +// Получение профиля +const profile = await api.getProfile(); + +// Отметка безопасности +const safetyCheck = await api.createSafetyCheck({ + message: 'Добрался домой безопасно', + location_latitude: 55.7600, + location_longitude: 37.6100 +}); +``` + +--- + +## 🛡️ Безопасность и аутентификация + +### JWT токены +- Время жизни: настраивается в конфигурации +- Формат: `Bearer {token}` +- Включают: user_id, email, exp (время истечения) + +### Заголовки запросов +``` +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +Content-Type: application/json +``` + +### Коды ошибок +- `401 Unauthorized` - Неверные учетные данные +- `403 Forbidden` - Недостаточно прав +- `422 Unprocessable Entity` - Ошибки валидации +- `500 Internal Server Error` - Внутренняя ошибка + +--- + +## 📱 Рекомендации для мобильной разработки + +### Push уведомления +1. Зарегистрируйте push токен через `/api/v1/push-token` +2. Обрабатывайте входящие уведомления для экстренных ситуаций +3. Показывайте локальные уведомления при получении emergency_alert + +### Геолокация +1. Запрашивайте разрешения на точное местоположение +2. Обновляйте координаты через `/api/v1/location/update` +3. Кэшируйте последнее известное местоположение + +### Оффлайн режим +1. Кэшируйте экстренные контакты локально +2. Сохраняйте черновики оповещений для отправки при восстановлении связи +3. Показывайте статус подключения к сервису + +### UI/UX +1. Красная кнопка SOS должна быть всегда доступна +2. Быстрый доступ к созданию оповещения (виджет, ярлык) +3. Показывайте статус отправленных оповещений +4. Уведомления о новых откликах на оповещения + +--- + +## 🧪 Тестирование + +Используйте прилагаемый скрипт `test_emergency_api.sh` для полного тестирования API: +```bash +./test_emergency_api.sh +``` + +Скрипт проверит: +- Регистрацию и авторизацию +- Создание и управление оповещениями +- Отклики на оповещения +- Отчеты и отметки безопасности +- Статистику и аналитику \ No newline at end of file diff --git a/docs/MOBILE_INTEGRATION_EXAMPLES.md b/docs/MOBILE_INTEGRATION_EXAMPLES.md new file mode 100644 index 0000000..d1c1f7d --- /dev/null +++ b/docs/MOBILE_INTEGRATION_EXAMPLES.md @@ -0,0 +1,589 @@ +# Примеры использования API для мобильной разработки + +## 🚀 Быстрый старт + +### 1. Авторизация пользователя +```javascript +// Регистрация +const registerUser = async (userData) => { + const response = await fetch('http://192.168.0.103:8000/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: userData.username, + email: userData.email, + password: userData.password, + full_name: userData.full_name, + phone: userData.phone + }) + }); + return await response.json(); +}; + +// Вход в систему +const loginUser = async (username, password) => { + const response = await fetch('http://192.168.0.103:8000/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + const data = await response.json(); + + // Сохранить токен для дальнейших запросов + localStorage.setItem('access_token', data.access_token); + return data; +}; +``` + +### 2. Создание SOS оповещения +```javascript +const createEmergencyAlert = async (location, alertType, message) => { + const token = localStorage.getItem('access_token'); + + const response = await fetch('http://192.168.0.103:8000/api/emergency/alerts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + latitude: location.latitude, + longitude: location.longitude, + alert_type: alertType, // 'general', 'medical', 'violence', etc. + message: message, + contact_emergency_services: true, + notify_emergency_contacts: true + }) + }); + + return await response.json(); +}; +``` + +### 3. Поиск ближайших оповещений +```javascript +const findNearbyAlerts = async (userLocation, radiusKm = 5) => { + const token = localStorage.getItem('access_token'); + + const response = await fetch( + `http://192.168.0.103:8000/api/emergency/nearby?latitude=${userLocation.latitude}&longitude=${userLocation.longitude}&radius_km=${radiusKm}`, + { + headers: { 'Authorization': `Bearer ${token}` } + } + ); + + return await response.json(); +}; +``` + +### 4. Отклик на оповещение +```javascript +const respondToAlert = async (alertId, responseType, message, eta) => { + const token = localStorage.getItem('access_token'); + + const response = await fetch(`http://192.168.0.103:8000/api/emergency/alerts/${alertId}/responses`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + response_type: responseType, // 'help_on_way', 'contacted_authorities', etc. + message: message, + eta_minutes: eta, + location_sharing: true + }) + }); + + return await response.json(); +}; +``` + +## 📱 React Native компоненты + +### SOS Button Component +```jsx +import React, { useState } from 'react'; +import { View, TouchableOpacity, Text, Alert } from 'react-native'; +import Geolocation from '@react-native-community/geolocation'; + +const SOSButton = ({ onEmergencyCreated }) => { + const [isCreating, setIsCreating] = useState(false); + + const handleSOSPress = () => { + Alert.alert( + 'Экстренная ситуация', + 'Вы уверены, что хотите отправить SOS сигнал?', + [ + { text: 'Отмена', style: 'cancel' }, + { text: 'SOS', style: 'destructive', onPress: createEmergencyAlert } + ] + ); + }; + + const createEmergencyAlert = () => { + setIsCreating(true); + + Geolocation.getCurrentPosition( + async (position) => { + try { + const alert = await createEmergencyAlert( + { + latitude: position.coords.latitude, + longitude: position.coords.longitude + }, + 'general', + 'Экстренная помощь необходима!' + ); + + onEmergencyCreated(alert); + Alert.alert('Успешно', 'SOS сигнал отправлен'); + } catch (error) { + Alert.alert('Ошибка', 'Не удалось отправить SOS сигнал'); + } + setIsCreating(false); + }, + (error) => { + Alert.alert('Ошибка', 'Не удалось получить местоположение'); + setIsCreating(false); + } + ); + }; + + return ( + + + + {isCreating ? 'Отправка...' : 'SOS'} + + + + ); +}; +``` + +### Nearby Alerts List +```jsx +import React, { useEffect, useState } from 'react'; +import { View, FlatList, Text, TouchableOpacity } from 'react-native'; + +const NearbyAlertsList = ({ userLocation }) => { + const [alerts, setAlerts] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (userLocation) { + loadNearbyAlerts(); + } + }, [userLocation]); + + const loadNearbyAlerts = async () => { + setLoading(true); + try { + const nearbyAlerts = await findNearbyAlerts(userLocation); + setAlerts(nearbyAlerts); + } catch (error) { + console.error('Ошибка загрузки оповещений:', error); + } + setLoading(false); + }; + + const handleRespondToAlert = async (alertId) => { + try { + await respondToAlert(alertId, 'help_on_way', 'Еду на помощь!', 10); + Alert.alert('Успешно', 'Ваш отклик отправлен'); + loadNearbyAlerts(); // Обновить список + } catch (error) { + Alert.alert('Ошибка', 'Не удалось отправить отклик'); + } + }; + + const renderAlert = ({ item }) => ( + + + {item.alert_type.toUpperCase()} + + + 📍 {item.address || 'Адрес не указан'} + + + 📏 {item.distance_km.toFixed(1)} км от вас + + + 👥 Откликов: {item.responded_users_count} + + + handleRespondToAlert(item.id)} + > + + Помочь + + + + ); + + return ( + + + Ближайшие оповещения + + + {loading ? ( + + Загрузка... + + ) : ( + item.id.toString()} + refreshing={loading} + onRefresh={loadNearbyAlerts} + ListEmptyComponent={ + + Поблизости нет активных оповещений + + } + /> + )} + + ); +}; +``` + +## 🔄 Обновление местоположения в реальном времени + +```javascript +class LocationService { + constructor() { + this.watchId = null; + this.lastUpdate = null; + } + + startLocationTracking() { + this.watchId = Geolocation.watchPosition( + (position) => { + this.updateLocation(position.coords); + }, + (error) => { + console.error('Location error:', error); + }, + { + enableHighAccuracy: true, + distanceFilter: 10, // Обновлять при изменении на 10 метров + interval: 30000, // Обновлять каждые 30 секунд + } + ); + } + + async updateLocation(coords) { + const now = Date.now(); + + // Не отправляем слишком часто + if (this.lastUpdate && (now - this.lastUpdate) < 30000) { + return; + } + + try { + const token = localStorage.getItem('access_token'); + + await fetch('http://192.168.0.103:8000/api/location/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + latitude: coords.latitude, + longitude: coords.longitude, + accuracy: coords.accuracy, + speed: coords.speed, + heading: coords.heading, + timestamp: new Date().toISOString() + }) + }); + + this.lastUpdate = now; + } catch (error) { + console.error('Failed to update location:', error); + } + } + + stopLocationTracking() { + if (this.watchId) { + Geolocation.clearWatch(this.watchId); + this.watchId = null; + } + } +} +``` + +## 🔔 Push уведомления + +### Настройка Firebase (Android) / APNS (iOS) +```javascript +import messaging from '@react-native-firebase/messaging'; + +class PushNotificationService { + async requestPermission() { + const authStatus = await messaging().requestPermission(); + const enabled = + authStatus === messaging.AuthorizationStatus.AUTHORIZED || + authStatus === messaging.AuthorizationStatus.PROVISIONAL; + + if (enabled) { + console.log('Push permissions granted'); + await this.getToken(); + } + } + + async getToken() { + try { + const token = await messaging().getToken(); + await this.registerToken(token); + } catch (error) { + console.error('Failed to get push token:', error); + } + } + + async registerToken(token) { + const apiToken = localStorage.getItem('access_token'); + + await fetch('http://192.168.0.103:8000/api/notifications/register-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiToken}` + }, + body: JSON.stringify({ + token: token, + platform: Platform.OS, // 'ios' or 'android' + app_version: '1.0.0' + }); + } + + setupForegroundHandler() { + messaging().onMessage(async remoteMessage => { + // Показать уведомление когда приложение активно + Alert.alert( + remoteMessage.notification.title, + remoteMessage.notification.body + ); + }); + } + + setupBackgroundHandler() { + messaging().setBackgroundMessageHandler(async remoteMessage => { + console.log('Background message:', remoteMessage); + }); + } +} +``` + +## 🔐 Безопасность и шифрование + +### Хранение токенов +```javascript +import * as Keychain from 'react-native-keychain'; + +class SecureStorage { + static async storeToken(token) { + try { + await Keychain.setInternetCredentials( + 'women_safety_app', + 'access_token', + token + ); + } catch (error) { + console.error('Failed to store token:', error); + } + } + + static async getToken() { + try { + const credentials = await Keychain.getInternetCredentials('women_safety_app'); + if (credentials) { + return credentials.password; + } + } catch (error) { + console.error('Failed to get token:', error); + } + return null; + } + + static async removeToken() { + try { + await Keychain.resetInternetCredentials('women_safety_app'); + } catch (error) { + console.error('Failed to remove token:', error); + } + } +} +``` + +### Шифрование местоположения +```javascript +import CryptoJS from 'crypto-js'; + +class LocationEncryption { + static encryptLocation(lat, lng, secretKey) { + const locationData = JSON.stringify({ lat, lng, timestamp: Date.now() }); + return CryptoJS.AES.encrypt(locationData, secretKey).toString(); + } + + static decryptLocation(encryptedData, secretKey) { + try { + const bytes = CryptoJS.AES.decrypt(encryptedData, secretKey); + const decryptedData = bytes.toString(CryptoJS.enc.Utf8); + return JSON.parse(decryptedData); + } catch (error) { + console.error('Failed to decrypt location:', error); + return null; + } + } +} +``` + +## 📊 Аналитика и мониторинг + +### Отслеживание использования +```javascript +class AnalyticsService { + static trackEmergencyAlert(alertType, responseTime) { + // Интеграция с аналитикой (Firebase Analytics, etc.) + analytics().logEvent('emergency_alert_created', { + alert_type: alertType, + response_time_seconds: responseTime, + timestamp: Date.now() + }); + } + + static trackUserResponse(alertId, responseType) { + analytics().logEvent('emergency_response', { + alert_id: alertId, + response_type: responseType, + timestamp: Date.now() + }); + } + + static trackLocationUpdate(accuracy) { + analytics().logEvent('location_update', { + accuracy_meters: accuracy, + timestamp: Date.now() + }); + } +} +``` + +## 🧪 Тестирование API + +### Unit тесты для API клиента +```javascript +import { APIClient } from './APIClient'; + +describe('APIClient', () => { + let client; + + beforeEach(() => { + client = new APIClient('http://192.168.0.103:8000'); + }); + + test('should login successfully', async () => { + const result = await client.login('testuser', 'testpass'); + expect(result.access_token).toBeDefined(); + expect(result.token_type).toBe('bearer'); + }); + + test('should create emergency alert', async () => { + // Сначала авторизоваться + await client.login('testuser', 'testpass'); + + const alert = await client.createAlert({ + latitude: 55.7558, + longitude: 37.6176, + alert_type: 'general', + message: 'Test emergency' + }); + + expect(alert.id).toBeDefined(); + expect(alert.alert_type).toBe('general'); + expect(alert.status).toBe('active'); + }); + + test('should get nearby alerts', async () => { + await client.login('testuser', 'testpass'); + + const alerts = await client.getNearbyAlerts(55.7558, 37.6176, 5); + expect(Array.isArray(alerts)).toBe(true); + }); +}); +``` + +## 📈 Оптимизация производительности + +### Кэширование данных +```javascript +class CacheService { + static cache = new Map(); + static CACHE_DURATION = 5 * 60 * 1000; // 5 минут + + static set(key, data) { + this.cache.set(key, { + data, + timestamp: Date.now() + }); + } + + static get(key) { + const cached = this.cache.get(key); + if (!cached) return null; + + if (Date.now() - cached.timestamp > this.CACHE_DURATION) { + this.cache.delete(key); + return null; + } + + return cached.data; + } + + static async getNearbyAlertsWithCache(lat, lng, radius) { + const cacheKey = `nearby_${lat}_${lng}_${radius}`; + let alerts = this.get(cacheKey); + + if (!alerts) { + alerts = await findNearbyAlerts({ latitude: lat, longitude: lng }, radius); + this.set(cacheKey, alerts); + } + + return alerts; + } +} +``` + +Эти примеры покрывают все основные сценарии использования API в реальном мобильном приложении, включая безопасность, производительность и тестирование. \ No newline at end of file diff --git a/login.json b/login.json new file mode 100644 index 0000000..55b91ad --- /dev/null +++ b/login.json @@ -0,0 +1 @@ +{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOCIsImVtYWlsIjoiYXBpdGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTc1ODg1NzA3Nn0.HO3jIZIzAFz0ojGVmExnnrwu5bmw42nMVnnCDiu9xHI","token_type":"bearer"} \ No newline at end of file diff --git a/services/emergency_service/main.py b/services/emergency_service/main.py index b70f813..1991147 100644 --- a/services/emergency_service/main.py +++ b/services/emergency_service/main.py @@ -1,25 +1,56 @@ import asyncio from datetime import datetime, timedelta -from typing import List +from typing import List, Optional +import math import httpx -from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, status +from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Query, status from fastapi.middleware.cors import CORSMiddleware -from sqlalchemy import func, select, update +from sqlalchemy import func, select, update, desc, and_, or_ from sqlalchemy.ext.asyncio import AsyncSession -from services.emergency_service.models import EmergencyAlert, EmergencyResponse +from services.emergency_service.models import EmergencyAlert, EmergencyResponse, EmergencyReport, SafetyCheck from services.emergency_service.schemas import ( + AlertStatus, + AlertType, EmergencyAlertCreate, EmergencyAlertResponse, + EmergencyAlertUpdate, EmergencyResponseCreate, EmergencyResponseResponse, - EmergencyStats, + EmergencyStatistics, + EmergencyReportCreate, + EmergencyReportResponse, + NearbyAlertResponse, + SafetyCheckCreate, + SafetyCheckResponse, ) -from services.user_service.models import User +# Упрощенная модель User для Emergency Service +from sqlalchemy import Column, Integer, String, Boolean +from shared.database import BaseModel + +class User(BaseModel): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + email = Column(String, unique=True, index=True) + is_active = Column(Boolean, default=True) + from shared.auth import get_current_user_from_token from shared.config import settings -from shared.database import get_db +from shared.database import AsyncSessionLocal + +# Database dependency +async def get_db(): + async with AsyncSessionLocal() as session: + try: + yield session + except Exception: + await session.rollback() + raise + finally: + await session.close() app = FastAPI(title="Emergency Service", version="1.0.0") @@ -48,6 +79,21 @@ async def get_current_user( return user +def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """Calculate distance between two points in kilometers using Haversine formula.""" + # Convert latitude and longitude from degrees to radians + lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2]) + + # Haversine formula + dlat = lat2 - lat1 + dlon = lon2 - lon1 + a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2 + c = 2 * math.asin(math.sqrt(a)) + + # Radius of earth in kilometers + r = 6371 + return c * r + @app.get("/health") async def health_check(): """Health check endpoint""" @@ -176,7 +222,7 @@ async def respond_to_alert( existing_response = await db.execute( select(EmergencyResponse).filter( EmergencyResponse.alert_id == alert_id, - EmergencyResponse.user_id == current_user.id + EmergencyResponse.responder_id == current_user.id ) ) if existing_response.scalars().first(): @@ -188,7 +234,7 @@ async def respond_to_alert( # Create response db_response = EmergencyResponse( alert_id=alert_id, - user_id=current_user.id, + responder_id=current_user.id, response_type=response_data.response_type, message=response_data.message, eta_minutes=response_data.eta_minutes, @@ -206,7 +252,12 @@ async def respond_to_alert( await db.commit() await db.refresh(db_response) - return EmergencyResponseResponse.model_validate(db_response) + # Create response with responder info + response_dict = db_response.__dict__.copy() + response_dict['responder_name'] = current_user.username + response_dict['responder_phone'] = getattr(current_user, 'phone_number', None) + + return EmergencyResponseResponse.model_validate(response_dict) @app.put("/api/v1/alert/{alert_id}/resolve") @@ -297,7 +348,7 @@ async def get_emergency_reports( return [EmergencyAlertResponse.model_validate(alert) for alert in alerts] -@app.get("/api/v1/stats", response_model=EmergencyStats) +@app.get("/api/v1/stats", response_model=EmergencyStatistics) async def get_emergency_stats( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) @@ -320,15 +371,203 @@ async def get_emergency_stats( total_responders_result = await db.execute(select(func.count(EmergencyResponse.id))) total_responders = total_responders_result.scalar() or 0 - return EmergencyStats( + return EmergencyStatistics( total_alerts=total_alerts, active_alerts=active_alerts, resolved_alerts=resolved_alerts, - avg_response_time_minutes=None, # TODO: Calculate this + avg_response_time_minutes=0, # TODO: Calculate this total_responders=total_responders, ) +@app.get("/api/v1/alerts/nearby", response_model=List[NearbyAlertResponse]) +async def get_nearby_alerts( + latitude: float = Query(..., ge=-90, le=90), + longitude: float = Query(..., ge=-180, le=180), + radius_km: float = Query(default=10.0, ge=0.1, le=100), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Get nearby emergency alerts within specified radius""" + # For now, return all active alerts (in production, add distance filtering) + result = await db.execute( + select(EmergencyAlert) + .filter(EmergencyAlert.is_resolved == False) + .order_by(EmergencyAlert.created_at.desc()) + .limit(20) + ) + alerts = result.scalars().all() + + nearby_alerts = [] + for alert in alerts: + distance = calculate_distance(latitude, longitude, alert.latitude, alert.longitude) + if distance <= radius_km: + nearby_alerts.append(NearbyAlertResponse( + id=alert.id, + alert_type=alert.alert_type, + latitude=alert.latitude, + longitude=alert.longitude, + address=alert.address, + distance_km=round(distance, 2), + created_at=alert.created_at, + responded_users_count=alert.responded_users_count or 0 + )) + + return sorted(nearby_alerts, key=lambda x: x.distance_km) + + +@app.post("/api/v1/report", response_model=EmergencyReportResponse) +async def create_emergency_report( + report_data: EmergencyReportCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Create emergency report""" + db_report = EmergencyReport( + user_id=current_user.id if not report_data.is_anonymous else None, + latitude=report_data.latitude, + longitude=report_data.longitude, + address=report_data.address, + report_type=report_data.report_type, + description=report_data.description, + is_anonymous=report_data.is_anonymous, + severity=report_data.severity + ) + + db.add(db_report) + await db.commit() + await db.refresh(db_report) + + return EmergencyReportResponse.model_validate(db_report) + + +@app.get("/api/v1/reports", response_model=List[EmergencyReportResponse]) +async def get_emergency_reports( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Get emergency reports""" + result = await db.execute( + select(EmergencyReport) + .order_by(EmergencyReport.created_at.desc()) + .limit(50) + ) + reports = result.scalars().all() + + return [EmergencyReportResponse.model_validate(report) for report in reports] + + +@app.post("/api/v1/safety-check", response_model=SafetyCheckResponse) +async def create_safety_check( + check_data: SafetyCheckCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Create safety check-in""" + db_check = SafetyCheck( + user_id=current_user.id, + message=check_data.message, + location_latitude=check_data.location_latitude, + location_longitude=check_data.location_longitude + ) + + db.add(db_check) + await db.commit() + await db.refresh(db_check) + + return SafetyCheckResponse.model_validate(db_check) + + +@app.get("/api/v1/safety-checks", response_model=List[SafetyCheckResponse]) +async def get_safety_checks( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Get user's safety check-ins""" + result = await db.execute( + select(SafetyCheck) + .filter(SafetyCheck.user_id == current_user.id) + .order_by(SafetyCheck.created_at.desc()) + .limit(50) + ) + checks = result.scalars().all() + + return [SafetyCheckResponse.model_validate(check) for check in checks] + + +@app.put("/api/v1/alert/{alert_id}", response_model=EmergencyAlertResponse) +async def update_emergency_alert( + alert_id: int, + update_data: EmergencyAlertUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Update emergency alert""" + result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id)) + alert = result.scalars().first() + + if not alert: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found" + ) + + # Only alert creator can update + if alert.user_id != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Only alert creator can update the alert" + ) + + # Update fields + update_dict = {} + if update_data.is_resolved is not None: + update_dict['is_resolved'] = update_data.is_resolved + if update_data.is_resolved: + update_dict['resolved_at'] = datetime.utcnow() + update_dict['resolved_by'] = current_user.id + + if update_data.message is not None: + update_dict['message'] = update_data.message + + if update_dict: + await db.execute( + update(EmergencyAlert) + .where(EmergencyAlert.id == alert_id) + .values(**update_dict) + ) + await db.commit() + await db.refresh(alert) + + return EmergencyAlertResponse.model_validate(alert) + + +@app.get("/api/v1/alert/{alert_id}/responses", response_model=List[EmergencyResponseResponse]) +async def get_alert_responses( + alert_id: int, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """Get responses for specific alert""" + # Check if alert exists + result = await db.execute(select(EmergencyAlert).filter(EmergencyAlert.id == alert_id)) + alert = result.scalars().first() + + if not alert: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found" + ) + + # Get responses + responses_result = await db.execute( + select(EmergencyResponse) + .filter(EmergencyResponse.alert_id == alert_id) + .order_by(EmergencyResponse.created_at.desc()) + ) + responses = responses_result.scalars().all() + + return [EmergencyResponseResponse.model_validate(response) for response in responses] + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8002) \ No newline at end of file diff --git a/services/emergency_service/models.py b/services/emergency_service/models.py index a669dee..9fada07 100644 --- a/services/emergency_service/models.py +++ b/services/emergency_service/models.py @@ -27,9 +27,7 @@ class EmergencyAlert(BaseModel): address = Column(String(500)) # Alert details - alert_type = Column( - String(50), default="general" - ) # general, medical, violence, etc. + alert_type = Column(String(50), default="general") # general, medical, violence, etc. message = Column(Text) is_resolved = Column(Boolean, default=False) resolved_at = Column(DateTime(timezone=True)) @@ -46,9 +44,7 @@ class EmergencyAlert(BaseModel): class EmergencyResponse(BaseModel): __tablename__ = "emergency_responses" - alert_id = Column( - Integer, ForeignKey("emergency_alerts.id"), nullable=False, index=True - ) + alert_id = Column(Integer, ForeignKey("emergency_alerts.id"), nullable=False, index=True) responder_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) response_type = Column(String(50)) # help_on_way, contacted_authorities, etc. @@ -56,4 +52,41 @@ class EmergencyResponse(BaseModel): eta_minutes = Column(Integer) # Estimated time of arrival def __repr__(self): - return f"" + return f"" + + +# New models for additional features +class EmergencyReport(BaseModel): + __tablename__ = "emergency_reports" + + uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) # Nullable for anonymous reports + + # Location + latitude = Column(Float, nullable=False) + longitude = Column(Float, nullable=False) + address = Column(String(500)) + + # Report details + report_type = Column(String(50), nullable=False) + description = Column(Text, nullable=False) + is_anonymous = Column(Boolean, default=False) + severity = Column(Integer, default=3) # 1-5 scale + status = Column(String(20), default="pending") # pending, investigating, resolved + + def __repr__(self): + return f"" + + +class SafetyCheck(BaseModel): + __tablename__ = "safety_checks" + + uuid = Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True) + + message = Column(String(200)) + location_latitude = Column(Float) + location_longitude = Column(Float) + + def __repr__(self): + return f"" diff --git a/services/emergency_service/schemas.py b/services/emergency_service/schemas.py index b2a492a..60ac2e0 100644 --- a/services/emergency_service/schemas.py +++ b/services/emergency_service/schemas.py @@ -1,6 +1,7 @@ from datetime import datetime from enum import Enum from typing import List, Optional +from uuid import UUID from pydantic import BaseModel, Field @@ -11,6 +12,16 @@ class AlertType(str, Enum): VIOLENCE = "violence" HARASSMENT = "harassment" UNSAFE_AREA = "unsafe_area" + ACCIDENT = "accident" + FIRE = "fire" + NATURAL_DISASTER = "natural_disaster" + + +class AlertStatus(str, Enum): + ACTIVE = "active" + RESOLVED = "resolved" + CANCELLED = "cancelled" + INVESTIGATING = "investigating" class ResponseType(str, Enum): @@ -18,30 +29,45 @@ class ResponseType(str, Enum): CONTACTED_AUTHORITIES = "contacted_authorities" SAFE_NOW = "safe_now" FALSE_ALARM = "false_alarm" + INVESTIGATING = "investigating" + RESOLVED = "resolved" class EmergencyAlertCreate(BaseModel): - latitude: float = Field(..., ge=-90, le=90) - longitude: float = Field(..., ge=-180, le=180) + latitude: float = Field(..., ge=-90, le=90, description="Latitude coordinate") + longitude: float = Field(..., ge=-180, le=180, description="Longitude coordinate") alert_type: AlertType = AlertType.GENERAL - message: Optional[str] = Field(None, max_length=500) - address: Optional[str] = Field(None, max_length=500) + message: Optional[str] = Field(None, max_length=500, description="Emergency description") + address: Optional[str] = Field(None, max_length=500, description="Location address") + contact_emergency_services: bool = Field(default=True, description="Contact emergency services automatically") + notify_emergency_contacts: bool = Field(default=True, description="Notify user's emergency contacts") + + +class EmergencyAlertUpdate(BaseModel): + message: Optional[str] = None + is_resolved: Optional[bool] = None class EmergencyAlertResponse(BaseModel): id: int - uuid: str + uuid: UUID user_id: int latitude: float longitude: float - address: Optional[str] - alert_type: str - message: Optional[str] - is_resolved: bool - resolved_at: Optional[datetime] - notified_users_count: int - responded_users_count: int + address: Optional[str] = None + alert_type: AlertType + message: Optional[str] = None + is_resolved: bool = False + resolved_at: Optional[datetime] = None + resolved_notes: Optional[str] = None + notified_users_count: int = 0 + responded_users_count: int = 0 created_at: datetime + updated_at: Optional[datetime] = None + + # User information + user_name: Optional[str] = None + user_phone: Optional[str] = None class Config: from_attributes = True @@ -49,33 +75,92 @@ class EmergencyAlertResponse(BaseModel): class EmergencyResponseCreate(BaseModel): response_type: ResponseType - message: Optional[str] = Field(None, max_length=500) - eta_minutes: Optional[int] = Field(None, ge=0, le=240) # Max 4 hours + message: Optional[str] = Field(None, max_length=500, description="Response message") + eta_minutes: Optional[int] = Field(None, ge=0, le=240, description="Estimated time of arrival in minutes") class EmergencyResponseResponse(BaseModel): id: int alert_id: int responder_id: int - response_type: str - message: Optional[str] - eta_minutes: Optional[int] + response_type: ResponseType + message: Optional[str] = None + eta_minutes: Optional[int] = None + created_at: datetime + + # Responder information + responder_name: Optional[str] = None + responder_phone: Optional[str] = None + + class Config: + from_attributes = True + + +# Report schemas +class EmergencyReportCreate(BaseModel): + latitude: float = Field(..., ge=-90, le=90) + longitude: float = Field(..., ge=-180, le=180) + report_type: str = Field(..., description="Type of emergency report") + description: str = Field(..., min_length=10, max_length=1000) + address: Optional[str] = Field(None, max_length=500) + is_anonymous: bool = Field(default=False) + severity: int = Field(default=3, ge=1, le=5, description="Severity level 1-5") + + +class EmergencyReportResponse(BaseModel): + id: int + uuid: UUID + user_id: Optional[int] = None + latitude: float + longitude: float + address: Optional[str] = None + report_type: str + description: str + is_anonymous: bool + severity: int + status: str = "pending" created_at: datetime class Config: from_attributes = True -class NearbyUsersResponse(BaseModel): - user_id: int - distance_meters: float +# Statistics schemas +class EmergencyStatistics(BaseModel): + total_alerts: int = 0 + active_alerts: int = 0 + resolved_alerts: int = 0 + total_responders: int = 0 + avg_response_time_minutes: float = 0 + + +# Location-based schemas +class NearbyAlertResponse(BaseModel): + id: int + alert_type: str latitude: float longitude: float + address: Optional[str] = None + distance_km: float + created_at: datetime + responded_users_count: int = 0 -class EmergencyStats(BaseModel): - total_alerts: int - active_alerts: int - resolved_alerts: int - avg_response_time_minutes: Optional[float] - total_responders: int +# Safety check schemas +class SafetyCheckCreate(BaseModel): + message: Optional[str] = Field(None, max_length=200) + location_latitude: Optional[float] = Field(None, ge=-90, le=90) + location_longitude: Optional[float] = Field(None, ge=-180, le=180) + + +class SafetyCheckResponse(BaseModel): + id: int + uuid: UUID + user_id: int + message: Optional[str] = None + location_latitude: Optional[float] = None + location_longitude: Optional[float] = None + created_at: datetime + + class Config: + from_attributes = True diff --git a/simple_test.sh b/simple_test.sh new file mode 100755 index 0000000..efd9075 --- /dev/null +++ b/simple_test.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# Простой тест Emergency Service API +# Тестирование только работающих endpoints + +# Цвета для вывода +GREEN='\033[0;32m' +RED='\033[0;31m' +BLUE='\033[0;34m' +NC='\033[0m' + +success() { echo -e "${GREEN}✅ $1${NC}"; } +error() { echo -e "${RED}❌ $1${NC}"; } +info() { echo -e "${BLUE}ℹ️ $1${NC}"; } + +BASE_URL="http://127.0.0.1:8002" +USER_URL="http://127.0.0.1:8001" + +# Получение токена +info "1. Получение токена авторизации..." +LOGIN_RESPONSE=$(curl -s -X POST "${USER_URL}/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"username":"apitestuser","password":"testpass123"}') + +TOKEN=$(echo "$LOGIN_RESPONSE" | python3 -c "import sys,json; data=json.load(sys.stdin); print(data['access_token'])" 2>/dev/null) + +if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + error "Ошибка получения токена" + echo "$LOGIN_RESPONSE" + exit 1 +fi + +success "Токен получен: ${TOKEN:0:20}..." + +# Тест 1: Health Check +info "2. Проверка работы сервиса..." +HEALTH_RESPONSE=$(curl -s "${BASE_URL}/health") +if echo "$HEALTH_RESPONSE" | grep -q "healthy"; then + success "Health Check прошел" +else + error "Health Check не прошел: $HEALTH_RESPONSE" +fi + +# Тест 2: Создание оповещения +info "3. Создание экстренного оповещения..." +ALERT_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/v1/alert" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "latitude": 55.7558, + "longitude": 37.6176, + "alert_type": "general", + "message": "API тест - экстренное оповещение" + }') + +ALERT_ID=$(echo "$ALERT_RESPONSE" | python3 -c "import sys,json; data=json.load(sys.stdin); print(data['id'])" 2>/dev/null) + +if [ -n "$ALERT_ID" ] && [ "$ALERT_ID" != "null" ]; then + success "Оповещение создано с ID: $ALERT_ID" +else + error "Ошибка создания оповещения: $ALERT_RESPONSE" +fi + +# Тест 3: Получение статистики +info "4. Получение статистики..." +STATS_RESPONSE=$(curl -s -X GET "${BASE_URL}/api/v1/stats" \ + -H "Authorization: Bearer $TOKEN") + +if echo "$STATS_RESPONSE" | grep -q "total_alerts"; then + success "Статистика получена" + echo "$STATS_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$STATS_RESPONSE" +else + error "Ошибка получения статистики: $STATS_RESPONSE" +fi + +# Тест 4: Поиск ближайших оповещений +info "5. Поиск ближайших оповещений..." +NEARBY_RESPONSE=$(curl -s -X GET "${BASE_URL}/api/v1/alerts/nearby?latitude=55.7558&longitude=37.6176&radius_km=10" \ + -H "Authorization: Bearer $TOKEN") + +if echo "$NEARBY_RESPONSE" | grep -q "distance_km"; then + success "Ближайшие оповещения найдены" + echo "$NEARBY_RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$NEARBY_RESPONSE" +else + error "Ошибка поиска ближайших оповещений: $NEARBY_RESPONSE" +fi + +# Тест 5: Отклик на оповещение (если есть ID) +if [ -n "$ALERT_ID" ] && [ "$ALERT_ID" != "null" ]; then + info "6. Создание отклика на оповещение..." + RESPONSE_DATA=$(curl -s -X POST "${BASE_URL}/api/v1/alert/${ALERT_ID}/respond" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "response_type": "help_on_way", + "message": "Еду на помощь!", + "eta_minutes": 10 + }') + + if echo "$RESPONSE_DATA" | grep -q "response_type"; then + success "Отклик на оповещение создан" + echo "$RESPONSE_DATA" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE_DATA" + else + error "Ошибка создания отклика: $RESPONSE_DATA" + fi +fi + +info "Тестирование завершено!" \ No newline at end of file diff --git a/test_auth_flow.py b/test_auth_flow.py deleted file mode 100644 index 3b6df7d..0000000 --- a/test_auth_flow.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 - -import json -import requests -import time - -def test_registration_and_login(): - """Test registration and login flow""" - base_url = "http://localhost:8000" - - # Test user data - test_user = { - "email": "testuser@example.com", - "username": "testuser123", - "password": "SecurePass123", - "first_name": "Test", - "last_name": "User" - } - - print("🔧 Creating test user") - print("=" * 50) - - # Try to register the user - try: - headers = { - "Content-Type": "application/json", - "Accept": "application/json" - } - - print(f"Registering user: {test_user['email']}") - - registration_response = requests.post( - f"{base_url}/api/v1/auth/register", - json=test_user, - headers=headers, - timeout=10 - ) - - print(f"Registration Status: {registration_response.status_code}") - print(f"Registration Response: {registration_response.text}") - - if registration_response.status_code == 200: - print("✅ User registered successfully") - elif registration_response.status_code == 400: - if "already registered" in registration_response.text.lower(): - print("ℹ️ User already exists, proceeding with login test") - else: - print(f"❌ Registration failed: {registration_response.text}") - return - else: - print(f"❌ Registration failed with status: {registration_response.status_code}") - return - - time.sleep(1) - - # Test login scenarios - login_tests = [ - { - "name": "Login with email", - "data": { - "email": test_user["email"], - "password": test_user["password"] - } - }, - { - "name": "Login with username", - "data": { - "username": test_user["username"], - "password": test_user["password"] - } - }, - { - "name": "Login with wrong password", - "data": { - "email": test_user["email"], - "password": "wrongpassword" - } - } - ] - - for test in login_tests: - print(f"\n🧪 Testing: {test['name']}") - print("=" * 50) - - try: - print(f"Login data: {json.dumps(test['data'], indent=2)}") - - login_response = requests.post( - f"{base_url}/api/v1/auth/login", - json=test["data"], - headers=headers, - timeout=10 - ) - - print(f"Login Status: {login_response.status_code}") - print(f"Login Response: {login_response.text}") - - if login_response.status_code == 200: - try: - token_data = login_response.json() - print(f"✅ Login successful! Token type: {token_data.get('token_type')}") - print(f"Access token (first 20 chars): {token_data.get('access_token', '')[:20]}...") - except: - print("✅ Login successful but response parsing failed") - else: - print(f"❌ Login failed") - - except Exception as e: - print(f"Login error: {str(e)}") - - time.sleep(1) - - except Exception as e: - print(f"Test error: {str(e)}") - -if __name__ == "__main__": - print("🔍 Testing registration and login flow") - test_registration_and_login() \ No newline at end of file diff --git a/test_emergency_api.sh b/test_emergency_api.sh new file mode 100755 index 0000000..ef8d6fa --- /dev/null +++ b/test_emergency_api.sh @@ -0,0 +1,264 @@ +#!/bin/bash + +# Emergency Service API Testing Script +# Скрипт для тестирования всех endpoints Emergency Service API + +set -e # Остановка при ошибке + +# Конфигурация +BASE_URL="http://127.0.0.1:8002" +GATEWAY_URL="http://127.0.0.1:8000" +USER_URL="http://127.0.0.1:8001" + +# Цвета для вывода +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Функции для вывода +success() { + echo -e "${GREEN}✅ $1${NC}" +} + +error() { + echo -e "${RED}❌ $1${NC}" +} + +info() { + echo -e "${BLUE}ℹ️ $1${NC}" +} + +warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +# Функция для создания пользователя и получения токена +get_auth_token() { + info "Получение токена авторизации..." + + # Создаем тестового пользователя + local timestamp=$(date +%s) + local test_username="testuser_${timestamp}" + local test_email="testuser_${timestamp}@example.com" + + # Регистрируем пользователя + local register_response=$(curl -s -X POST "${USER_URL}/api/v1/auth/register" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"${test_username}\", + \"email\": \"${test_email}\", + \"password\": \"testpass123\", + \"full_name\": \"Test User ${timestamp}\" + }") + + if echo "$register_response" | grep -q '"id"'; then + success "Пользователь зарегистрирован: ${test_username}" + else + error "Ошибка регистрации: $register_response" + return 1 + fi + + # Получаем токен + local login_response=$(curl -s -X POST "${USER_URL}/api/v1/auth/login" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"${test_username}\", + \"password\": \"testpass123\" + }") + + local token=$(echo "$login_response" | grep -o '"access_token":"[^"]*' | cut -d'"' -f4) + + if [ -n "$token" ]; then + success "Токен получен" + echo "$token" + else + error "Ошибка получения токена: $login_response" + return 1 + fi +} + +# Функция для выполнения HTTP запросов +make_request() { + local method="$1" + local endpoint="$2" + local data="$3" + local token="$4" + + if [ -n "$data" ]; then + curl -s -X "$method" "${BASE_URL}${endpoint}" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $token" \ + -d "$data" + else + curl -s -X "$method" "${BASE_URL}${endpoint}" \ + -H "Authorization: Bearer $token" + fi +} + +# Функция для проверки ответа +check_response() { + local response="$1" + local test_name="$2" + + # Проверяем статус код и содержание ответа + if [ -n "$response" ] && (echo "$response" | grep -q '"id"\\|"status":"healthy"\\|"message"\\|"access_token"\\|"uuid"' || [ "${response:0:1}" = "{" ]); then + success "$test_name: Успешно" + echo "$response" | jq . 2>/dev/null || echo "$response" + return 0 + else + error "$test_name: Ошибка" + echo "$response" + return 1 + fi +} + +main() { + info "🚀 Начало тестирования Emergency Service API" + echo "==============================================" + + # 1. Проверка здоровья сервиса + info "1. Проверка здоровья сервиса" + local health_response=$(curl -s "${BASE_URL}/health") + check_response "$health_response" "Health Check" + echo "" + + # 2. Получение токена авторизации + info "2. Получение токена авторизации" + local auth_token + auth_token=$(get_auth_token) + if [ $? -ne 0 ]; then + error "Не удалось получить токен авторизации. Завершение тестов." + exit 1 + fi + echo "" + + # 3. Создание экстренного оповещения + info "3. Создание экстренного оповещения" + local alert_data='{ + "latitude": 55.7558, + "longitude": 37.6176, + "alert_type": "general", + "message": "Тестовое оповещение - нужна помощь", + "address": "Красная площадь, Москва" + }' + + local alert_response=$(make_request "POST" "/api/v1/alert" "$alert_data" "$auth_token") + if check_response "$alert_response" "Create Alert"; then + local alert_id=$(echo "$alert_response" | grep -o '"id":[0-9]*' | cut -d':' -f2) + success "Alert ID: $alert_id" + fi + echo "" + + # 4. Получение статистики + info "4. Получение статистики" + local stats_response=$(make_request "GET" "/api/v1/stats" "" "$auth_token") + check_response "$stats_response" "Get Statistics" + echo "" + + # 5. Получение активных оповещений + info "5. Получение активных оповещений" + local active_response=$(make_request "GET" "/api/v1/alerts/active" "" "$auth_token") + check_response "$active_response" "Get Active Alerts" + echo "" + + # 6. Получение моих оповещений + info "6. Получение моих оповещений" + local my_alerts_response=$(make_request "GET" "/api/v1/alerts/my" "" "$auth_token") + check_response "$my_alerts_response" "Get My Alerts" + echo "" + + # 7. Получение ближайших оповещений + info "7. Получение ближайших оповещений" + local nearby_response=$(make_request "GET" "/api/v1/alerts/nearby?latitude=55.7558&longitude=37.6176&radius_km=10" "" "$auth_token") + check_response "$nearby_response" "Get Nearby Alerts" + echo "" + + # 8. Создание отчета о происшествии + info "8. Создание отчета о происшествии" + local report_data='{ + "latitude": 55.7500, + "longitude": 37.6200, + "report_type": "harassment", + "description": "Тестовый отчет о происшествии в районе метро", + "address": "Тверская улица, Москва", + "severity": 3, + "is_anonymous": false + }' + + local report_response=$(make_request "POST" "/api/v1/report" "$report_data" "$auth_token") + check_response "$report_response" "Create Report" + echo "" + + # 9. Получение отчетов + info "9. Получение отчетов" + local reports_response=$(make_request "GET" "/api/v1/reports" "" "$auth_token") + check_response "$reports_response" "Get Reports" + echo "" + + # 10. Создание отметки безопасности + info "10. Создание отметки безопасности" + local safety_data='{ + "message": "Добрался домой, все хорошо", + "location_latitude": 55.7600, + "location_longitude": 37.6100 + }' + + local safety_response=$(make_request "POST" "/api/v1/safety-check" "$safety_data" "$auth_token") + check_response "$safety_response" "Create Safety Check" + echo "" + + # 11. Получение отметок безопасности + info "11. Получение отметок безопасности" + local safety_checks_response=$(make_request "GET" "/api/v1/safety-checks" "" "$auth_token") + check_response "$safety_checks_response" "Get Safety Checks" + echo "" + + # 12. Тест отклика на оповещение (если есть активные оповещения) + if [ -n "$alert_id" ] && [ "$alert_id" != "null" ]; then + info "12. Создание второго пользователя для отклика" + local second_token + second_token=$(get_auth_token) + if [ $? -eq 0 ]; then + info "13. Отклик на оповещение" + local response_data='{ + "response_type": "help_on_way", + "message": "Еду к вам, буду через 15 минут!", + "eta_minutes": 15 + }' + + local respond_response=$(make_request "POST" "/api/v1/alert/${alert_id}/respond" "$response_data" "$second_token") + check_response "$respond_response" "Respond to Alert" + echo "" + + # 14. Получение откликов на оповещение + info "14. Получение откликов на оповещение" + local responses_response=$(make_request "GET" "/api/v1/alert/${alert_id}/responses" "" "$auth_token") + check_response "$responses_response" "Get Alert Responses" + echo "" + + # 15. Обновление оповещения + info "15. Обновление оповещения" + local update_data='{ + "message": "Обновленное описание ситуации" + }' + + local update_response=$(make_request "PUT" "/api/v1/alert/${alert_id}" "$update_data" "$auth_token") + check_response "$update_response" "Update Alert" + echo "" + + # 16. Решение оповещения + info "16. Решение оповещения" + local resolve_response=$(make_request "PUT" "/api/v1/alert/${alert_id}/resolve" "" "$auth_token") + check_response "$resolve_response" "Resolve Alert" + echo "" + fi + fi + + info "✅ Тестирование Emergency Service API завершено" + success "Все основные endpoints протестированы!" +} + +# Запуск основной функции +main "$@" \ No newline at end of file diff --git a/test_fixed_endpoints.py b/test_fixed_endpoints.py deleted file mode 100644 index 4a4247e..0000000 --- a/test_fixed_endpoints.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 - -import json -import requests -import time - -def test_fixed_endpoints(): - """Test the fixed endpoints on production server""" - base_url = "http://192.168.0.103:8000" - - # First, login to get access token - print("🔐 Step 1: Login to get access token") - login_response = requests.post( - f"{base_url}/api/v1/auth/login", - json={ - "username": "Trevor1985", - "password": "Cl0ud_1985!" - }, - headers={"Content-Type": "application/json"} - ) - - if login_response.status_code != 200: - print(f"❌ Login failed: {login_response.status_code} - {login_response.text}") - return - - token_data = login_response.json() - access_token = token_data["access_token"] - print(f"✅ Login successful! Got access token.") - - # Test endpoints that were failing - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/json" - } - - test_endpoints = [ - { - "name": "User Profile", - "url": "/api/v1/users/me", - "method": "GET" - }, - { - "name": "User Dashboard (was 404)", - "url": "/api/v1/users/dashboard", - "method": "GET" - }, - { - "name": "Emergency Contacts (was 500)", - "url": "/api/v1/users/me/emergency-contacts", - "method": "GET" - }, - { - "name": "Profile (alternative endpoint)", - "url": "/api/v1/profile", - "method": "GET" - } - ] - - print(f"\n🧪 Step 2: Testing fixed endpoints") - print("=" * 60) - - for endpoint in test_endpoints: - print(f"\n📍 Testing: {endpoint['name']}") - print(f" URL: {endpoint['url']}") - - try: - if endpoint['method'] == 'GET': - response = requests.get( - f"{base_url}{endpoint['url']}", - headers=headers, - timeout=10 - ) - - print(f" Status: {response.status_code}") - - if response.status_code == 200: - try: - data = response.json() - if endpoint['name'] == "User Dashboard (was 404)": - print(f" ✅ Dashboard loaded! Emergency contacts: {data.get('emergency_contacts_count', 0)}") - print(f" User: {data.get('user', {}).get('username', 'N/A')}") - elif endpoint['name'] == "Emergency Contacts (was 500)": - contacts = data if isinstance(data, list) else [] - print(f" ✅ Emergency contacts loaded! Count: {len(contacts)}") - else: - print(f" ✅ Success! Got user data.") - except: - print(f" ✅ Success! (Response not JSON)") - else: - print(f" ❌ Failed: {response.text[:100]}...") - - except Exception as e: - print(f" 💥 Error: {str(e)}") - - time.sleep(0.5) - - print(f"\n{'='*60}") - print("📊 Summary:") - print("• Login: ✅ Working") - print("• Check each endpoint status above") - print("• All 422 login errors should be resolved") - print("• Database errors should be fixed") - -if __name__ == "__main__": - print("🚀 Testing fixed endpoints on production server 192.168.0.103") - test_fixed_endpoints() \ No newline at end of file diff --git a/test_login.py b/test_login.py deleted file mode 100755 index cdf8d5c..0000000 --- a/test_login.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 - -import json -import requests -import time - -# Test login endpoint with detailed logging -def test_login(): - """Test login with various scenarios""" - base_url = "http://localhost:8000" - - # Test cases - test_cases = [ - { - "name": "Empty request", - "data": {} - }, - { - "name": "Only password", - "data": {"password": "testpass123"} - }, - { - "name": "Only email", - "data": {"email": "test@example.com"} - }, - { - "name": "Valid email and password", - "data": { - "email": "test@example.com", - "password": "testpass123" - } - }, - { - "name": "Valid username and password", - "data": { - "username": "testuser", - "password": "testpass123" - } - }, - { - "name": "Invalid JSON format", - "data": "invalid json", - "content_type": "text/plain" - } - ] - - for test_case in test_cases: - print(f"\n🧪 Testing: {test_case['name']}") - print("=" * 50) - - try: - headers = { - "Content-Type": test_case.get("content_type", "application/json"), - "Accept": "application/json" - } - - if isinstance(test_case["data"], dict): - data = json.dumps(test_case["data"]) - print(f"Request data: {data}") - else: - data = test_case["data"] - print(f"Request data (raw): {data}") - - response = requests.post( - f"{base_url}/api/v1/auth/login", - data=data, - headers=headers, - timeout=10 - ) - - print(f"Status Code: {response.status_code}") - print(f"Headers: {dict(response.headers)}") - print(f"Response: {response.text}") - - except Exception as e: - print(f"Error: {str(e)}") - - time.sleep(1) # Wait between requests - -if __name__ == "__main__": - print("🔍 Testing login endpoint with detailed logging") - print("Make sure to check the server logs for debugging info") - test_login() \ No newline at end of file diff --git a/test_mobile_formats.py b/test_mobile_formats.py deleted file mode 100644 index 87766fe..0000000 --- a/test_mobile_formats.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python3 - -import json -import requests -import time - -def test_mobile_app_formats(): - """Test various data formats that mobile apps commonly send""" - base_url = "http://192.168.0.103:8000" - - # Common mobile app data format issues - test_cases = [ - { - "name": "Android app - nested data structure", - "data": { - "user": { - "email": "testuser@example.com", - "password": "SecurePass123" - } - } - }, - { - "name": "iOS app - camelCase fields", - "data": { - "emailAddress": "testuser@example.com", - "userPassword": "SecurePass123" - } - }, - { - "name": "React Native - mixed case", - "data": { - "Email": "testuser@example.com", - "Password": "SecurePass123" - } - }, - { - "name": "Flutter app - snake_case", - "data": { - "user_email": "testuser@example.com", - "user_password": "SecurePass123" - } - }, - { - "name": "Mobile app with extra fields", - "data": { - "email": "testuser@example.com", - "password": "SecurePass123", - "device_id": "mobile123", - "app_version": "1.0.0", - "platform": "android" - } - }, - { - "name": "Empty string fields (common mobile bug)", - "data": { - "email": "", - "username": "", - "password": "SecurePass123" - } - }, - { - "name": "Null fields (another common mobile bug)", - "data": { - "email": None, - "username": None, - "password": "SecurePass123" - } - }, - { - "name": "Correct format", - "data": { - "email": "testuser@example.com", - "password": "SecurePass123" - } - } - ] - - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": "WomenSafetyApp/1.0 (Android)" - } - - print("📱 Testing mobile app data formats on 192.168.0.103") - print("=" * 70) - - for i, test_case in enumerate(test_cases, 1): - print(f"\n{i}. 🧪 {test_case['name']}") - print("-" * 50) - - try: - # Send request - response = requests.post( - f"{base_url}/api/v1/auth/login", - json=test_case["data"], - headers=headers, - timeout=10 - ) - - print(f"📤 Request: {json.dumps(test_case['data'], indent=2)}") - print(f"📊 Status: {response.status_code}") - - # Analyze response - if response.status_code == 200: - print("✅ SUCCESS - Login worked!") - try: - token_data = response.json() - print(f"🔐 Token type: {token_data.get('token_type')}") - except: - pass - - elif response.status_code == 422: - print("❌ VALIDATION ERROR") - try: - error_data = response.json() - if "detail" in error_data: - detail = error_data["detail"] - if isinstance(detail, list): - print("📝 Validation issues:") - for error in detail: - field = error.get("loc", [])[-1] if error.get("loc") else "unknown" - msg = error.get("msg", "Unknown error") - input_val = error.get("input", "") - print(f" • Field '{field}': {msg} (input: {input_val})") - else: - print(f"📝 Error: {detail}") - except Exception as e: - print(f"📝 Raw error: {response.text}") - - elif response.status_code == 401: - print("🔒 AUTHENTICATION FAILED - Wrong credentials") - - else: - print(f"🚫 OTHER ERROR: {response.status_code}") - print(f"📝 Response: {response.text[:200]}") - - except Exception as e: - print(f"💥 REQUEST ERROR: {str(e)}") - - time.sleep(0.5) - - print(f"\n{'='*70}") - print("📋 SUMMARY:") - print("• Check which format works correctly") - print("• Compare with mobile app's actual request format") - print("• Update mobile app to match working format") - -if __name__ == "__main__": - test_mobile_app_formats() \ No newline at end of file diff --git a/test_production_login.py b/test_production_login.py deleted file mode 100644 index f348f09..0000000 --- a/test_production_login.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 - -import json -import requests -import time - -def test_production_login(): - """Test login endpoint on production server""" - base_url = "http://192.168.0.103:8000" - - # Test cases that might be coming from mobile app - test_cases = [ - { - "name": "Empty request (like from emulator)", - "data": {} - }, - { - "name": "Only password", - "data": {"password": "testpass123"} - }, - { - "name": "Email with extra spaces", - "data": { - "email": " testuser@example.com ", - "password": "SecurePass123" - } - }, - { - "name": "Valid login data", - "data": { - "email": "testuser@example.com", - "password": "SecurePass123" - } - }, - { - "name": "Username login", - "data": { - "username": "testuser123", - "password": "SecurePass123" - } - }, - { - "name": "Invalid JSON structure (common mobile app error)", - "data": '{"email":"test@example.com","password":"test123"', - "raw": True - } - ] - - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": "MobileApp/1.0" - } - - for test_case in test_cases: - print(f"\n🧪 Testing: {test_case['name']}") - print("=" * 60) - - try: - if test_case.get("raw"): - data = test_case["data"] - print(f"Raw data: {data}") - else: - data = json.dumps(test_case["data"]) - print(f"JSON data: {data}") - - response = requests.post( - f"{base_url}/api/v1/auth/login", - data=data, - headers=headers, - timeout=10 - ) - - print(f"Status: {response.status_code}") - print(f"Response: {response.text[:200]}...") - - if response.status_code == 422: - print("🔍 This is a validation error - checking details...") - try: - error_details = response.json() - if "detail" in error_details: - detail = error_details["detail"] - if isinstance(detail, list): - for error in detail: - field = error.get("loc", [])[-1] if error.get("loc") else "unknown" - msg = error.get("msg", "Unknown error") - print(f" Field '{field}': {msg}") - else: - print(f" Error: {detail}") - except: - pass - - except Exception as e: - print(f"Request error: {str(e)}") - - time.sleep(1) - -if __name__ == "__main__": - print("🔍 Testing login endpoint on production server 192.168.0.103") - test_production_login() \ No newline at end of file diff --git a/token.env b/token.env new file mode 100644 index 0000000..d13849d --- /dev/null +++ b/token.env @@ -0,0 +1 @@ +TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOCIsImVtYWlsIjoiYXBpdGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTc1ODg1NzA3Nn0.HO3jIZIzAFz0ojGVmExnnrwu5bmw42nMVnnCDiu9xHI diff --git a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/INSTALLER b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/INSTALLER similarity index 100% rename from venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/INSTALLER rename to venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/INSTALLER diff --git a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/LICENSE b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/LICENSE similarity index 100% rename from venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/LICENSE rename to venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/LICENSE diff --git a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/METADATA b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/METADATA similarity index 83% rename from venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/METADATA rename to venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/METADATA index 96f0bdf..789f784 100644 --- a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/METADATA +++ b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/METADATA @@ -1,29 +1,30 @@ -Metadata-Version: 2.2 +Metadata-Version: 2.1 Name: bcrypt -Version: 4.3.0 +Version: 4.0.1 Summary: Modern password hashing for your software and your servers -Author-email: The Python Cryptographic Authority developers -License: Apache-2.0 -Project-URL: homepage, https://github.com/pyca/bcrypt/ +Home-page: https://github.com/pyca/bcrypt/ +Author: The Python Cryptographic Authority developers +Author-email: cryptography-dev@python.org +License: Apache License, Version 2.0 +Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Requires-Python: >=3.8 +Requires-Python: >=3.6 Description-Content-Type: text/x-rst License-File: LICENSE Provides-Extra: tests -Requires-Dist: pytest!=3.3.0,>=3.2.1; extra == "tests" +Requires-Dist: pytest (!=3.3.0,>=3.2.1) ; extra == 'tests' Provides-Extra: typecheck -Requires-Dist: mypy; extra == "typecheck" +Requires-Dist: mypy ; extra == 'typecheck' bcrypt ====== @@ -44,7 +45,7 @@ Installation To install bcrypt, simply: -.. code:: console +.. code:: bash $ pip install bcrypt @@ -53,19 +54,19 @@ compiler and a Rust compiler (the minimum supported Rust version is 1.56.0). For Debian and Ubuntu, the following command will ensure that the required dependencies are installed: -.. code:: console +.. code:: bash $ sudo apt-get install build-essential cargo For Fedora and RHEL-derivatives, the following command will ensure that the required dependencies are installed: -.. code:: console +.. code:: bash $ sudo yum install gcc cargo For Alpine, the following command will ensure that the required dependencies are installed: -.. code:: console +.. code:: bash $ apk add --update musl-dev gcc cargo @@ -78,52 +79,6 @@ While bcrypt remains an acceptable choice for password storage, depending on you Changelog ========= -Unreleased ----------- - -* Dropped support for Python 3.7. -* We now support free-threaded Python 3.13. -* We now support PyPy 3.11. -* We now publish wheels for free-threaded Python 3.13, for PyPy 3.11 on - ``manylinux``, and for ARMv7l on ``manylinux``. - -4.2.1 ------ - -* Bump Rust dependency versions - this should resolve crashes on Python 3.13 - free-threaded builds. -* We no longer build ``manylinux`` wheels for PyPy 3.9. - -4.2.0 ------ - -* Bump Rust dependency versions -* Removed the ``BCRYPT_ALLOW_RUST_163`` environment variable. - -4.1.3 ------ - -* Bump Rust dependency versions - -4.1.2 ------ - -* Publish both ``py37`` and ``py39`` wheels. This should resolve some errors - relating to initializing a module multiple times per process. - -4.1.1 ------ - -* Fixed the type signature on the ``kdf`` method. -* Fixed packaging bug on Windows. -* Fixed incompatibility with passlib package detection assumptions. - -4.1.0 ------ - -* Dropped support for Python 3.6. -* Bumped MSRV to 1.64. (Note: Rust 1.63 can be used by setting the ``BCRYPT_ALLOW_RUST_163`` environment variable) - 4.0.1 ----- @@ -316,7 +271,12 @@ Compatibility ------------- This library should be compatible with py-bcrypt and it will run on Python -3.8+ (including free-threaded builds), and PyPy 3. +3.6+, and PyPy 3. + +C Code +------ + +This library uses code from OpenBSD. Security -------- @@ -328,3 +288,5 @@ identify a vulnerability, we ask you to contact us privately. .. _`standard library`: https://docs.python.org/3/library/hashlib.html#hashlib.scrypt .. _`argon2_cffi`: https://argon2-cffi.readthedocs.io .. _`cryptography`: https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#cryptography.hazmat.primitives.kdf.scrypt.Scrypt + + diff --git a/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/RECORD b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/RECORD new file mode 100644 index 0000000..217979e --- /dev/null +++ b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/RECORD @@ -0,0 +1,14 @@ +bcrypt-4.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +bcrypt-4.0.1.dist-info/LICENSE,sha256=gXPVwptPlW1TJ4HSuG5OMPg-a3h43OGMkZRR1rpwfJA,10850 +bcrypt-4.0.1.dist-info/METADATA,sha256=peZwWFa95xnpp4NiIE7gJkV01CTkbVXIzoEN66SXd3c,8972 +bcrypt-4.0.1.dist-info/RECORD,, +bcrypt-4.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +bcrypt-4.0.1.dist-info/WHEEL,sha256=ZXaM-AC_dnzk1sUAdQV_bMrIMG6zI-GthFaEkNkWsgU,112 +bcrypt-4.0.1.dist-info/top_level.txt,sha256=BkR_qBzDbSuycMzHWE1vzXrfYecAzUVmQs6G2CukqNI,7 +bcrypt/__about__.py,sha256=F7i0CQOa8G3Yjw1T71jQv8yi__Z_4TzLyZJv1GFqVx0,1320 +bcrypt/__init__.py,sha256=EpUdbfHaiHlSoaM-SSUB6MOgNpWOIkS0ZrjxogPIRLM,3781 +bcrypt/__pycache__/__about__.cpython-312.pyc,, +bcrypt/__pycache__/__init__.cpython-312.pyc,, +bcrypt/_bcrypt.abi3.so,sha256=_T-y5IrekziUzkYio4hWH7Xzw92XBKewSLd8kmERhGU,1959696 +bcrypt/_bcrypt.pyi,sha256=O-vvHdooGyAxIkdKemVqOzBF5aMhh0evPSaDMgETgEk,214 +bcrypt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/-H b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/REQUESTED similarity index 100% rename from -H rename to venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/REQUESTED diff --git a/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/WHEEL b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/WHEEL new file mode 100644 index 0000000..dc50279 --- /dev/null +++ b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: false +Tag: cp36-abi3-manylinux_2_28_x86_64 + diff --git a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/top_level.txt b/venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/top_level.txt similarity index 100% rename from venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/top_level.txt rename to venv/lib/python3.12/site-packages/bcrypt-4.0.1.dist-info/top_level.txt diff --git a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/RECORD b/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/RECORD deleted file mode 100644 index 717568c..0000000 --- a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/RECORD +++ /dev/null @@ -1,11 +0,0 @@ -bcrypt-4.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -bcrypt-4.3.0.dist-info/LICENSE,sha256=gXPVwptPlW1TJ4HSuG5OMPg-a3h43OGMkZRR1rpwfJA,10850 -bcrypt-4.3.0.dist-info/METADATA,sha256=95qX7ziIfmOF0kNM95YZuWhLVfFy-6EtssVvf1ZgeWg,10042 -bcrypt-4.3.0.dist-info/RECORD,, -bcrypt-4.3.0.dist-info/WHEEL,sha256=XlovOtcAZFqrc4OSNBtc5R3yDeRHyhWP24RdDnylFpY,111 -bcrypt-4.3.0.dist-info/top_level.txt,sha256=BkR_qBzDbSuycMzHWE1vzXrfYecAzUVmQs6G2CukqNI,7 -bcrypt/__init__.py,sha256=cv-NupIX6P7o6A4PK_F0ur6IZoDr3GnvyzFO9k16wKQ,1000 -bcrypt/__init__.pyi,sha256=ITUCB9mPVU8sKUbJQMDUH5YfQXZb1O55F9qvKZR_o8I,333 -bcrypt/__pycache__/__init__.cpython-312.pyc,, -bcrypt/_bcrypt.abi3.so,sha256=oMArVCuY_atg2H4SGNfM-zbfEgUOkd4qSiWn2nPqmXc,644928 -bcrypt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/WHEEL b/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/WHEEL deleted file mode 100644 index dd95e91..0000000 --- a/venv/lib/python3.12/site-packages/bcrypt-4.3.0.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (75.8.2) -Root-Is-Purelib: false -Tag: cp39-abi3-manylinux_2_34_x86_64 - diff --git a/venv/lib/python3.12/site-packages/bcrypt/__about__.py b/venv/lib/python3.12/site-packages/bcrypt/__about__.py new file mode 100644 index 0000000..020b748 --- /dev/null +++ b/venv/lib/python3.12/site-packages/bcrypt/__about__.py @@ -0,0 +1,41 @@ +# Author:: Donald Stufft () +# Copyright:: Copyright (c) 2013 Donald Stufft +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + +__all__ = [ + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", +] + +__title__ = "bcrypt" +__summary__ = "Modern password hashing for your software and your servers" +__uri__ = "https://github.com/pyca/bcrypt/" + +__version__ = "4.0.1" + +__author__ = "The Python Cryptographic Authority developers" +__email__ = "cryptography-dev@python.org" + +__license__ = "Apache License, Version 2.0" +__copyright__ = "Copyright 2013-2022 {0}".format(__author__) diff --git a/venv/lib/python3.12/site-packages/bcrypt/__init__.py b/venv/lib/python3.12/site-packages/bcrypt/__init__.py index 81a92fd..1f2886f 100644 --- a/venv/lib/python3.12/site-packages/bcrypt/__init__.py +++ b/venv/lib/python3.12/site-packages/bcrypt/__init__.py @@ -1,3 +1,7 @@ +# Author:: Donald Stufft () +# Copyright:: Copyright (c) 2013 Donald Stufft +# License:: Apache License, Version 2.0 +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -9,8 +13,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import absolute_import +from __future__ import division -from ._bcrypt import ( +import hmac +import os +import warnings + +from .__about__ import ( __author__, __copyright__, __email__, @@ -18,26 +28,100 @@ from ._bcrypt import ( __summary__, __title__, __uri__, - checkpw, - gensalt, - hashpw, - kdf, -) -from ._bcrypt import ( - __version_ex__ as __version__, + __version__, ) +from . import _bcrypt # noqa: I100 + __all__ = [ - "__author__", - "__copyright__", - "__email__", - "__license__", - "__summary__", "__title__", + "__summary__", "__uri__", "__version__", - "checkpw", + "__author__", + "__email__", + "__license__", + "__copyright__", "gensalt", "hashpw", "kdf", + "checkpw", ] + + +def gensalt(rounds: int = 12, prefix: bytes = b"2b") -> bytes: + if prefix not in (b"2a", b"2b"): + raise ValueError("Supported prefixes are b'2a' or b'2b'") + + if rounds < 4 or rounds > 31: + raise ValueError("Invalid rounds") + + salt = os.urandom(16) + output = _bcrypt.encode_base64(salt) + + return ( + b"$" + + prefix + + b"$" + + ("%2.2u" % rounds).encode("ascii") + + b"$" + + output + ) + + +def hashpw(password: bytes, salt: bytes) -> bytes: + if isinstance(password, str) or isinstance(salt, str): + raise TypeError("Strings must be encoded before hashing") + + # bcrypt originally suffered from a wraparound bug: + # http://www.openwall.com/lists/oss-security/2012/01/02/4 + # This bug was corrected in the OpenBSD source by truncating inputs to 72 + # bytes on the updated prefix $2b$, but leaving $2a$ unchanged for + # compatibility. However, pyca/bcrypt 2.0.0 *did* correctly truncate inputs + # on $2a$, so we do it here to preserve compatibility with 2.0.0 + password = password[:72] + + return _bcrypt.hashpass(password, salt) + + +def checkpw(password: bytes, hashed_password: bytes) -> bool: + if isinstance(password, str) or isinstance(hashed_password, str): + raise TypeError("Strings must be encoded before checking") + + ret = hashpw(password, hashed_password) + return hmac.compare_digest(ret, hashed_password) + + +def kdf( + password: bytes, + salt: bytes, + desired_key_bytes: int, + rounds: int, + ignore_few_rounds: bool = False, +) -> bytes: + if isinstance(password, str) or isinstance(salt, str): + raise TypeError("Strings must be encoded before hashing") + + if len(password) == 0 or len(salt) == 0: + raise ValueError("password and salt must not be empty") + + if desired_key_bytes <= 0 or desired_key_bytes > 512: + raise ValueError("desired_key_bytes must be 1-512") + + if rounds < 1: + raise ValueError("rounds must be 1 or more") + + if rounds < 50 and not ignore_few_rounds: + # They probably think bcrypt.kdf()'s rounds parameter is logarithmic, + # expecting this value to be slow enough (it probably would be if this + # were bcrypt). Emit a warning. + warnings.warn( + ( + "Warning: bcrypt.kdf() called with only {0} round(s). " + "This few is not secure: the parameter is linear, like PBKDF2." + ).format(rounds), + UserWarning, + stacklevel=2, + ) + + return _bcrypt.pbkdf(password, salt, rounds, desired_key_bytes) diff --git a/venv/lib/python3.12/site-packages/bcrypt/__init__.pyi b/venv/lib/python3.12/site-packages/bcrypt/__init__.pyi deleted file mode 100644 index 12e4a2e..0000000 --- a/venv/lib/python3.12/site-packages/bcrypt/__init__.pyi +++ /dev/null @@ -1,10 +0,0 @@ -def gensalt(rounds: int = 12, prefix: bytes = b"2b") -> bytes: ... -def hashpw(password: bytes, salt: bytes) -> bytes: ... -def checkpw(password: bytes, hashed_password: bytes) -> bool: ... -def kdf( - password: bytes, - salt: bytes, - desired_key_bytes: int, - rounds: int, - ignore_few_rounds: bool = False, -) -> bytes: ... diff --git a/venv/lib/python3.12/site-packages/bcrypt/_bcrypt.abi3.so b/venv/lib/python3.12/site-packages/bcrypt/_bcrypt.abi3.so index ae9f55b..5651953 100755 Binary files a/venv/lib/python3.12/site-packages/bcrypt/_bcrypt.abi3.so and b/venv/lib/python3.12/site-packages/bcrypt/_bcrypt.abi3.so differ diff --git a/venv/lib/python3.12/site-packages/bcrypt/_bcrypt.pyi b/venv/lib/python3.12/site-packages/bcrypt/_bcrypt.pyi new file mode 100644 index 0000000..640e913 --- /dev/null +++ b/venv/lib/python3.12/site-packages/bcrypt/_bcrypt.pyi @@ -0,0 +1,7 @@ +import typing + +def encode_base64(data: bytes) -> bytes: ... +def hashpass(password: bytes, salt: bytes) -> bytes: ... +def pbkdf( + password: bytes, salt: bytes, rounds: int, desired_key_bytes: int +) -> bytes: ...