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:
dmit.b
2026-05-15 17:43:53 +03:00
parent 6b4c599981
commit fde96c0197
25 changed files with 53278 additions and 49937 deletions
+346
View File
@@ -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
+3 -1
View File
@@ -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)
PASSWORD_PEPPER=your-random-pepper-string-change-me
# Database connection settings
POSTGRES_HOST="db"
POSTGRES_HOST="localhost"
POSTGRES_PORT="5432"
POSTGRES_DB="family_safety"
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
# TOKEN_LIFETIME in minutes
TOKEN_LIFETIME=600
# Secret key for registration (MD5 hash of this key must be sent by the user)
REGISTRATION_SECRET_KEY=FtracKer*1405.
+26
View File
@@ -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('');
}
+32 -7
View File
@@ -5,6 +5,7 @@ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dotenv/dotenv.dart';
import '../database/database_provider.dart';
import '../middleware/auth_middleware.dart';
import '../middleware/request_logger.dart';
import 'dart:convert';
class AuthRoutes {
@@ -29,9 +30,13 @@ class AuthRoutes {
final user = await database.findUserByLogin(login);
final stopwatch = Stopwatch()..start();
if (user == null || !BCrypt.checkpw(password, user.pwdHash)) {
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 токена
@@ -44,10 +49,14 @@ class AuthRoutes {
final token = jwt.sign(SecretKey(secret));
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 {
final stopwatch = Stopwatch()..start();
final body = await request.readAsString();
final data = jsonDecode(body);
@@ -56,35 +65,51 @@ class AuthRoutes {
await database.createUser(login, password);
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 {
final stopwatch = Stopwatch()..start();
final uniqueId = request.url.queryParameters['share_id'];
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);
if (geoId == null) {
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);
if (position == null) {
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');
return Response(200, body: jsonEncode({
stopwatch.stop();
final response = Response(200, body: jsonEncode({
'x': position.xValue,
'y': position.yValue,
'last_update': position.lastUpdate.toIso8601String(),
'expires_at': position.expiresAt.toIso8601String(),
}), headers: {'Content-Type': 'application/json'});
logRequest(method: 'GET', url: '/watch', status: 200, duration: stopwatch.elapsed, responseHeaders: response.headers);
return response;
}
}
+11 -2
View File
@@ -2,6 +2,7 @@ import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../database/database_provider.dart';
import '../middleware/auth_middleware.dart';
import '../middleware/request_logger.dart';
import 'dart:convert';
class GeoRoutes {
@@ -17,6 +18,7 @@ class GeoRoutes {
}
Future<Response> _updatePosition(Request request, String login) async {
final stopwatch = Stopwatch()..start();
final id = request.url.queryParameters['id']!;
final body = await request.readAsString();
final data = jsonDecode(body);
@@ -26,10 +28,14 @@ class GeoRoutes {
await database.updatePosition(id, x, y);
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 {
final stopwatch = Stopwatch()..start();
final body = await request.readAsString();
final data = jsonDecode(body);
@@ -40,9 +46,12 @@ class GeoRoutes {
final shareId = database.createShareId(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,
'share_id': shareId
}), headers: {'Content-Type': 'application/json'});
logRequest(method: 'POST', url: '/share', status: 201, duration: stopwatch.elapsed, body: body, responseHeaders: response.headers);
return response;
}
}
+20 -2
View File
@@ -4,13 +4,29 @@ import 'dart:async';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.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 'database/database_provider.dart';
import 'routes/auth_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 {
final database = DatabaseProvider();
await database.initialize();
@@ -27,11 +43,13 @@ void main(List<String> args) async {
..mount('/', geoRoutes.routes.call);
final staticHandler = createStaticHandler('web', defaultDocument: 'index.html');
final apiHandler = createStaticHandler('web/swagger', defaultDocument: 'index.html');
final handler = Pipeline()
.addMiddleware(corsHeaders())
.addMiddleware(_corsMiddleware)
.addMiddleware(logRequests())
.addHandler(Cascade()
.add(apiHandler)
.add(staticHandler)
.add(router.call)
.handler);
-2
View File
@@ -9,8 +9,6 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: family_safety
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
+124 -13
View File
@@ -1,27 +1,138 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"llama.cpp": {
"npm": "@ai-sdk/openai-compatible",
"name": "llama-server (local)",
"options": {
"baseURL": "http://127.0.0.1:9988/v1"
"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": {
"llama_swap": {
"name": "llama_swap",
"npm": "@ai-sdk/openai-compatible",
"models": {
"qwen3-coder:a3b": {
"name": "Qwen_Qwen3.5-9B-Q6_K (local)",
"Qwen3.6-35B-A3B-Q8_0_img": {
"name": "Qwen3.6-35B-A3B-Q8_0_img",
"tool_call": true,
"reasoning": true,
"limit": {
"context": 248000,
"output": 65655
"context": 196608,
"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": {
"IntelliJIdea": {
"type": "remote",
"url": "http://127.0.0.1:64342/stream",
"chrome-devtools": {
"type": "local",
"command": ["/home/andrey/chrome-devtools-mcp-venv/bin/python", "/home/andrey/chrome-devtools-mcp/server.py"],
"environment": {
"CHROME_DEBUG_PORT": "9222"
},
"enabled": true
}
}
+1 -1
View File
@@ -114,7 +114,7 @@ packages:
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
dependency: "direct main"
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
+1
View File
@@ -16,6 +16,7 @@ dependencies:
uuid: ^4.5.0
shelf_cors_headers: ^0.1.5
shelf_static: ^1.1.3
crypto: ^3.0.3
dev_dependencies:
http: ^1.2.2
+33 -32
View File
@@ -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
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
@@ -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.
See the License for the specific language governing permissions and
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
Binary file not shown.
+1 -1
View File
@@ -38,6 +38,6 @@ _flutter.buildConfig = {"engineRevision":"13e658725ddaa270601426d1485636157e38c3
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: "240864774"
serviceWorkerVersion: "3111892966"
}
});
+13 -4
View File
@@ -6,8 +6,8 @@ const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {"assets/AssetManifest.bin": "a53491ba33870e68dce7c820b0867cda",
"assets/AssetManifest.bin.json": "bb5daa477ab092a8c1a34a960dc05d3a",
"assets/FontManifest.json": "dc3d03800ccca4601324923c0b1d6d57",
"assets/fonts/MaterialIcons-Regular.otf": "23db0ca57ad7be2949ab89f7262d4e6b",
"assets/NOTICES": "65b9f87b7760a12c8af2dfb632f65576",
"assets/fonts/MaterialIcons-Regular.otf": "d32e2e1722cfb06f6bfb59bbcd7212a8",
"assets/NOTICES": "ca5774b803374f1cbef1c7369cf6421f",
"assets/packages/cupertino_icons/assets/CupertinoIcons.ttf": "33b7d9392238c04c131b6ce224e13711",
"assets/packages/flutter_map/lib/assets/flutter_map_logo.png": "208d63cc917af9713fc9572bd5c09362",
"assets/shaders/ink_sparkle.frag": "ecc85a2e95f5e9f53123dcaf8cb9b6ce",
@@ -26,15 +26,24 @@ const RESOURCES = {"assets/AssetManifest.bin": "a53491ba33870e68dce7c820b0867cda
"canvaskit/skwasm_heavy.wasm": "b0be7910760d205ea4e011458df6ee01",
"favicon.png": "5dcef449791fa27946b3d35ad8803796",
"flutter.js": "24bc71911b75b5f8135c949e27a2984e",
"flutter_bootstrap.js": "15099f2c5c01c0361ce5d2fabb8e45d4",
"flutter_bootstrap.js": "237272135d040669f2bc437493a8fbb0",
"icons/Icon-192.png": "ac9a721a12bbc803b44f645561ecb1e1",
"icons/Icon-512.png": "96e752610906ba2a93c65f8abe1645f1",
"icons/Icon-maskable-192.png": "c457ef57daa1d16f64b27b786ec2ea3c",
"icons/Icon-maskable-512.png": "301a7604d45b3e739efc881eb04896ea",
"index.html": "fd3772c1a2e81dd78760dd1828630f54",
"/": "fd3772c1a2e81dd78760dd1828630f54",
"main.dart.js": "1659f85323a9023cc3fc9360a93c2fff",
"main.dart.js": "d7729309ffa707d7e972e7c0cbf86371",
"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"};
// The application shell files that are downloaded before a service worker can
// start.
+1
View File
@@ -18,6 +18,7 @@
<meta charset="UTF-8">
<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.">
<!-- iOS meta tags & icons -->
+52181 -49870
View File
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

+16
View File
@@ -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;
}
+18
View File
@@ -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>
+79
View File
@@ -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>
+19
View File
@@ -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
+346
View File
@@ -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