This commit is contained in:
1
-d
1
-d
@@ -1 +0,0 @@
|
|||||||
{"detail":[{"type":"missing","loc":["body"],"msg":"Field required","input":null,"url":"https://errors.pydantic.dev/2.4/v/missing"}]}
|
|
||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# Результаты тестирования API
|
|
||||||
Дата: 2025-09-25 14:57:40
|
|
||||||
## Итоги
|
|
||||||
- Всего тестов: 32
|
|
||||||
- Успешно: 13
|
|
||||||
- Ошибок: 19
|
|
||||||
|
|
||||||
## Подробности
|
|
||||||
[0;32m✓ УСПЕШНО: Сервис API Gateway доступен[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Сервис User Service доступен[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Сервис Emergency Service доступен[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Сервис Location Service доступен[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Сервис Calendar Service доступен[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Сервис Notification Service доступен[0m
|
|
||||||
[0;31m✗ ОШИБКА: Регистрация пользователя[0m
|
|
||||||
[0;31m✗ ОШИБКА: Проверка на дублирование пользователя (должна быть ошибка)[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Получение токена авторизации[0m
|
|
||||||
[0;31m✗ ОШИБКА: Проверка авторизации с неверным паролем (должна быть ошибка)[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Получение профиля пользователя[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Получение профиля через альтернативный эндпоинт[0m
|
|
||||||
[0;31m✗ ОШИБКА: Обновление профиля пользователя[0m
|
|
||||||
[0;31m✗ ОШИБКА: Проверка обновления имени в профиле[0m
|
|
||||||
[0;31m✗ ОШИБКА: Добавление экстренного контакта[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Получение списка экстренных контактов (найдено: 1)[0m
|
|
||||||
[0;31m✗ ОШИБКА: Обновление местоположения[0m
|
|
||||||
[0;31m✗ ОШИБКА: Получение последнего местоположения[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Получение истории местоположений[0m
|
|
||||||
[0;31m✗ ОШИБКА: Создание экстренного оповещения[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Получение активных оповещений (найдено: 1)[0m
|
|
||||||
[0;31m✗ ОШИБКА: Получение статистики по оповещениям[0m
|
|
||||||
[0;31m✗ ОШИБКА: Создание записи в календаре[0m
|
|
||||||
[0;32m✓ УСПЕШНО: Получение записей календаря (найдено: 1)[0m
|
|
||||||
[0;31m✗ ОШИБКА: Получение прогноза цикла[0m
|
|
||||||
[0;31m✗ ОШИБКА: Регистрация устройства для уведомлений[0m
|
|
||||||
[0;31m✗ ОШИБКА: Настройка предпочтений уведомлений[0m
|
|
||||||
[0;31m✗ ОШИБКА: Получение предпочтений уведомлений[0m
|
|
||||||
[0;31m✗ ОШИБКА: Отправка тестового уведомления[0m
|
|
||||||
[0;31m✗ ОШИБКА: Получение данных пользователя через API Gateway[0m
|
|
||||||
[0;31m✗ ОШИБКА: Получение OpenAPI схемы[0m
|
|
||||||
[0;31m✗ ОШИБКА: Проверка наличия схем данных[0m
|
|
||||||
617
docs/DATA_SCHEMAS.md
Normal file
617
docs/DATA_SCHEMAS.md
Normal file
@@ -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<TokenResponse>;
|
||||||
|
register(userData: UserRegister): Promise<UserProfile>;
|
||||||
|
getProfile(): Promise<UserProfile>;
|
||||||
|
createAlert(alertData: EmergencyAlertCreate): Promise<EmergencyAlert>;
|
||||||
|
getNearbyAlerts(lat: number, lng: number, radius?: number): Promise<NearbyAlert[]>;
|
||||||
|
respondToAlert(alertId: number, response: EmergencyResponseCreate): Promise<EmergencyResponse>;
|
||||||
|
updateLocation(location: LocationUpdate): Promise<void>;
|
||||||
|
createSafetyCheck(data: { message?: string; location_latitude?: number; location_longitude?: number }): Promise<any>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Валидация данных
|
||||||
|
|
||||||
|
### Ограничения полей
|
||||||
|
- **Координаты**: 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.
|
||||||
600
docs/EMERGENCY_SERVICE_API.md
Normal file
600
docs/EMERGENCY_SERVICE_API.md
Normal file
@@ -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 токены имеют ограниченный срок действия
|
||||||
495
docs/MOBILE_API_SPECS.md
Normal file
495
docs/MOBILE_API_SPECS.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт проверит:
|
||||||
|
- Регистрацию и авторизацию
|
||||||
|
- Создание и управление оповещениями
|
||||||
|
- Отклики на оповещения
|
||||||
|
- Отчеты и отметки безопасности
|
||||||
|
- Статистику и аналитику
|
||||||
589
docs/MOBILE_INTEGRATION_EXAMPLES.md
Normal file
589
docs/MOBILE_INTEGRATION_EXAMPLES.md
Normal file
@@ -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 (
|
||||||
|
<View style={{ alignItems: 'center', margin: 20 }}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 60,
|
||||||
|
backgroundColor: isCreating ? '#ccc' : '#ff4444',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
elevation: 5
|
||||||
|
}}
|
||||||
|
onPress={handleSOSPress}
|
||||||
|
disabled={isCreating}
|
||||||
|
>
|
||||||
|
<Text style={{ color: 'white', fontSize: 18, fontWeight: 'bold' }}>
|
||||||
|
{isCreating ? 'Отправка...' : 'SOS'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 }) => (
|
||||||
|
<View style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
margin: 10,
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: 10,
|
||||||
|
elevation: 2
|
||||||
|
}}>
|
||||||
|
<Text style={{ fontWeight: 'bold', fontSize: 16 }}>
|
||||||
|
{item.alert_type.toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: '#666', marginTop: 5 }}>
|
||||||
|
📍 {item.address || 'Адрес не указан'}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: '#666' }}>
|
||||||
|
📏 {item.distance_km.toFixed(1)} км от вас
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: '#666' }}>
|
||||||
|
👥 Откликов: {item.responded_users_count}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#007AFF',
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
marginTop: 10,
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
onPress={() => handleRespondToAlert(item.id)}
|
||||||
|
>
|
||||||
|
<Text style={{ color: 'white', fontWeight: 'bold' }}>
|
||||||
|
Помочь
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={{ fontSize: 18, fontWeight: 'bold', margin: 15 }}>
|
||||||
|
Ближайшие оповещения
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Text style={{ textAlign: 'center', margin: 20 }}>
|
||||||
|
Загрузка...
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={alerts}
|
||||||
|
renderItem={renderAlert}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
refreshing={loading}
|
||||||
|
onRefresh={loadNearbyAlerts}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Text style={{ textAlign: 'center', margin: 20, color: '#666' }}>
|
||||||
|
Поблизости нет активных оповещений
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Обновление местоположения в реальном времени
|
||||||
|
|
||||||
|
```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 в реальном мобильном приложении, включая безопасность, производительность и тестирование.
|
||||||
1
login.json
Normal file
1
login.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOCIsImVtYWlsIjoiYXBpdGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTc1ODg1NzA3Nn0.HO3jIZIzAFz0ojGVmExnnrwu5bmw42nMVnnCDiu9xHI","token_type":"bearer"}
|
||||||
@@ -1,25 +1,56 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List
|
from typing import List, Optional
|
||||||
|
import math
|
||||||
|
|
||||||
import httpx
|
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 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 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 (
|
from services.emergency_service.schemas import (
|
||||||
|
AlertStatus,
|
||||||
|
AlertType,
|
||||||
EmergencyAlertCreate,
|
EmergencyAlertCreate,
|
||||||
EmergencyAlertResponse,
|
EmergencyAlertResponse,
|
||||||
|
EmergencyAlertUpdate,
|
||||||
EmergencyResponseCreate,
|
EmergencyResponseCreate,
|
||||||
EmergencyResponseResponse,
|
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.auth import get_current_user_from_token
|
||||||
from shared.config import settings
|
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")
|
app = FastAPI(title="Emergency Service", version="1.0.0")
|
||||||
|
|
||||||
@@ -48,6 +79,21 @@ async def get_current_user(
|
|||||||
return 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")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Health check endpoint"""
|
"""Health check endpoint"""
|
||||||
@@ -176,7 +222,7 @@ async def respond_to_alert(
|
|||||||
existing_response = await db.execute(
|
existing_response = await db.execute(
|
||||||
select(EmergencyResponse).filter(
|
select(EmergencyResponse).filter(
|
||||||
EmergencyResponse.alert_id == alert_id,
|
EmergencyResponse.alert_id == alert_id,
|
||||||
EmergencyResponse.user_id == current_user.id
|
EmergencyResponse.responder_id == current_user.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if existing_response.scalars().first():
|
if existing_response.scalars().first():
|
||||||
@@ -188,7 +234,7 @@ async def respond_to_alert(
|
|||||||
# Create response
|
# Create response
|
||||||
db_response = EmergencyResponse(
|
db_response = EmergencyResponse(
|
||||||
alert_id=alert_id,
|
alert_id=alert_id,
|
||||||
user_id=current_user.id,
|
responder_id=current_user.id,
|
||||||
response_type=response_data.response_type,
|
response_type=response_data.response_type,
|
||||||
message=response_data.message,
|
message=response_data.message,
|
||||||
eta_minutes=response_data.eta_minutes,
|
eta_minutes=response_data.eta_minutes,
|
||||||
@@ -206,7 +252,12 @@ async def respond_to_alert(
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(db_response)
|
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")
|
@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]
|
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(
|
async def get_emergency_stats(
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db)
|
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_result = await db.execute(select(func.count(EmergencyResponse.id)))
|
||||||
total_responders = total_responders_result.scalar() or 0
|
total_responders = total_responders_result.scalar() or 0
|
||||||
|
|
||||||
return EmergencyStats(
|
return EmergencyStatistics(
|
||||||
total_alerts=total_alerts,
|
total_alerts=total_alerts,
|
||||||
active_alerts=active_alerts,
|
active_alerts=active_alerts,
|
||||||
resolved_alerts=resolved_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,
|
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__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8002)
|
uvicorn.run(app, host="0.0.0.0", port=8002)
|
||||||
@@ -27,9 +27,7 @@ class EmergencyAlert(BaseModel):
|
|||||||
address = Column(String(500))
|
address = Column(String(500))
|
||||||
|
|
||||||
# Alert details
|
# Alert details
|
||||||
alert_type = Column(
|
alert_type = Column(String(50), default="general") # general, medical, violence, etc.
|
||||||
String(50), default="general"
|
|
||||||
) # general, medical, violence, etc.
|
|
||||||
message = Column(Text)
|
message = Column(Text)
|
||||||
is_resolved = Column(Boolean, default=False)
|
is_resolved = Column(Boolean, default=False)
|
||||||
resolved_at = Column(DateTime(timezone=True))
|
resolved_at = Column(DateTime(timezone=True))
|
||||||
@@ -46,9 +44,7 @@ class EmergencyAlert(BaseModel):
|
|||||||
class EmergencyResponse(BaseModel):
|
class EmergencyResponse(BaseModel):
|
||||||
__tablename__ = "emergency_responses"
|
__tablename__ = "emergency_responses"
|
||||||
|
|
||||||
alert_id = Column(
|
alert_id = Column(Integer, ForeignKey("emergency_alerts.id"), nullable=False, index=True)
|
||||||
Integer, ForeignKey("emergency_alerts.id"), nullable=False, index=True
|
|
||||||
)
|
|
||||||
responder_id = Column(Integer, ForeignKey("users.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.
|
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
|
eta_minutes = Column(Integer) # Estimated time of arrival
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<EmergencyResponse {self.id}>"
|
return f"<EmergencyResponse {self.uuid}>"
|
||||||
|
|
||||||
|
|
||||||
|
# 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"<EmergencyReport {self.uuid}>"
|
||||||
|
|
||||||
|
|
||||||
|
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"<SafetyCheck {self.uuid}>"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -11,6 +12,16 @@ class AlertType(str, Enum):
|
|||||||
VIOLENCE = "violence"
|
VIOLENCE = "violence"
|
||||||
HARASSMENT = "harassment"
|
HARASSMENT = "harassment"
|
||||||
UNSAFE_AREA = "unsafe_area"
|
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):
|
class ResponseType(str, Enum):
|
||||||
@@ -18,30 +29,45 @@ class ResponseType(str, Enum):
|
|||||||
CONTACTED_AUTHORITIES = "contacted_authorities"
|
CONTACTED_AUTHORITIES = "contacted_authorities"
|
||||||
SAFE_NOW = "safe_now"
|
SAFE_NOW = "safe_now"
|
||||||
FALSE_ALARM = "false_alarm"
|
FALSE_ALARM = "false_alarm"
|
||||||
|
INVESTIGATING = "investigating"
|
||||||
|
RESOLVED = "resolved"
|
||||||
|
|
||||||
|
|
||||||
class EmergencyAlertCreate(BaseModel):
|
class EmergencyAlertCreate(BaseModel):
|
||||||
latitude: float = Field(..., ge=-90, le=90)
|
latitude: float = Field(..., ge=-90, le=90, description="Latitude coordinate")
|
||||||
longitude: float = Field(..., ge=-180, le=180)
|
longitude: float = Field(..., ge=-180, le=180, description="Longitude coordinate")
|
||||||
alert_type: AlertType = AlertType.GENERAL
|
alert_type: AlertType = AlertType.GENERAL
|
||||||
message: 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)
|
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):
|
class EmergencyAlertResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
uuid: str
|
uuid: UUID
|
||||||
user_id: int
|
user_id: int
|
||||||
latitude: float
|
latitude: float
|
||||||
longitude: float
|
longitude: float
|
||||||
address: Optional[str]
|
address: Optional[str] = None
|
||||||
alert_type: str
|
alert_type: AlertType
|
||||||
message: Optional[str]
|
message: Optional[str] = None
|
||||||
is_resolved: bool
|
is_resolved: bool = False
|
||||||
resolved_at: Optional[datetime]
|
resolved_at: Optional[datetime] = None
|
||||||
notified_users_count: int
|
resolved_notes: Optional[str] = None
|
||||||
responded_users_count: int
|
notified_users_count: int = 0
|
||||||
|
responded_users_count: int = 0
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
# User information
|
||||||
|
user_name: Optional[str] = None
|
||||||
|
user_phone: Optional[str] = None
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -49,33 +75,92 @@ class EmergencyAlertResponse(BaseModel):
|
|||||||
|
|
||||||
class EmergencyResponseCreate(BaseModel):
|
class EmergencyResponseCreate(BaseModel):
|
||||||
response_type: ResponseType
|
response_type: ResponseType
|
||||||
message: Optional[str] = Field(None, max_length=500)
|
message: Optional[str] = Field(None, max_length=500, description="Response message")
|
||||||
eta_minutes: Optional[int] = Field(None, ge=0, le=240) # Max 4 hours
|
eta_minutes: Optional[int] = Field(None, ge=0, le=240, description="Estimated time of arrival in minutes")
|
||||||
|
|
||||||
|
|
||||||
class EmergencyResponseResponse(BaseModel):
|
class EmergencyResponseResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
alert_id: int
|
alert_id: int
|
||||||
responder_id: int
|
responder_id: int
|
||||||
response_type: str
|
response_type: ResponseType
|
||||||
message: Optional[str]
|
message: Optional[str] = None
|
||||||
eta_minutes: Optional[int]
|
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
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class NearbyUsersResponse(BaseModel):
|
# Statistics schemas
|
||||||
user_id: int
|
class EmergencyStatistics(BaseModel):
|
||||||
distance_meters: float
|
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
|
latitude: float
|
||||||
longitude: float
|
longitude: float
|
||||||
|
address: Optional[str] = None
|
||||||
|
distance_km: float
|
||||||
|
created_at: datetime
|
||||||
|
responded_users_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
class EmergencyStats(BaseModel):
|
# Safety check schemas
|
||||||
total_alerts: int
|
class SafetyCheckCreate(BaseModel):
|
||||||
active_alerts: int
|
message: Optional[str] = Field(None, max_length=200)
|
||||||
resolved_alerts: int
|
location_latitude: Optional[float] = Field(None, ge=-90, le=90)
|
||||||
avg_response_time_minutes: Optional[float]
|
location_longitude: Optional[float] = Field(None, ge=-180, le=180)
|
||||||
total_responders: int
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
108
simple_test.sh
Executable file
108
simple_test.sh
Executable file
@@ -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 "Тестирование завершено!"
|
||||||
@@ -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()
|
|
||||||
264
test_emergency_api.sh
Executable file
264
test_emergency_api.sh
Executable file
@@ -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 "$@"
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
1
token.env
Normal file
1
token.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyOCIsImVtYWlsIjoiYXBpdGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTc1ODg1NzA3Nn0.HO3jIZIzAFz0ojGVmExnnrwu5bmw42nMVnnCDiu9xHI
|
||||||
@@ -1,29 +1,30 @@
|
|||||||
Metadata-Version: 2.2
|
Metadata-Version: 2.1
|
||||||
Name: bcrypt
|
Name: bcrypt
|
||||||
Version: 4.3.0
|
Version: 4.0.1
|
||||||
Summary: Modern password hashing for your software and your servers
|
Summary: Modern password hashing for your software and your servers
|
||||||
Author-email: The Python Cryptographic Authority developers <cryptography-dev@python.org>
|
Home-page: https://github.com/pyca/bcrypt/
|
||||||
License: Apache-2.0
|
Author: The Python Cryptographic Authority developers
|
||||||
Project-URL: homepage, https://github.com/pyca/bcrypt/
|
Author-email: cryptography-dev@python.org
|
||||||
|
License: Apache License, Version 2.0
|
||||||
|
Platform: UNKNOWN
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
Classifier: License :: OSI Approved :: Apache Software License
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
Classifier: Programming Language :: Python :: 3
|
Classifier: Programming Language :: Python :: 3
|
||||||
Classifier: Programming Language :: Python :: 3 :: Only
|
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.8
|
||||||
Classifier: Programming Language :: Python :: 3.9
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
Classifier: Programming Language :: Python :: 3.10
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
Classifier: Programming Language :: Python :: 3.11
|
Requires-Python: >=3.6
|
||||||
Classifier: Programming Language :: Python :: 3.12
|
|
||||||
Classifier: Programming Language :: Python :: 3.13
|
|
||||||
Requires-Python: >=3.8
|
|
||||||
Description-Content-Type: text/x-rst
|
Description-Content-Type: text/x-rst
|
||||||
License-File: LICENSE
|
License-File: LICENSE
|
||||||
Provides-Extra: tests
|
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
|
Provides-Extra: typecheck
|
||||||
Requires-Dist: mypy; extra == "typecheck"
|
Requires-Dist: mypy ; extra == 'typecheck'
|
||||||
|
|
||||||
bcrypt
|
bcrypt
|
||||||
======
|
======
|
||||||
@@ -44,7 +45,7 @@ Installation
|
|||||||
|
|
||||||
To install bcrypt, simply:
|
To install bcrypt, simply:
|
||||||
|
|
||||||
.. code:: console
|
.. code:: bash
|
||||||
|
|
||||||
$ pip install bcrypt
|
$ 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:
|
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
|
$ sudo apt-get install build-essential cargo
|
||||||
|
|
||||||
For Fedora and RHEL-derivatives, the following command will ensure that the required dependencies are installed:
|
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
|
$ sudo yum install gcc cargo
|
||||||
|
|
||||||
For Alpine, the following command will ensure that the required dependencies are installed:
|
For Alpine, the following command will ensure that the required dependencies are installed:
|
||||||
|
|
||||||
.. code:: console
|
.. code:: bash
|
||||||
|
|
||||||
$ apk add --update musl-dev gcc cargo
|
$ apk add --update musl-dev gcc cargo
|
||||||
|
|
||||||
@@ -78,52 +79,6 @@ While bcrypt remains an acceptable choice for password storage, depending on you
|
|||||||
Changelog
|
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
|
4.0.1
|
||||||
-----
|
-----
|
||||||
|
|
||||||
@@ -316,7 +271,12 @@ Compatibility
|
|||||||
-------------
|
-------------
|
||||||
|
|
||||||
This library should be compatible with py-bcrypt and it will run on Python
|
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
|
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
|
.. _`standard library`: https://docs.python.org/3/library/hashlib.html#hashlib.scrypt
|
||||||
.. _`argon2_cffi`: https://argon2-cffi.readthedocs.io
|
.. _`argon2_cffi`: https://argon2-cffi.readthedocs.io
|
||||||
.. _`cryptography`: https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#cryptography.hazmat.primitives.kdf.scrypt.Scrypt
|
.. _`cryptography`: https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#cryptography.hazmat.primitives.kdf.scrypt.Scrypt
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
41
venv/lib/python3.12/site-packages/bcrypt/__about__.py
Normal file
41
venv/lib/python3.12/site-packages/bcrypt/__about__.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Author:: Donald Stufft (<donald@stufft.io>)
|
||||||
|
# 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__)
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# Author:: Donald Stufft (<donald@stufft.io>)
|
||||||
|
# Copyright:: Copyright (c) 2013 Donald Stufft
|
||||||
|
# License:: Apache License, Version 2.0
|
||||||
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# You may obtain a copy of the License at
|
||||||
@@ -9,8 +13,14 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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__,
|
__author__,
|
||||||
__copyright__,
|
__copyright__,
|
||||||
__email__,
|
__email__,
|
||||||
@@ -18,26 +28,100 @@ from ._bcrypt import (
|
|||||||
__summary__,
|
__summary__,
|
||||||
__title__,
|
__title__,
|
||||||
__uri__,
|
__uri__,
|
||||||
checkpw,
|
__version__,
|
||||||
gensalt,
|
|
||||||
hashpw,
|
|
||||||
kdf,
|
|
||||||
)
|
|
||||||
from ._bcrypt import (
|
|
||||||
__version_ex__ as __version__,
|
|
||||||
)
|
)
|
||||||
|
from . import _bcrypt # noqa: I100
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"__author__",
|
|
||||||
"__copyright__",
|
|
||||||
"__email__",
|
|
||||||
"__license__",
|
|
||||||
"__summary__",
|
|
||||||
"__title__",
|
"__title__",
|
||||||
|
"__summary__",
|
||||||
"__uri__",
|
"__uri__",
|
||||||
"__version__",
|
"__version__",
|
||||||
"checkpw",
|
"__author__",
|
||||||
|
"__email__",
|
||||||
|
"__license__",
|
||||||
|
"__copyright__",
|
||||||
"gensalt",
|
"gensalt",
|
||||||
"hashpw",
|
"hashpw",
|
||||||
"kdf",
|
"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)
|
||||||
|
|||||||
@@ -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: ...
|
|
||||||
Binary file not shown.
7
venv/lib/python3.12/site-packages/bcrypt/_bcrypt.pyi
Normal file
7
venv/lib/python3.12/site-packages/bcrypt/_bcrypt.pyi
Normal file
@@ -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: ...
|
||||||
Reference in New Issue
Block a user