feat: add Docker support, refactor DB layer, update API responses
This commit is contained in:
+17
-8
@@ -1,9 +1,18 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
build/
|
||||
.dart_tool/
|
||||
.git/
|
||||
.github/
|
||||
.gitignore
|
||||
.env
|
||||
.env.*
|
||||
*.md
|
||||
!.gitignore
|
||||
.DS_Store
|
||||
.idea/
|
||||
.packages
|
||||
.vscode/
|
||||
*.iml
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.Dockerfile
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
*.log
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
FROM dart:3.10.1 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy only pubspec files first for better caching
|
||||
COPY pubspec.yaml pubspec.lock ./
|
||||
|
||||
# Install dependencies
|
||||
RUN dart pub get
|
||||
|
||||
# Copy all source files
|
||||
COPY . .
|
||||
|
||||
# Build the Dart application
|
||||
RUN dart pub get && dart compile exe bin/server.dart -o bin/server.exe
|
||||
|
||||
# Production stage
|
||||
FROM debian:bookworm-slim AS production
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the compiled executable from builder
|
||||
COPY --from=builder /app/bin/server.exe ./bin/server.exe
|
||||
|
||||
# Set environment variables
|
||||
ENV PORT=9090
|
||||
ENV POSTGRES_HOST=localhost
|
||||
ENV POSTGRES_PORT=5432
|
||||
ENV POSTGRES_DB=family_safety
|
||||
ENV POSTGRES_USER=postgres
|
||||
ENV POSTGRES_PASSWORD=postgres
|
||||
ENV JWT_SECRET=your-super-secret-key-change-me
|
||||
ENV TOKEN_LIFETIME=600
|
||||
|
||||
EXPOSE 9090
|
||||
|
||||
CMD ["./bin/server.exe"]
|
||||
@@ -1,18 +1,319 @@
|
||||
A server app built using [Shelf](https://pub.dev/packages/shelf),
|
||||
# Family Safety Tracker API
|
||||
|
||||
Это новый проект.
|
||||
Давай добавим описание проекта -
|
||||
Сервер - приложения для реализации возможности делиться геопозицией с другим человеком.
|
||||
База данных (в данном случае это будет postges) с отметками о геопозиции человека
|
||||
Какие таблицы будут в базе данных -
|
||||
Пользователи - ID, login, pwd
|
||||
Geoposition - id, x value, y value, datetime, lifetime
|
||||
Logs - username, action, datetime
|
||||
REST API для обмена геолокацией между членами семьи.
|
||||
|
||||
Основа приложения - это REST API.
|
||||
Вот какие методы нужны.
|
||||
/login - авторизация.
|
||||
/user - CRUD пользователей.
|
||||
/geo - POST - создание позиции, UPDATE - обновление позиции. (при создании указывается время жизни, после которого данные будут удалены из базы)
|
||||
/watch?{unique id} - возращает геопозицию+время последней отметки + оставшееся время жизни отметки
|
||||
/share - метод который создает одноразовую ссылку (/watch?{unique id} ) - по которой доступны данные о геопозиции. В памяти приложения создается связь между geo из таблицы и {unique id}
|
||||
## Аутентификация
|
||||
|
||||
Сервис использует JWT токены для авторизации.
|
||||
|
||||
### JWT Токен
|
||||
|
||||
При успешном `POST /login` возвращается JWT токен:
|
||||
|
||||
```json
|
||||
{
|
||||
"user": {...},
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
**Структура токена:**
|
||||
- `issuer: "family_safety_tracker"`
|
||||
- `payload: {"user_id": N, "login": "user"}`
|
||||
|
||||
### Авторизация
|
||||
|
||||
Защищённые эндпоинты (`/watch`, `/geo`, `/share`) требуют JWT в заголовке:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Таблица эндпоинтов
|
||||
|
||||
| Метод | URL | Описание | Защита |
|
||||
|-------|-----|----------|--------|
|
||||
| `POST` | `/login` | Вход в систему | — |
|
||||
| `POST` | `/reg` | Регистрация нового пользователя | — |
|
||||
| `GET` | `/watch?unique_id=...` | Получить последнюю позицию по share-ссылке | ✅ |
|
||||
| `PUT` | `/geo?id=N` | Обновить геопозицию | ✅ |
|
||||
| `POST` | `/share` | Создать share-ссылку | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
### `POST /login` — Вход
|
||||
|
||||
#### Запрос
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9090/reg \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"login": "ivan", "password": "secret123"}'
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
```powershell
|
||||
$body = @{ login = "ivan"; password = "secret123" } | ConvertTo-Json
|
||||
Invoke-RestMethod -Uri http://localhost:9090/reg -Method Post -Body $body -ContentType "application/json"
|
||||
```
|
||||
|
||||
#### Успешный ответ (200)
|
||||
|
||||
```json
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
```
|
||||
|
||||
#### Ошибки
|
||||
|
||||
| Код | Сообщение |
|
||||
|-----|-----------|
|
||||
| `401` | `Invalid credentials` — неверный логин или пароль |
|
||||
|
||||
---
|
||||
|
||||
### `POST /reg` — Регистрация
|
||||
|
||||
#### Запрос
|
||||
|
||||
```http
|
||||
Content-Type: application/json
|
||||
|
||||
{"login": "maria", "password": "newpass"}
|
||||
```
|
||||
|
||||
#### Успешный ответ (201)
|
||||
|
||||
Ничего не возвращается.
|
||||
|
||||
---
|
||||
|
||||
### `GET /watch?unique_id=...` — Получить позицию
|
||||
|
||||
#### Запрос
|
||||
|
||||
```http
|
||||
Authorization: Bearer <jwt_token>
|
||||
|
||||
GET /watch?unique_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
||||
```
|
||||
|
||||
#### Успешный ответ (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 42,
|
||||
"x": 55.7558,
|
||||
"y": 37.6173,
|
||||
"created_at": "2024-01-01T12:00:00Z",
|
||||
"expires_at": "2024-01-01T12:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Ошибки
|
||||
|
||||
| Код | Сообщение |
|
||||
|-----|-----------|
|
||||
| `404` | `Share link not found` — неверный `unique_id` |
|
||||
| `404` | `No position available` — share-ссылка создана, но позиция не была добавлена |
|
||||
|
||||
---
|
||||
|
||||
### `PUT /geo?id=N` — Обновить позицию
|
||||
|
||||
#### Запрос
|
||||
|
||||
```http
|
||||
Authorization: Bearer <jwt_token>
|
||||
Content-Type: application/json
|
||||
|
||||
PUT /geo?id=1
|
||||
|
||||
{
|
||||
"x": 55.7558,
|
||||
"y": 37.6173
|
||||
}
|
||||
```
|
||||
|
||||
#### Успешный ответ (200)
|
||||
|
||||
Ничего не возвращается.
|
||||
|
||||
---
|
||||
|
||||
### `POST /share` — Создать share-ссылку
|
||||
|
||||
#### Запрос
|
||||
|
||||
```http
|
||||
Authorization: Bearer <jwt_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"x": 55.7558,
|
||||
"y": 37.6173
|
||||
}
|
||||
```
|
||||
|
||||
#### Успешный ответ (201)
|
||||
|
||||
```json
|
||||
{
|
||||
"geo_id": 5,
|
||||
"share_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры запросов
|
||||
|
||||
### Регистрация
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/reg \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"login": "ivan", "password": "secret123"}'
|
||||
```
|
||||
|
||||
### Вход
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9090/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"login": "ivan", "password": "secret123"}'
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
```powershell
|
||||
$body = @{ login = "ivan"; password = "secret123" } | ConvertTo-Json
|
||||
Invoke-RestMethod -Uri http://localhost:9090/login -Method Post -Body $body -ContentType "application/json"
|
||||
```
|
||||
|
||||
### Создание share-ссылки
|
||||
|
||||
```bash
|
||||
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
|
||||
curl -X POST http://localhost:9090/share \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"x": 55.7558, "y": 37.6173}'
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
```powershell
|
||||
$token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
$body = @{ x = 55.7558; y = 37.6173 } | ConvertTo-Json
|
||||
Invoke-RestMethod -Uri http://localhost:9090/share -Method Post -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } -Body $body
|
||||
```
|
||||
|
||||
### Получение позиции
|
||||
|
||||
```bash
|
||||
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
SHARE_ID="a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
|
||||
curl "http://localhost:9090/watch?unique_id=$SHARE_ID" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
```powershell
|
||||
$token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
$shareId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||
Invoke-RestMethod -Uri "http://localhost:9090/watch?unique_id=$shareId" -Method Get -Headers @{ Authorization = "Bearer $token" }
|
||||
```
|
||||
|
||||
### Обновление позиции
|
||||
|
||||
```bash
|
||||
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
|
||||
curl -X PUT "http://localhost:9090/geo?id=1" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"x": 55.7558, "y": 37.6173}'
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
||||
```powershell
|
||||
$token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
$body = @{ x = 55.7558; y = 37.6173 } | ConvertTo-Json
|
||||
Invoke-RestMethod -Uri "http://localhost:9090/geo?id=1" -Method Put -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } -Body $body
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Сервер
|
||||
- Порт: `9090` (изменяется через `PORT`)
|
||||
|
||||
### Технологии
|
||||
- **Dart** (SDK `^3.10.1`)
|
||||
- **Shelf** + **Shelf Router** — HTTP фреймворк
|
||||
- **PostgreSQL** — база данных
|
||||
- **bcrypt** — хэширование паролей
|
||||
- **dart_jsonwebtoken** — JWT-токены
|
||||
- **dotenv** — переменные окружения
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
| Переменная | Описание |
|
||||
|------------|----------|
|
||||
| `PORT` | Порт сервера (по умолчанию 8080) |
|
||||
| `POSTGRES_HOST` | Хост PostgreSQL |
|
||||
| `POSTGRES_DB` | База данных |
|
||||
| `POSTGRES_USER` | Пользователь |
|
||||
| `POSTGRES_PASSWORD` | Пароль |
|
||||
| `JWT_SECRET` | Секрет для JWT-токенов |
|
||||
|
||||
### Автоматическая миграция
|
||||
|
||||
При запуске сервера таблицы создаются автоматически:
|
||||
- `users` — пользователи
|
||||
- `geopositions` — геопозиции
|
||||
- `logs` — логи
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
bin/
|
||||
├── server.dart # Точка входа
|
||||
├── routes/
|
||||
│ ├── auth_routes.dart # Маршруты авторизации
|
||||
│ └── geo_routes.dart # Маршруты геолокации
|
||||
├── repositories/
|
||||
│ ├── user_repo.dart
|
||||
│ └── geoposition_repo.dart
|
||||
├── models/
|
||||
│ ├── user.dart
|
||||
│ ├── geoposition.dart
|
||||
│ └── log.dart
|
||||
└── database/
|
||||
└── database_provider.dart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
dart pub get
|
||||
dart run bin/server.dart
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT
|
||||
|
||||
+7
-3
@@ -1,9 +1,13 @@
|
||||
# Environment variables for sky_kfm_backend
|
||||
# Environment variables for family_safety_tracker
|
||||
# JWT secret used for signing tokens
|
||||
JWT_SECRET=your-super-secret-key-change-me
|
||||
# Secret pepper added to passwords before hashing (should be random and kept secret)
|
||||
PASSWORD_PEPPER=your-random-pepper-string-change-me
|
||||
# Database connection URL
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/family_safety
|
||||
# Database connection settings
|
||||
POSTGRES_HOST="db"
|
||||
POSTGRES_PORT="5432"
|
||||
POSTGRES_DB="family_safety"
|
||||
POSTGRES_USER="postgres"
|
||||
POSTGRES_PASSWORD="postgres"
|
||||
# TOKEN_LIFETIME in minutes
|
||||
TOKEN_LIFETIME=600
|
||||
@@ -18,11 +18,11 @@ class DatabaseProvider {
|
||||
Future<void> initialize() async {
|
||||
final dotenv = DotEnv();
|
||||
|
||||
final host = dotenv['POSTGRES_HOST'] ?? 'localhost';
|
||||
final host = dotenv['POSTGRES_HOST'] ?? 'db';
|
||||
final port = int.parse(dotenv['POSTGRES_PORT'] ?? '5432');
|
||||
final databaseName = dotenv['POSTGRES_DB'] ?? 'family_safety';
|
||||
final username = dotenv['POSTGRES_USER'] ?? 'postgres';
|
||||
final password = dotenv['POSTGRES_PASSWORD'] ?? '';
|
||||
final password = dotenv['POSTGRES_PASSWORD'] ?? 'postgres';
|
||||
|
||||
try {
|
||||
final defaultConnection = await Connection.open(
|
||||
@@ -43,8 +43,7 @@ class DatabaseProvider {
|
||||
|
||||
if (results.isEmpty) {
|
||||
await defaultConnection.execute(
|
||||
Sql.named('CREATE DATABASE @dbName'),
|
||||
parameters: {'dbName': databaseName},
|
||||
Sql('CREATE DATABASE $databaseName'),
|
||||
);
|
||||
print('Database $databaseName created.');
|
||||
} else {
|
||||
@@ -78,7 +77,7 @@ class DatabaseProvider {
|
||||
await _dbConnection.execute(
|
||||
Sql.named('''
|
||||
CREATE TABLE IF NOT EXISTS geopositions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
x_value DOUBLE PRECISION NOT NULL,
|
||||
y_value DOUBLE PRECISION NOT NULL,
|
||||
last_update TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
@@ -265,7 +264,7 @@ class DatabaseProvider {
|
||||
Future<Geoposition?> getLatestPosition() async {
|
||||
final results = await _dbConnection.execute(
|
||||
Sql.named('''
|
||||
SELECT id, x_value, y_value, last_update, expires_at
|
||||
SELECT x_value, y_value, last_update, expires_at
|
||||
FROM geopositions
|
||||
WHERE expires_at > NOW()
|
||||
ORDER BY last_update DESC
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
import 'package:dotenv/dotenv.dart';
|
||||
|
||||
class AuthMiddleware {
|
||||
final Handler handler;
|
||||
final Future<Response> Function(Request, String) handler;
|
||||
|
||||
AuthMiddleware(this.handler);
|
||||
|
||||
@@ -19,11 +19,15 @@ class AuthMiddleware {
|
||||
try {
|
||||
final dotenv = DotEnv();
|
||||
final secret = dotenv['JWT_SECRET'] ?? '';
|
||||
final decoded = JWT.verify(token, SecretKey(secret));
|
||||
final jwt = JWT.verify(token, SecretKey(secret));
|
||||
final payload = jwt.payload;
|
||||
final login = payload['login'] as String;
|
||||
|
||||
return handler(request);
|
||||
} catch (e) {
|
||||
return Response(401, body: 'Invalid or expired token');
|
||||
return handler(request, login);
|
||||
} on JWTExpiredException {
|
||||
return Response(401, body: 'Token expired');
|
||||
} on JWTException {
|
||||
return Response(401, body: 'Invalid token');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,7 @@ class AuthRoutes {
|
||||
final token = jwt.sign(SecretKey(secret));
|
||||
|
||||
await database.createLog(login, 'Successful login');
|
||||
return Response(200, body: jsonEncode({
|
||||
'user': user.toMap(),
|
||||
'token': token
|
||||
}));
|
||||
return Response(200, body: jsonEncode(token));
|
||||
}
|
||||
|
||||
Future<Response> _register(Request request) async {
|
||||
@@ -57,9 +54,9 @@ class AuthRoutes {
|
||||
final login = data['login'];
|
||||
final password = data['password'];
|
||||
|
||||
final user = await database.createUser(login, password);
|
||||
await database.createUser(login, password);
|
||||
await database.createLog(login, 'User registration');
|
||||
return Response(201, body: jsonEncode(user.toMap()));
|
||||
return Response(201);
|
||||
}
|
||||
|
||||
Future<Response> _watch(Request request, String login) async {
|
||||
@@ -78,6 +75,11 @@ class AuthRoutes {
|
||||
}
|
||||
|
||||
await database.createLog(login, 'Accessed share link');
|
||||
return Response(200, body: jsonEncode(position.toJson()));
|
||||
return Response(200, body: jsonEncode({
|
||||
'x': position.xValue,
|
||||
'y': position.yValue,
|
||||
'last_update': position.lastUpdate,
|
||||
'expires_at': position.expiresAt,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class GeoRoutes {
|
||||
return router;
|
||||
}
|
||||
|
||||
Future<Response> _updatePosition(Request request) async {
|
||||
Future<Response> _updatePosition(Request request, String login) async {
|
||||
final id = int.parse(request.url.queryParameters['id']!);
|
||||
final body = await request.readAsString();
|
||||
final data = jsonDecode(body);
|
||||
@@ -24,11 +24,12 @@ class GeoRoutes {
|
||||
final x = data['x'];
|
||||
final y = data['y'];
|
||||
|
||||
final position = await database.updatePosition(id, x, y);
|
||||
return Response(200, body: position.toJson());
|
||||
await database.updatePosition(id, x, y);
|
||||
await database.createLog(login, 'Updated position id=$id');
|
||||
return Response(200);
|
||||
}
|
||||
|
||||
Future<Response> _createShare(Request request) async {
|
||||
Future<Response> _createShare(Request request, String login) async {
|
||||
final body = await request.readAsString();
|
||||
final data = jsonDecode(body);
|
||||
|
||||
@@ -38,6 +39,7 @@ class GeoRoutes {
|
||||
final position = await database.createPosition(x, y);
|
||||
final shareId = database.createShareId();
|
||||
|
||||
await database.createLog(login, 'Created share link geo_id=${position.id}');
|
||||
return Response(201, body: jsonEncode({
|
||||
'geo_id': position.id,
|
||||
'share_id': shareId
|
||||
|
||||
+3
-3
@@ -21,8 +21,8 @@ void main(List<String> args) async {
|
||||
final geoRoutes = GeoRoutes(database);
|
||||
|
||||
final router = Router()
|
||||
..mount('', authRoutes.routes.call)
|
||||
..mount('', geoRoutes.routes.call)
|
||||
..mount('/', authRoutes.routes.call)
|
||||
..mount('/', geoRoutes.routes.call)
|
||||
..get('/', (Request req) => Response.ok('Family Safety Tracker API\n'));
|
||||
|
||||
final handler = Pipeline()
|
||||
@@ -30,7 +30,7 @@ void main(List<String> args) async {
|
||||
.addHandler(router.call);
|
||||
|
||||
final ip = InternetAddress.anyIPv4;
|
||||
final port = int.parse(Platform.environment['PORT'] ?? '8080');
|
||||
final port = int.parse(Platform.environment['PORT'] ?? '9090');
|
||||
final server = await serve(handler, ip, port);
|
||||
print('Server listening on port ${server.port}');
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: family_safety_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: family_safety
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
app:
|
||||
build: .
|
||||
container_name: family_safety_app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:9090"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
+2
-2
@@ -11,8 +11,8 @@
|
||||
"qwen3-coder:a3b": {
|
||||
"name": "Qwen_Qwen3.5-9B-Q6_K (local)",
|
||||
"limit": {
|
||||
"context": 42000,
|
||||
"output": 42000
|
||||
"context": 248000,
|
||||
"output": 65655
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+117
-20
@@ -1,39 +1,136 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
final port = '8080';
|
||||
final host = 'http://0.0.0.0:$port';
|
||||
final port = '9090';
|
||||
final host = 'http://localhost:$port';
|
||||
late Process p;
|
||||
String? authToken;
|
||||
|
||||
setUp(() async {
|
||||
Future<String?> getAuthToken() async {
|
||||
if (authToken != null) return authToken;
|
||||
|
||||
final regResponse = await http.post(
|
||||
Uri.parse('$host/reg'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'login': 'testuser', 'password': 'testpass'}),
|
||||
);
|
||||
|
||||
final loginResponse = await http.post(
|
||||
Uri.parse('$host/login'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'login': 'testuser', 'password': 'testpass'}),
|
||||
);
|
||||
|
||||
if (loginResponse.statusCode == 200) {
|
||||
final data = jsonDecode(loginResponse.body) as Map<String, dynamic>;
|
||||
authToken = data['token'];
|
||||
}
|
||||
return authToken;
|
||||
}
|
||||
|
||||
setUpAll(() async {
|
||||
stdout.writeln("Starting server...");
|
||||
p = await Process.start(
|
||||
'dart',
|
||||
['run', 'bin/server.dart'],
|
||||
environment: {'PORT': port},
|
||||
);
|
||||
// Wait for server to start and print to stdout.
|
||||
await p.stdout.first;
|
||||
|
||||
// Wait for server to be ready
|
||||
for (int i = 0; i < 30; i++) {
|
||||
try {
|
||||
final response = await http.get(Uri.parse('$host/'));
|
||||
if (response.statusCode == 200) break;
|
||||
} catch (e) {
|
||||
// Server not ready yet
|
||||
}
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
stdout.writeln("Server ready");
|
||||
});
|
||||
|
||||
tearDown(() => p.kill());
|
||||
|
||||
test('Root', () async {
|
||||
final response = await get(Uri.parse('$host/'));
|
||||
expect(response.statusCode, 200);
|
||||
expect(response.body, 'Hello, World!\n');
|
||||
tearDownAll(() {
|
||||
p.kill();
|
||||
});
|
||||
|
||||
test('Echo', () async {
|
||||
final response = await get(Uri.parse('$host/echo/hello'));
|
||||
expect(response.statusCode, 200);
|
||||
expect(response.body, 'hello\n');
|
||||
group('Root endpoint', () {
|
||||
test('GET / - Root endpoint', () async {
|
||||
final response = await http.get(Uri.parse('$host/'));
|
||||
expect(response.statusCode, 200);
|
||||
expect(response.body, contains('Family Safety Tracker API'));
|
||||
});
|
||||
|
||||
test('GET /nonexistent - 404', () async {
|
||||
final response = await http.get(Uri.parse('$host/nonexistent'));
|
||||
expect(response.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
test('404', () async {
|
||||
final response = await get(Uri.parse('$host/foobar'));
|
||||
expect(response.statusCode, 404);
|
||||
group('User registration', () {
|
||||
test('POST /reg - Register new user', () async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$host/reg'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'login': 'newuser', 'password': 'newpass'}),
|
||||
);
|
||||
expect(response.statusCode, 201);
|
||||
final data = jsonDecode(response.body);
|
||||
expect(data['login'], 'newuser');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group('Authentication', () {
|
||||
test('POST /login - Valid credentials', () async {
|
||||
await getAuthToken();
|
||||
expect(authToken, isNotEmpty);
|
||||
});
|
||||
|
||||
test('POST /login - Invalid credentials', () async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$host/login'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'login': 'testuser', 'password': 'wrongpass'}),
|
||||
);
|
||||
expect(response.statusCode, 401);
|
||||
});
|
||||
});
|
||||
|
||||
group('Geo operations', () {
|
||||
test('POST /share - Create share link', () async {
|
||||
await getAuthToken();
|
||||
final response = await http.post(
|
||||
Uri.parse('$host/share'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer $authToken',
|
||||
},
|
||||
body: jsonEncode({'x': 40.7128, 'y': -74.006}),
|
||||
);
|
||||
expect(response.statusCode, 201);
|
||||
final data = jsonDecode(response.body);
|
||||
expect(data['share_id'], isA<String>());
|
||||
});
|
||||
|
||||
test('POST /share - No auth token', () async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$host/share'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'x': 40.7128, 'y': -74.006}),
|
||||
);
|
||||
expect(response.statusCode, 401);
|
||||
});
|
||||
});
|
||||
|
||||
group('Watch endpoint', () {
|
||||
test('GET /watch - Invalid share link', () async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$host/watch?unique_id=invalid-uuid'),
|
||||
);
|
||||
expect(response.statusCode, 404);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user