feat: add Docker support, refactor DB layer, update API responses

This commit is contained in:
dmit.b
2026-05-08 16:38:57 +03:00
parent 9ecbc3aa79
commit cd85f5f2db
12 changed files with 568 additions and 74 deletions
+13
View File
@@ -0,0 +1,13 @@
# Environment variables for family_safety_tracker
# JWT secret used for signing tokens
JWT_SECRET=your-super-secret-key-change-me
# Secret pepper added to passwords before hashing (should be random and kept secret)
PASSWORD_PEPPER=your-random-pepper-string-change-me
# Database connection settings
POSTGRES_HOST="db"
POSTGRES_PORT="5432"
POSTGRES_DB="family_safety"
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
# TOKEN_LIFETIME in minutes
TOKEN_LIFETIME=600
+5 -6
View File
@@ -18,11 +18,11 @@ class DatabaseProvider {
Future<void> initialize() async {
final dotenv = DotEnv();
final host = dotenv['POSTGRES_HOST'] ?? 'localhost';
final host = dotenv['POSTGRES_HOST'] ?? 'db';
final port = int.parse(dotenv['POSTGRES_PORT'] ?? '5432');
final databaseName = dotenv['POSTGRES_DB'] ?? 'family_safety';
final username = dotenv['POSTGRES_USER'] ?? 'postgres';
final password = dotenv['POSTGRES_PASSWORD'] ?? '';
final password = dotenv['POSTGRES_PASSWORD'] ?? 'postgres';
try {
final defaultConnection = await Connection.open(
@@ -43,8 +43,7 @@ class DatabaseProvider {
if (results.isEmpty) {
await defaultConnection.execute(
Sql.named('CREATE DATABASE @dbName'),
parameters: {'dbName': databaseName},
Sql('CREATE DATABASE $databaseName'),
);
print('Database $databaseName created.');
} else {
@@ -78,7 +77,7 @@ class DatabaseProvider {
await _dbConnection.execute(
Sql.named('''
CREATE TABLE IF NOT EXISTS geopositions (
id SERIAL PRIMARY KEY,
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
x_value DOUBLE PRECISION NOT NULL,
y_value DOUBLE PRECISION NOT NULL,
last_update TIMESTAMP NOT NULL DEFAULT NOW(),
@@ -265,7 +264,7 @@ class DatabaseProvider {
Future<Geoposition?> getLatestPosition() async {
final results = await _dbConnection.execute(
Sql.named('''
SELECT id, x_value, y_value, last_update, expires_at
SELECT x_value, y_value, last_update, expires_at
FROM geopositions
WHERE expires_at > NOW()
ORDER BY last_update DESC
+9 -5
View File
@@ -3,7 +3,7 @@ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dotenv/dotenv.dart';
class AuthMiddleware {
final Handler handler;
final Future<Response> Function(Request, String) handler;
AuthMiddleware(this.handler);
@@ -19,11 +19,15 @@ class AuthMiddleware {
try {
final dotenv = DotEnv();
final secret = dotenv['JWT_SECRET'] ?? '';
final decoded = JWT.verify(token, SecretKey(secret));
final jwt = JWT.verify(token, SecretKey(secret));
final payload = jwt.payload;
final login = payload['login'] as String;
return handler(request);
} catch (e) {
return Response(401, body: 'Invalid or expired token');
return handler(request, login);
} on JWTExpiredException {
return Response(401, body: 'Token expired');
} on JWTException {
return Response(401, body: 'Invalid token');
}
}
}
+9 -7
View File
@@ -44,10 +44,7 @@ class AuthRoutes {
final token = jwt.sign(SecretKey(secret));
await database.createLog(login, 'Successful login');
return Response(200, body: jsonEncode({
'user': user.toMap(),
'token': token
}));
return Response(200, body: jsonEncode(token));
}
Future<Response> _register(Request request) async {
@@ -57,9 +54,9 @@ class AuthRoutes {
final login = data['login'];
final password = data['password'];
final user = await database.createUser(login, password);
await database.createUser(login, password);
await database.createLog(login, 'User registration');
return Response(201, body: jsonEncode(user.toMap()));
return Response(201);
}
Future<Response> _watch(Request request, String login) async {
@@ -78,6 +75,11 @@ class AuthRoutes {
}
await database.createLog(login, 'Accessed share link');
return Response(200, body: jsonEncode(position.toJson()));
return Response(200, body: jsonEncode({
'x': position.xValue,
'y': position.yValue,
'last_update': position.lastUpdate,
'expires_at': position.expiresAt,
}));
}
}
+6 -4
View File
@@ -16,7 +16,7 @@ class GeoRoutes {
return router;
}
Future<Response> _updatePosition(Request request) async {
Future<Response> _updatePosition(Request request, String login) async {
final id = int.parse(request.url.queryParameters['id']!);
final body = await request.readAsString();
final data = jsonDecode(body);
@@ -24,11 +24,12 @@ class GeoRoutes {
final x = data['x'];
final y = data['y'];
final position = await database.updatePosition(id, x, y);
return Response(200, body: position.toJson());
await database.updatePosition(id, x, y);
await database.createLog(login, 'Updated position id=$id');
return Response(200);
}
Future<Response> _createShare(Request request) async {
Future<Response> _createShare(Request request, String login) async {
final body = await request.readAsString();
final data = jsonDecode(body);
@@ -38,6 +39,7 @@ class GeoRoutes {
final position = await database.createPosition(x, y);
final shareId = database.createShareId();
await database.createLog(login, 'Created share link geo_id=${position.id}');
return Response(201, body: jsonEncode({
'geo_id': position.id,
'share_id': shareId
+3 -3
View File
@@ -21,8 +21,8 @@ void main(List<String> args) async {
final geoRoutes = GeoRoutes(database);
final router = Router()
..mount('', authRoutes.routes.call)
..mount('', geoRoutes.routes.call)
..mount('/', authRoutes.routes.call)
..mount('/', geoRoutes.routes.call)
..get('/', (Request req) => Response.ok('Family Safety Tracker API\n'));
final handler = Pipeline()
@@ -30,7 +30,7 @@ void main(List<String> args) async {
.addHandler(router.call);
final ip = InternetAddress.anyIPv4;
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final port = int.parse(Platform.environment['PORT'] ?? '9090');
final server = await serve(handler, ip, port);
print('Server listening on port ${server.port}');
}