feat: add Swagger UI docs, custom CORS middleware, and request logging
- Add Swagger UI files and updated API spec for Family Safety Tracker - Replace shelf_cors_headers with custom CORS middleware in server.dart - Add request_logger middleware with timing for auth and geo routes - Add REGISTRATION_SECRET_KEY to .env for registration validation - Remove postgres port exposure from docker-compose.yml - Update opencode.json model configuration - Add crypto dependency and update Flutter web assets
This commit is contained in:
@@ -0,0 +1,346 @@
|
|||||||
|
openapi: "3.0.3"
|
||||||
|
info:
|
||||||
|
title: Family Safety Tracker API
|
||||||
|
description: REST API for sharing geolocation with family members. Authentication via JWT tokens.
|
||||||
|
version: 1.0.0
|
||||||
|
contact:
|
||||||
|
name: Family Safety Tracker
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:9090
|
||||||
|
description: Local development server
|
||||||
|
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/login:
|
||||||
|
post:
|
||||||
|
summary: Authenticate user
|
||||||
|
description: Login with login and password. Returns a JWT token on success.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/LoginRequest"
|
||||||
|
example:
|
||||||
|
login: "john_doe"
|
||||||
|
password: "secret123"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Authentication successful
|
||||||
|
headers:
|
||||||
|
Access-Control-Allow-Origin:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/LoginResponse"
|
||||||
|
example:
|
||||||
|
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
"401":
|
||||||
|
description: Invalid credentials
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
example:
|
||||||
|
error: "Invalid credentials"
|
||||||
|
|
||||||
|
/reg:
|
||||||
|
post:
|
||||||
|
summary: Register a new user
|
||||||
|
description: Create a new user account. Password is hashed with bcrypt.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RegisterRequest"
|
||||||
|
example:
|
||||||
|
login: "john_doe"
|
||||||
|
password: "secret123"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: User registered successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RegisterResponse"
|
||||||
|
example:
|
||||||
|
message: "User registered"
|
||||||
|
"400":
|
||||||
|
description: Bad request (e.g. login already exists)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
|
||||||
|
/geo:
|
||||||
|
put:
|
||||||
|
summary: Update geolocation position
|
||||||
|
description: Update the coordinates of an existing geoposition record.
|
||||||
|
tags:
|
||||||
|
- Geolocation
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
description: UUID of the geoposition to update
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PositionRequest"
|
||||||
|
example:
|
||||||
|
x: 55.7558
|
||||||
|
y: 37.6173
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Position updated successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/MessageResponse"
|
||||||
|
example:
|
||||||
|
message: "Position updated"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized - invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
"404":
|
||||||
|
description: Geoposition not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
|
||||||
|
/share:
|
||||||
|
post:
|
||||||
|
summary: Create a share link
|
||||||
|
description: Create a new geoposition and generate a unique share link UUID for public viewing.
|
||||||
|
tags:
|
||||||
|
- Sharing
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PositionRequest"
|
||||||
|
example:
|
||||||
|
x: 55.7558
|
||||||
|
y: 37.6173
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Share link created successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ShareResponse"
|
||||||
|
example:
|
||||||
|
geo_id: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
share_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized - invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
|
||||||
|
/watch:
|
||||||
|
get:
|
||||||
|
summary: Get latest position via share link
|
||||||
|
description: Retrieve the latest position for a given share link UUID. Share links are stored in-memory.
|
||||||
|
tags:
|
||||||
|
- Sharing
|
||||||
|
parameters:
|
||||||
|
- name: share_id
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
description: UUID of the share link
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Position retrieved successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PositionResponse"
|
||||||
|
example:
|
||||||
|
x: 55.7558
|
||||||
|
y: 37.6173
|
||||||
|
last_update: "2026-05-15T10:30:00.000Z"
|
||||||
|
expires_at: "2026-05-15T11:30:00.000Z"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized - invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
"404":
|
||||||
|
description: Share link not found or no position available
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
examples:
|
||||||
|
linkNotFound:
|
||||||
|
summary: Share link not found
|
||||||
|
value:
|
||||||
|
error: "Share link not found"
|
||||||
|
noPosition:
|
||||||
|
summary: No position available
|
||||||
|
value:
|
||||||
|
error: "No position available"
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
BearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
description: JWT token obtained from /login. Payload contains user_id, login, and iss="family_safety_tracker".
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
LoginRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- login
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
login:
|
||||||
|
type: string
|
||||||
|
description: User login / username
|
||||||
|
example: "john_doe"
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
format: password
|
||||||
|
description: User password (verified against bcrypt hash)
|
||||||
|
example: "secret123"
|
||||||
|
|
||||||
|
RegisterRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- login
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
login:
|
||||||
|
type: string
|
||||||
|
description: Desired login / username
|
||||||
|
example: "john_doe"
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
format: password
|
||||||
|
description: Desired password (will be hashed with bcrypt)
|
||||||
|
example: "secret123"
|
||||||
|
|
||||||
|
LoginResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: JWT token for authenticated requests
|
||||||
|
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
|
||||||
|
RegisterResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: "User registered"
|
||||||
|
|
||||||
|
PositionRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- x
|
||||||
|
- y
|
||||||
|
properties:
|
||||||
|
x:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: Longitude coordinate
|
||||||
|
example: 55.7558
|
||||||
|
y:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: Latitude coordinate
|
||||||
|
example: 37.6173
|
||||||
|
|
||||||
|
PositionResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
x:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: Longitude coordinate
|
||||||
|
example: 55.7558
|
||||||
|
y:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: Latitude coordinate
|
||||||
|
example: 37.6173
|
||||||
|
last_update:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Timestamp of the last position update (ISO 8601)
|
||||||
|
example: "2026-05-15T10:30:00.000Z"
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Expiration timestamp for the position (ISO 8601)
|
||||||
|
example: "2026-05-15T11:30:00.000Z"
|
||||||
|
|
||||||
|
ShareResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
geo_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: UUID of the created geoposition record in the database
|
||||||
|
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
share_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: UUID of the share link for public viewing
|
||||||
|
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
|
|
||||||
|
MessageResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: "Position updated"
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: Error description
|
||||||
|
example: "Invalid credentials"
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Authentication
|
||||||
|
description: User registration and login
|
||||||
|
- name: Geolocation
|
||||||
|
description: Create and update geolocation positions
|
||||||
|
- name: Sharing
|
||||||
|
description: Share links for public position viewing
|
||||||
@@ -4,10 +4,12 @@ JWT_SECRET=your-super-secret-key-change-me
|
|||||||
# Secret pepper added to passwords before hashing (should be random and kept secret)
|
# Secret pepper added to passwords before hashing (should be random and kept secret)
|
||||||
PASSWORD_PEPPER=your-random-pepper-string-change-me
|
PASSWORD_PEPPER=your-random-pepper-string-change-me
|
||||||
# Database connection settings
|
# Database connection settings
|
||||||
POSTGRES_HOST="db"
|
POSTGRES_HOST="localhost"
|
||||||
POSTGRES_PORT="5432"
|
POSTGRES_PORT="5432"
|
||||||
POSTGRES_DB="family_safety"
|
POSTGRES_DB="family_safety"
|
||||||
POSTGRES_USER="postgres"
|
POSTGRES_USER="postgres"
|
||||||
POSTGRES_PASSWORD="postgres"
|
POSTGRES_PASSWORD="postgres"
|
||||||
# TOKEN_LIFETIME in minutes
|
# TOKEN_LIFETIME in minutes
|
||||||
TOKEN_LIFETIME=600
|
TOKEN_LIFETIME=600
|
||||||
|
# Secret key for registration (MD5 hash of this key must be sent by the user)
|
||||||
|
REGISTRATION_SECRET_KEY=FtracKer*1405.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
void logRequest({
|
||||||
|
required String method,
|
||||||
|
required String url,
|
||||||
|
required int status,
|
||||||
|
required Duration duration,
|
||||||
|
DateTime? startTime,
|
||||||
|
String? body,
|
||||||
|
Map<String, String>? responseHeaders,
|
||||||
|
}) {
|
||||||
|
final start = startTime ?? DateTime.now();
|
||||||
|
final timestamp = start.toIso8601String();
|
||||||
|
final durationMs = duration.inMilliseconds;
|
||||||
|
final bodyPreview = body != null && body.length > 500 ? '${body.substring(0, 500)}...' : body;
|
||||||
|
|
||||||
|
print('[$timestamp] $method $url -> $status (${durationMs}ms)');
|
||||||
|
if (bodyPreview != null && bodyPreview.isNotEmpty) {
|
||||||
|
print(' Body: $bodyPreview');
|
||||||
|
}
|
||||||
|
if (responseHeaders != null) {
|
||||||
|
final contentType = responseHeaders['content-type'];
|
||||||
|
if (contentType != null) {
|
||||||
|
print(' Content-Type: $contentType');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print('');
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
|||||||
import 'package:dotenv/dotenv.dart';
|
import 'package:dotenv/dotenv.dart';
|
||||||
import '../database/database_provider.dart';
|
import '../database/database_provider.dart';
|
||||||
import '../middleware/auth_middleware.dart';
|
import '../middleware/auth_middleware.dart';
|
||||||
|
import '../middleware/request_logger.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
class AuthRoutes {
|
class AuthRoutes {
|
||||||
@@ -29,9 +30,13 @@ class AuthRoutes {
|
|||||||
|
|
||||||
final user = await database.findUserByLogin(login);
|
final user = await database.findUserByLogin(login);
|
||||||
|
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
if (user == null || !BCrypt.checkpw(password, user.pwdHash)) {
|
if (user == null || !BCrypt.checkpw(password, user.pwdHash)) {
|
||||||
await database.createLog(login, 'Failed login attempt');
|
await database.createLog(login, 'Failed login attempt');
|
||||||
return Response(401, body: jsonEncode({'error': 'Invalid credentials'}), headers: {'Content-Type': 'application/json'});
|
stopwatch.stop();
|
||||||
|
final response = Response(401, body: jsonEncode({'error': 'Invalid credentials'}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'POST', url: '/login', status: 401, duration: stopwatch.elapsed, body: body, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерация JWT токена
|
// Генерация JWT токена
|
||||||
@@ -44,10 +49,14 @@ class AuthRoutes {
|
|||||||
final token = jwt.sign(SecretKey(secret));
|
final token = jwt.sign(SecretKey(secret));
|
||||||
|
|
||||||
await database.createLog(login, 'Successful login');
|
await database.createLog(login, 'Successful login');
|
||||||
return Response(200, body: jsonEncode({'token': token}), headers: {'Content-Type': 'application/json'});
|
stopwatch.stop();
|
||||||
|
final response = Response(200, body: jsonEncode({'token': token}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'POST', url: '/login', status: 200, duration: stopwatch.elapsed, body: body, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> _register(Request request) async {
|
Future<Response> _register(Request request) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
final body = await request.readAsString();
|
final body = await request.readAsString();
|
||||||
final data = jsonDecode(body);
|
final data = jsonDecode(body);
|
||||||
|
|
||||||
@@ -56,35 +65,51 @@ class AuthRoutes {
|
|||||||
|
|
||||||
await database.createUser(login, password);
|
await database.createUser(login, password);
|
||||||
await database.createLog(login, 'User registration');
|
await database.createLog(login, 'User registration');
|
||||||
return Response(201, body: jsonEncode({'message': 'User registered'}), headers: {'Content-Type': 'application/json'});
|
stopwatch.stop();
|
||||||
|
final response = Response(201, body: jsonEncode({'message': 'User registered'}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'POST', url: '/reg', status: 201, duration: stopwatch.elapsed, body: body, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> _watch(Request request, String login) async {
|
Future<Response> _watch(Request request, String login) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
final uniqueId = request.url.queryParameters['share_id'];
|
final uniqueId = request.url.queryParameters['share_id'];
|
||||||
if (uniqueId == null) {
|
if (uniqueId == null) {
|
||||||
return Response(404, body: jsonEncode({'error': 'Share link not found'}), headers: {'Content-Type': 'application/json'});
|
stopwatch.stop();
|
||||||
|
final response = Response(404, body: jsonEncode({'error': 'Share link not found'}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'GET', url: '/watch', status: 404, duration: stopwatch.elapsed, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
final geoId = database.getGeoIdByShareId(uniqueId);
|
final geoId = database.getGeoIdByShareId(uniqueId);
|
||||||
|
|
||||||
if (geoId == null) {
|
if (geoId == null) {
|
||||||
await database.createLog(login, 'Accessed invalid share link');
|
await database.createLog(login, 'Accessed invalid share link');
|
||||||
return Response(404, body: jsonEncode({'error': 'Share link not found'}), headers: {'Content-Type': 'application/json'});
|
stopwatch.stop();
|
||||||
|
final response = Response(404, body: jsonEncode({'error': 'Share link not found'}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'GET', url: '/watch', status: 404, duration: stopwatch.elapsed, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
final position = await database.getPositionById(geoId);
|
final position = await database.getPositionById(geoId);
|
||||||
|
|
||||||
if (position == null) {
|
if (position == null) {
|
||||||
await database.createLog(login, 'Accessed share link - no position');
|
await database.createLog(login, 'Accessed share link - no position');
|
||||||
return Response(404, body: jsonEncode({'error': 'No position available'}), headers: {'Content-Type': 'application/json'});
|
stopwatch.stop();
|
||||||
|
final response = Response(404, body: jsonEncode({'error': 'No position available'}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'GET', url: '/watch', status: 404, duration: stopwatch.elapsed, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
await database.createLog(login, 'Accessed share link');
|
await database.createLog(login, 'Accessed share link');
|
||||||
return Response(200, body: jsonEncode({
|
stopwatch.stop();
|
||||||
|
final response = Response(200, body: jsonEncode({
|
||||||
'x': position.xValue,
|
'x': position.xValue,
|
||||||
'y': position.yValue,
|
'y': position.yValue,
|
||||||
'last_update': position.lastUpdate.toIso8601String(),
|
'last_update': position.lastUpdate.toIso8601String(),
|
||||||
'expires_at': position.expiresAt.toIso8601String(),
|
'expires_at': position.expiresAt.toIso8601String(),
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'GET', url: '/watch', status: 200, duration: stopwatch.elapsed, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:shelf/shelf.dart';
|
|||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
import '../database/database_provider.dart';
|
import '../database/database_provider.dart';
|
||||||
import '../middleware/auth_middleware.dart';
|
import '../middleware/auth_middleware.dart';
|
||||||
|
import '../middleware/request_logger.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
class GeoRoutes {
|
class GeoRoutes {
|
||||||
@@ -17,6 +18,7 @@ class GeoRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> _updatePosition(Request request, String login) async {
|
Future<Response> _updatePosition(Request request, String login) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
final id = request.url.queryParameters['id']!;
|
final id = request.url.queryParameters['id']!;
|
||||||
final body = await request.readAsString();
|
final body = await request.readAsString();
|
||||||
final data = jsonDecode(body);
|
final data = jsonDecode(body);
|
||||||
@@ -26,10 +28,14 @@ class GeoRoutes {
|
|||||||
|
|
||||||
await database.updatePosition(id, x, y);
|
await database.updatePosition(id, x, y);
|
||||||
await database.createLog(login, 'Updated position id=$id');
|
await database.createLog(login, 'Updated position id=$id');
|
||||||
return Response(200, body: jsonEncode({'message': 'Position updated'}), headers: {'Content-Type': 'application/json'});
|
stopwatch.stop();
|
||||||
|
final response = Response(200, body: jsonEncode({'message': 'Position updated'}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'PUT', url: '/geo', status: 200, duration: stopwatch.elapsed, body: body, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> _createShare(Request request, String login) async {
|
Future<Response> _createShare(Request request, String login) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
final body = await request.readAsString();
|
final body = await request.readAsString();
|
||||||
final data = jsonDecode(body);
|
final data = jsonDecode(body);
|
||||||
|
|
||||||
@@ -40,9 +46,12 @@ class GeoRoutes {
|
|||||||
final shareId = database.createShareId(position.id);
|
final shareId = database.createShareId(position.id);
|
||||||
|
|
||||||
await database.createLog(login, 'Created share link geo_id=${position.id}');
|
await database.createLog(login, 'Created share link geo_id=${position.id}');
|
||||||
return Response(201, body: jsonEncode({
|
stopwatch.stop();
|
||||||
|
final response = Response(201, body: jsonEncode({
|
||||||
'geo_id': position.id,
|
'geo_id': position.id,
|
||||||
'share_id': shareId
|
'share_id': shareId
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'});
|
||||||
|
logRequest(method: 'POST', url: '/share', status: 201, duration: stopwatch.elapsed, body: body, responseHeaders: response.headers);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+21
-3
@@ -4,13 +4,29 @@ import 'dart:async';
|
|||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf/shelf_io.dart';
|
import 'package:shelf/shelf_io.dart';
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
import 'package:shelf_cors_headers/shelf_cors_headers.dart';
|
|
||||||
import 'package:shelf_static/shelf_static.dart';
|
import 'package:shelf_static/shelf_static.dart';
|
||||||
|
|
||||||
import 'database/database_provider.dart';
|
import 'database/database_provider.dart';
|
||||||
import 'routes/auth_routes.dart';
|
import 'routes/auth_routes.dart';
|
||||||
import 'routes/geo_routes.dart';
|
import 'routes/geo_routes.dart';
|
||||||
|
|
||||||
|
Middleware _corsMiddleware = (Handler innerHandler) {
|
||||||
|
return (Request request) {
|
||||||
|
final headers = <String, String>{
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Access-Control-Max-Age': '86400',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.method == 'OPTIONS') {
|
||||||
|
return Response.ok(null, headers: headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Future.value(innerHandler(request)).then((response) => response.change(headers: headers));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
void main(List<String> args) async {
|
void main(List<String> args) async {
|
||||||
final database = DatabaseProvider();
|
final database = DatabaseProvider();
|
||||||
await database.initialize();
|
await database.initialize();
|
||||||
@@ -22,16 +38,18 @@ void main(List<String> args) async {
|
|||||||
final authRoutes = AuthRoutes(database);
|
final authRoutes = AuthRoutes(database);
|
||||||
final geoRoutes = GeoRoutes(database);
|
final geoRoutes = GeoRoutes(database);
|
||||||
|
|
||||||
final router = Router()
|
final router = Router()
|
||||||
..mount('/', authRoutes.routes.call)
|
..mount('/', authRoutes.routes.call)
|
||||||
..mount('/', geoRoutes.routes.call);
|
..mount('/', geoRoutes.routes.call);
|
||||||
|
|
||||||
final staticHandler = createStaticHandler('web', defaultDocument: 'index.html');
|
final staticHandler = createStaticHandler('web', defaultDocument: 'index.html');
|
||||||
|
final apiHandler = createStaticHandler('web/swagger', defaultDocument: 'index.html');
|
||||||
|
|
||||||
final handler = Pipeline()
|
final handler = Pipeline()
|
||||||
.addMiddleware(corsHeaders())
|
.addMiddleware(_corsMiddleware)
|
||||||
.addMiddleware(logRequests())
|
.addMiddleware(logRequests())
|
||||||
.addHandler(Cascade()
|
.addHandler(Cascade()
|
||||||
|
.add(apiHandler)
|
||||||
.add(staticHandler)
|
.add(staticHandler)
|
||||||
.add(router.call)
|
.add(router.call)
|
||||||
.handler);
|
.handler);
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ services:
|
|||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: family_safety
|
POSTGRES_DB: family_safety
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
+124
-13
@@ -1,28 +1,139 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"server": {
|
||||||
|
"port": 4096,
|
||||||
|
"hostname": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"compaction": {
|
||||||
|
"auto": false
|
||||||
|
},
|
||||||
|
"autoupdate": false,
|
||||||
|
"model": "llama_swap/Qwen3.6-35B-A3B-Q8_0_img",
|
||||||
|
"small_model": "llama_swap/cpu_gemma4-E2B-Q4_K_M",
|
||||||
"provider": {
|
"provider": {
|
||||||
"llama.cpp": {
|
"llama_swap": {
|
||||||
|
"name": "llama_swap",
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
"name": "llama-server (local)",
|
|
||||||
"options": {
|
|
||||||
"baseURL": "http://127.0.0.1:9988/v1"
|
|
||||||
},
|
|
||||||
"models": {
|
"models": {
|
||||||
"qwen3-coder:a3b": {
|
"Qwen3.6-35B-A3B-Q8_0_img": {
|
||||||
"name": "Qwen_Qwen3.5-9B-Q6_K (local)",
|
"name": "Qwen3.6-35B-A3B-Q8_0_img",
|
||||||
|
"tool_call": true,
|
||||||
|
"reasoning": true,
|
||||||
"limit": {
|
"limit": {
|
||||||
"context": 248000,
|
"context": 196608,
|
||||||
"output": 65655
|
"output": 83968
|
||||||
|
},
|
||||||
|
"modalities": {
|
||||||
|
"input": ["text", "image", "pdf"],
|
||||||
|
"output": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Qwen3.6-35B-A3B-Q8_0_fullctx": {
|
||||||
|
"name": "Qwen3.6-35B-A3B-Q8_0_fullctx",
|
||||||
|
"tool_call": true,
|
||||||
|
"reasoning": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 262144,
|
||||||
|
"output": 83968
|
||||||
|
},
|
||||||
|
"modalities": {
|
||||||
|
"input": ["text"],
|
||||||
|
"output": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"google_gemma-4-E4B-it-Q4_K_M": {
|
||||||
|
"name": "google_gemma-4-E4B-it-Q4_K_M",
|
||||||
|
"tool_call": true,
|
||||||
|
"reasoning": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 262144,
|
||||||
|
"output": 65536
|
||||||
|
},
|
||||||
|
"modalities": {
|
||||||
|
"input": ["text", "image", "pdf", "audio", "video"],
|
||||||
|
"output": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"GRM-2.6-Plus-Q8_0": {
|
||||||
|
"name": "GRM-2.6-Plus-Q8_0",
|
||||||
|
"tool_call": true,
|
||||||
|
"reasoning": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 196608,
|
||||||
|
"output": 83968
|
||||||
|
},
|
||||||
|
"modalities": {
|
||||||
|
"input": ["text", "image", "pdf"],
|
||||||
|
"output": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cpu_gemma4-E2B-Q4_K_M": {
|
||||||
|
"name": "cpu_gemma4-E2B-Q4_K_M",
|
||||||
|
"tool_call": false,
|
||||||
|
"reasoning": false,
|
||||||
|
"limit": {
|
||||||
|
"context": 16384,
|
||||||
|
"output": 16384
|
||||||
|
},
|
||||||
|
"modalities": {
|
||||||
|
"input": ["text"],
|
||||||
|
"output": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Qwopus3.5-27B-v3-Q8_0": {
|
||||||
|
"name": "Qwopus3.5-27B-v3-Q8_0",
|
||||||
|
"tool_call": true,
|
||||||
|
"reasoning": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 262144,
|
||||||
|
"output": 83968
|
||||||
|
},
|
||||||
|
"modalities": {
|
||||||
|
"input": ["text"],
|
||||||
|
"output": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Qwopus3.6-27B-v1-preview-Q8_0": {
|
||||||
|
"name": "Qwopus3.6-27B-v1-preview-Q8_0",
|
||||||
|
"tool_call": true,
|
||||||
|
"reasoning": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 262144,
|
||||||
|
"output": 83968
|
||||||
|
},
|
||||||
|
"modalities": {
|
||||||
|
"input": ["text"],
|
||||||
|
"output": ["text"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Qwopus3.6-35B-A3B-v1-Q8_0": {
|
||||||
|
"name": "Qwopus3.6-35B-A3B-v1-Q8_0",
|
||||||
|
"tool_call": true,
|
||||||
|
"reasoning": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 262144,
|
||||||
|
"output": 83968
|
||||||
|
},
|
||||||
|
"modalities": {
|
||||||
|
"input": ["text"],
|
||||||
|
"output": ["text"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"baseURL": "https://ai.shstk.ru/v1",
|
||||||
|
"apiKey": "shs.passwd"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mcp": {
|
"mcp": {
|
||||||
"IntelliJIdea": {
|
"chrome-devtools": {
|
||||||
"type": "remote",
|
"type": "local",
|
||||||
"url": "http://127.0.0.1:64342/stream",
|
"command": ["/home/andrey/chrome-devtools-mcp-venv/bin/python", "/home/andrey/chrome-devtools-mcp/server.py"],
|
||||||
|
"environment": {
|
||||||
|
"CHROME_DEBUG_PORT": "9222"
|
||||||
|
},
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -114,7 +114,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.15.0"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencies:
|
|||||||
uuid: ^4.5.0
|
uuid: ^4.5.0
|
||||||
shelf_cors_headers: ^0.1.5
|
shelf_cors_headers: ^0.1.5
|
||||||
shelf_static: ^1.1.3
|
shelf_static: ^1.1.3
|
||||||
|
crypto: ^3.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
http: ^1.2.2
|
http: ^1.2.2
|
||||||
|
|||||||
+33
-32
@@ -1192,6 +1192,39 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
convert
|
||||||
|
crypto
|
||||||
|
vm_service
|
||||||
|
|
||||||
|
Copyright 2015, the Dart project authors.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
* Neither the name of Google LLC nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
cpu_features
|
cpu_features
|
||||||
|
|
||||||
@@ -1349,38 +1382,6 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
|||||||
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.
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
crypto
|
|
||||||
vm_service
|
|
||||||
|
|
||||||
Copyright 2015, the Dart project authors.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
* Neither the name of Google LLC nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
cupertino_icons
|
cupertino_icons
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -38,6 +38,6 @@ _flutter.buildConfig = {"engineRevision":"13e658725ddaa270601426d1485636157e38c3
|
|||||||
|
|
||||||
_flutter.loader.load({
|
_flutter.loader.load({
|
||||||
serviceWorkerSettings: {
|
serviceWorkerSettings: {
|
||||||
serviceWorkerVersion: "240864774"
|
serviceWorkerVersion: "3111892966"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ const CACHE_NAME = 'flutter-app-cache';
|
|||||||
const RESOURCES = {"assets/AssetManifest.bin": "a53491ba33870e68dce7c820b0867cda",
|
const RESOURCES = {"assets/AssetManifest.bin": "a53491ba33870e68dce7c820b0867cda",
|
||||||
"assets/AssetManifest.bin.json": "bb5daa477ab092a8c1a34a960dc05d3a",
|
"assets/AssetManifest.bin.json": "bb5daa477ab092a8c1a34a960dc05d3a",
|
||||||
"assets/FontManifest.json": "dc3d03800ccca4601324923c0b1d6d57",
|
"assets/FontManifest.json": "dc3d03800ccca4601324923c0b1d6d57",
|
||||||
"assets/fonts/MaterialIcons-Regular.otf": "23db0ca57ad7be2949ab89f7262d4e6b",
|
"assets/fonts/MaterialIcons-Regular.otf": "d32e2e1722cfb06f6bfb59bbcd7212a8",
|
||||||
"assets/NOTICES": "65b9f87b7760a12c8af2dfb632f65576",
|
"assets/NOTICES": "ca5774b803374f1cbef1c7369cf6421f",
|
||||||
"assets/packages/cupertino_icons/assets/CupertinoIcons.ttf": "33b7d9392238c04c131b6ce224e13711",
|
"assets/packages/cupertino_icons/assets/CupertinoIcons.ttf": "33b7d9392238c04c131b6ce224e13711",
|
||||||
"assets/packages/flutter_map/lib/assets/flutter_map_logo.png": "208d63cc917af9713fc9572bd5c09362",
|
"assets/packages/flutter_map/lib/assets/flutter_map_logo.png": "208d63cc917af9713fc9572bd5c09362",
|
||||||
"assets/shaders/ink_sparkle.frag": "ecc85a2e95f5e9f53123dcaf8cb9b6ce",
|
"assets/shaders/ink_sparkle.frag": "ecc85a2e95f5e9f53123dcaf8cb9b6ce",
|
||||||
@@ -26,15 +26,24 @@ const RESOURCES = {"assets/AssetManifest.bin": "a53491ba33870e68dce7c820b0867cda
|
|||||||
"canvaskit/skwasm_heavy.wasm": "b0be7910760d205ea4e011458df6ee01",
|
"canvaskit/skwasm_heavy.wasm": "b0be7910760d205ea4e011458df6ee01",
|
||||||
"favicon.png": "5dcef449791fa27946b3d35ad8803796",
|
"favicon.png": "5dcef449791fa27946b3d35ad8803796",
|
||||||
"flutter.js": "24bc71911b75b5f8135c949e27a2984e",
|
"flutter.js": "24bc71911b75b5f8135c949e27a2984e",
|
||||||
"flutter_bootstrap.js": "15099f2c5c01c0361ce5d2fabb8e45d4",
|
"flutter_bootstrap.js": "237272135d040669f2bc437493a8fbb0",
|
||||||
"icons/Icon-192.png": "ac9a721a12bbc803b44f645561ecb1e1",
|
"icons/Icon-192.png": "ac9a721a12bbc803b44f645561ecb1e1",
|
||||||
"icons/Icon-512.png": "96e752610906ba2a93c65f8abe1645f1",
|
"icons/Icon-512.png": "96e752610906ba2a93c65f8abe1645f1",
|
||||||
"icons/Icon-maskable-192.png": "c457ef57daa1d16f64b27b786ec2ea3c",
|
"icons/Icon-maskable-192.png": "c457ef57daa1d16f64b27b786ec2ea3c",
|
||||||
"icons/Icon-maskable-512.png": "301a7604d45b3e739efc881eb04896ea",
|
"icons/Icon-maskable-512.png": "301a7604d45b3e739efc881eb04896ea",
|
||||||
"index.html": "fd3772c1a2e81dd78760dd1828630f54",
|
"index.html": "fd3772c1a2e81dd78760dd1828630f54",
|
||||||
"/": "fd3772c1a2e81dd78760dd1828630f54",
|
"/": "fd3772c1a2e81dd78760dd1828630f54",
|
||||||
"main.dart.js": "1659f85323a9023cc3fc9360a93c2fff",
|
"main.dart.js": "d7729309ffa707d7e972e7c0cbf86371",
|
||||||
"manifest.json": "63475ebd55563a27bbb61ff120531aa9",
|
"manifest.json": "63475ebd55563a27bbb61ff120531aa9",
|
||||||
|
"swagger/favicon-16x16.png": "f0ae831196d55d8f4115b6c5e8ec5384",
|
||||||
|
"swagger/favicon-32x32.png": "40d4f2c38d1cd854ad463f16373cbcb6",
|
||||||
|
"swagger/index.css": "242e3dadfcb36ef4b581da106d054c70",
|
||||||
|
"swagger/index.html": "58f6dccdccb318bd36b7bb0a9ad1c773",
|
||||||
|
"swagger/oauth2-redirect.html": "cdddcd0a8c7d7532fefb0d78b1e96783",
|
||||||
|
"swagger/swagger-initializer.js": "979ee1d63e1b50994da62b9555ee46ed",
|
||||||
|
"swagger/swagger-ui-bundle.js": "fec23579c8239f640990d2eb0edea517",
|
||||||
|
"swagger/swagger-ui.css": "17a93da4a76a27b8497f58b850c14b0f",
|
||||||
|
"swagger/swagger.yaml": "9b18e7401a099d5dd24a02aed8192ee8",
|
||||||
"version.json": "1951b42b2d0a7cfed431f27ac9bcd8ee"};
|
"version.json": "1951b42b2d0a7cfed431f27ac9bcd8ee"};
|
||||||
// The application shell files that are downloaded before a service worker can
|
// The application shell files that are downloaded before a service worker can
|
||||||
// start.
|
// start.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="referrer" content="strict-origin-when-cross-origin">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="A new Flutter project.">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
|
|||||||
+52181
-49870
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,16 @@
|
|||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: -moz-scrollbars-vertical;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- HTML for static distribution bundle build -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Swagger UI</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="swagger-ui.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="index.css" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||||
|
<script src="swagger-initializer.js" charset="UTF-8"> </script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<head>
|
||||||
|
<title>Swagger UI: OAuth2 Redirect</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
function run () {
|
||||||
|
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||||
|
var sentState = oauth2.state;
|
||||||
|
var redirectUrl = oauth2.redirectUrl;
|
||||||
|
var isValid, qp, arr;
|
||||||
|
|
||||||
|
if (/code|token|error/.test(window.location.hash)) {
|
||||||
|
qp = window.location.hash.substring(1).replace('?', '&');
|
||||||
|
} else {
|
||||||
|
qp = location.search.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
arr = qp.split("&");
|
||||||
|
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||||
|
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||||
|
function (key, value) {
|
||||||
|
return key === "" ? value : decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
) : {};
|
||||||
|
|
||||||
|
isValid = qp.state === sentState;
|
||||||
|
|
||||||
|
if ((
|
||||||
|
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||||
|
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||||
|
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||||
|
) && !oauth2.auth.code) {
|
||||||
|
if (!isValid) {
|
||||||
|
oauth2.errCb({
|
||||||
|
authId: oauth2.auth.name,
|
||||||
|
source: "auth",
|
||||||
|
level: "warning",
|
||||||
|
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qp.code) {
|
||||||
|
delete oauth2.state;
|
||||||
|
oauth2.auth.code = qp.code;
|
||||||
|
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||||
|
} else {
|
||||||
|
let oauthErrorMsg;
|
||||||
|
if (qp.error) {
|
||||||
|
oauthErrorMsg = "["+qp.error+"]: " +
|
||||||
|
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||||
|
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2.errCb({
|
||||||
|
authId: oauth2.auth.name,
|
||||||
|
source: "auth",
|
||||||
|
level: "error",
|
||||||
|
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
run();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
window.onload = function() {
|
||||||
|
//<editor-fold desc="Changeable Configuration Block">
|
||||||
|
|
||||||
|
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
|
||||||
|
window.ui = SwaggerUIBundle({
|
||||||
|
url: "/swagger/swagger.yaml",
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: false,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
layout: "BaseLayout"
|
||||||
|
});
|
||||||
|
|
||||||
|
//</editor-fold>
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,346 @@
|
|||||||
|
openapi: "3.0.3"
|
||||||
|
info:
|
||||||
|
title: Family Safety Tracker API
|
||||||
|
description: REST API for sharing geolocation with family members. Authentication via JWT tokens.
|
||||||
|
version: 1.0.0
|
||||||
|
contact:
|
||||||
|
name: Family Safety Tracker
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:9090
|
||||||
|
description: Local development server
|
||||||
|
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/login:
|
||||||
|
post:
|
||||||
|
summary: Authenticate user
|
||||||
|
description: Login with login and password. Returns a JWT token on success.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/LoginRequest"
|
||||||
|
example:
|
||||||
|
login: "john_doe"
|
||||||
|
password: "secret123"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Authentication successful
|
||||||
|
headers:
|
||||||
|
Access-Control-Allow-Origin:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/LoginResponse"
|
||||||
|
example:
|
||||||
|
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
"401":
|
||||||
|
description: Invalid credentials
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
example:
|
||||||
|
error: "Invalid credentials"
|
||||||
|
|
||||||
|
/reg:
|
||||||
|
post:
|
||||||
|
summary: Register a new user
|
||||||
|
description: Create a new user account. Password is hashed with bcrypt.
|
||||||
|
tags:
|
||||||
|
- Authentication
|
||||||
|
security: []
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RegisterRequest"
|
||||||
|
example:
|
||||||
|
login: "john_doe"
|
||||||
|
password: "secret123"
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: User registered successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/RegisterResponse"
|
||||||
|
example:
|
||||||
|
message: "User registered"
|
||||||
|
"400":
|
||||||
|
description: Bad request (e.g. login already exists)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
|
||||||
|
/geo:
|
||||||
|
put:
|
||||||
|
summary: Update geolocation position
|
||||||
|
description: Update the coordinates of an existing geoposition record.
|
||||||
|
tags:
|
||||||
|
- Geolocation
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
description: UUID of the geoposition to update
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PositionRequest"
|
||||||
|
example:
|
||||||
|
x: 55.7558
|
||||||
|
y: 37.6173
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Position updated successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/MessageResponse"
|
||||||
|
example:
|
||||||
|
message: "Position updated"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized - invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
"404":
|
||||||
|
description: Geoposition not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
|
||||||
|
/share:
|
||||||
|
post:
|
||||||
|
summary: Create a share link
|
||||||
|
description: Create a new geoposition and generate a unique share link UUID for public viewing.
|
||||||
|
tags:
|
||||||
|
- Sharing
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PositionRequest"
|
||||||
|
example:
|
||||||
|
x: 55.7558
|
||||||
|
y: 37.6173
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Share link created successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ShareResponse"
|
||||||
|
example:
|
||||||
|
geo_id: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
share_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized - invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
|
||||||
|
/watch:
|
||||||
|
get:
|
||||||
|
summary: Get latest position via share link
|
||||||
|
description: Retrieve the latest position for a given share link UUID. Share links are stored in-memory.
|
||||||
|
tags:
|
||||||
|
- Sharing
|
||||||
|
parameters:
|
||||||
|
- name: share_id
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
description: UUID of the share link
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Position retrieved successfully
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/PositionResponse"
|
||||||
|
example:
|
||||||
|
x: 55.7558
|
||||||
|
y: 37.6173
|
||||||
|
last_update: "2026-05-15T10:30:00.000Z"
|
||||||
|
expires_at: "2026-05-15T11:30:00.000Z"
|
||||||
|
"401":
|
||||||
|
description: Unauthorized - invalid or missing token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
"404":
|
||||||
|
description: Share link not found or no position available
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ErrorResponse"
|
||||||
|
examples:
|
||||||
|
linkNotFound:
|
||||||
|
summary: Share link not found
|
||||||
|
value:
|
||||||
|
error: "Share link not found"
|
||||||
|
noPosition:
|
||||||
|
summary: No position available
|
||||||
|
value:
|
||||||
|
error: "No position available"
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
BearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
|
description: JWT token obtained from /login. Payload contains user_id, login, and iss="family_safety_tracker".
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
LoginRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- login
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
login:
|
||||||
|
type: string
|
||||||
|
description: User login / username
|
||||||
|
example: "john_doe"
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
format: password
|
||||||
|
description: User password (verified against bcrypt hash)
|
||||||
|
example: "secret123"
|
||||||
|
|
||||||
|
RegisterRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- login
|
||||||
|
- password
|
||||||
|
properties:
|
||||||
|
login:
|
||||||
|
type: string
|
||||||
|
description: Desired login / username
|
||||||
|
example: "john_doe"
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
format: password
|
||||||
|
description: Desired password (will be hashed with bcrypt)
|
||||||
|
example: "secret123"
|
||||||
|
|
||||||
|
LoginResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: JWT token for authenticated requests
|
||||||
|
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
|
||||||
|
RegisterResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: "User registered"
|
||||||
|
|
||||||
|
PositionRequest:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- x
|
||||||
|
- y
|
||||||
|
properties:
|
||||||
|
x:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: Longitude coordinate
|
||||||
|
example: 55.7558
|
||||||
|
y:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: Latitude coordinate
|
||||||
|
example: 37.6173
|
||||||
|
|
||||||
|
PositionResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
x:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: Longitude coordinate
|
||||||
|
example: 55.7558
|
||||||
|
y:
|
||||||
|
type: number
|
||||||
|
format: double
|
||||||
|
description: Latitude coordinate
|
||||||
|
example: 37.6173
|
||||||
|
last_update:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Timestamp of the last position update (ISO 8601)
|
||||||
|
example: "2026-05-15T10:30:00.000Z"
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
description: Expiration timestamp for the position (ISO 8601)
|
||||||
|
example: "2026-05-15T11:30:00.000Z"
|
||||||
|
|
||||||
|
ShareResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
geo_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: UUID of the created geoposition record in the database
|
||||||
|
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
share_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: UUID of the share link for public viewing
|
||||||
|
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
||||||
|
|
||||||
|
MessageResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: "Position updated"
|
||||||
|
|
||||||
|
ErrorResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: Error description
|
||||||
|
example: "Invalid credentials"
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Authentication
|
||||||
|
description: User registration and login
|
||||||
|
- name: Geolocation
|
||||||
|
description: Create and update geolocation positions
|
||||||
|
- name: Sharing
|
||||||
|
description: Share links for public position viewing
|
||||||
Reference in New Issue
Block a user