Dev #3

Merged
rezidir merged 3 commits from dev into release 2026-06-22 11:08:50 +03:00
13 changed files with 500 additions and 8 deletions
+3 -3
View File
@@ -7,9 +7,9 @@ PASSWORD_PEPPER=your-random-pepper-string-change-me
POSTGRES_HOST="localhost"
POSTGRES_PORT="5432"
POSTGRES_DB="family_safety"
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
POSTGRES_USER="user"
POSTGRES_PASSWORD="pwd"
# 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.
REGISTRATION_SECRET_KEY=reg
+1 -1
View File
@@ -1,7 +1,6 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
/web
/build
/windows
/opencode.json
@@ -12,3 +11,4 @@
/api.md
/android
/.idea
.env
+10
View File
@@ -7,6 +7,7 @@ import '../database/database_provider.dart';
import '../middleware/auth_middleware.dart';
import '../middleware/request_logger.dart';
import 'dart:convert';
import 'dart:io';
class AuthRoutes {
final DatabaseProvider database;
@@ -18,6 +19,7 @@ class AuthRoutes {
router.post('/login', _login);
router.post('/reg', _register);
router.get('/watch', AuthMiddleware(_watch).call);
router.get('/api_info', _apiInfo);
return router;
}
@@ -112,4 +114,12 @@ class AuthRoutes {
logRequest(method: 'GET', url: '/watch', status: 200, duration: stopwatch.elapsed, responseHeaders: response.headers);
return response;
}
Future<Response> _apiInfo(Request request) async {
final swaggerDir = Directory('web/swagger');
final swaggerFile = File('${swaggerDir.path}/index.html');
final html = await swaggerFile.readAsString();
final response = Response(200, body: html, headers: {'Content-Type': 'text/html'});
return response;
}
}
+1 -2
View File
@@ -38,18 +38,17 @@ void main(List<String> args) async {
final authRoutes = AuthRoutes(database);
final geoRoutes = GeoRoutes(database);
final router = Router()
..mount('/', authRoutes.routes.call)
..mount('/', geoRoutes.routes.call);
final staticHandler = createStaticHandler('web', defaultDocument: 'index.html');
final apiHandler = createStaticHandler('web/swagger', defaultDocument: 'index.html');
final handler = Pipeline()
.addMiddleware(_corsMiddleware)
.addMiddleware(logRequests())
.addHandler(Cascade()
.add(apiHandler)
.add(staticHandler)
.add(router.call)
.handler);
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