Add JWT auth for protected routes, add /reg endpoint, remove /user endpoints

This commit is contained in:
dmit.b
2026-05-08 13:38:52 +03:00
parent 49bb854ca2
commit 3124629e6c
9 changed files with 109 additions and 145 deletions
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+30 -52
View File
@@ -12,7 +12,7 @@ import '../models/log.dart';
class DatabaseProvider { class DatabaseProvider {
late Connection _dbConnection; late Connection _dbConnection;
final Map<String, int> _shareLinks = {}; final Map<String, bool> _shareLinks = {};
final _uuid = const Uuid(); final _uuid = const Uuid();
Future<void> initialize() async { Future<void> initialize() async {
@@ -79,11 +79,9 @@ class DatabaseProvider {
Sql.named(''' Sql.named('''
CREATE TABLE IF NOT EXISTS geopositions ( CREATE TABLE IF NOT EXISTS geopositions (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
x_value DOUBLE PRECISION NOT NULL, x_value DOUBLE PRECISION NOT NULL,
y_value DOUBLE PRECISION NOT NULL, y_value DOUBLE PRECISION NOT NULL,
datetime TIMESTAMP NOT NULL DEFAULT NOW(), last_update TIMESTAMP NOT NULL DEFAULT NOW(),
lifetime INTERVAL NOT NULL,
expires_at TIMESTAMP NOT NULL expires_at TIMESTAMP NOT NULL
) )
'''), '''),
@@ -204,24 +202,20 @@ class DatabaseProvider {
// ==================== Geoposition operations ==================== // ==================== Geoposition operations ====================
Future<Geoposition> createPosition( Future<Geoposition> createPosition(
int userId,
double x, double x,
double y, double y,
Duration lifetime,
) async { ) async {
final expiresAt = DateTime.now().add(lifetime); final expiresAt = DateTime.now().add(const Duration(hours: 24));
final results = await _dbConnection.execute( final results = await _dbConnection.execute(
Sql.named(''' Sql.named('''
INSERT INTO geopositions (user_id, x_value, y_value, datetime, lifetime, expires_at) INSERT INTO geopositions (x_value, y_value, last_update, expires_at)
VALUES (@userId, @xValue, @yValue, NOW(), @lifetime, @expiresAt) VALUES (@xValue, @yValue, NOW(), @expiresAt)
RETURNING id, user_id, x_value, y_value, datetime, lifetime, expires_at RETURNING id, x_value, y_value, last_update, expires_at
'''), '''),
parameters: { parameters: {
'userId': userId,
'xValue': x, 'xValue': x,
'yValue': y, 'yValue': y,
'lifetime': _toInterval(lifetime),
'expiresAt': expiresAt.toIso8601String(), 'expiresAt': expiresAt.toIso8601String(),
}, },
); );
@@ -229,34 +223,28 @@ class DatabaseProvider {
final row = results.first; final row = results.first;
return Geoposition( return Geoposition(
id: int.parse(row[0].toString()), id: int.parse(row[0].toString()),
userId: int.parse(row[1].toString()), xValue: double.parse(row[1].toString()),
xValue: double.parse(row[2].toString()), yValue: double.parse(row[2].toString()),
yValue: double.parse(row[3].toString()), lastUpdate: DateTime.parse(row[3].toString()),
datetime: DateTime.parse(row[4].toString()), expiresAt: DateTime.parse(row[4].toString()),
lifetime: lifetime,
expiresAt: DateTime.parse(row[6].toString()),
); );
} }
Future<Geoposition> updatePosition( Future<Geoposition> updatePosition(
int userId,
double x, double x,
double y, double y,
Duration lifetime,
) async { ) async {
final expiresAt = DateTime.now().add(lifetime); final expiresAt = DateTime.now().add(const Duration(hours: 24));
final results = await _dbConnection.execute( final results = await _dbConnection.execute(
Sql.named(''' Sql.named('''
INSERT INTO geopositions (user_id, x_value, y_value, datetime, lifetime, expires_at) INSERT INTO geopositions (x_value, y_value, last_update, expires_at)
VALUES (@userId, @xValue, @yValue, NOW(), @lifetime, @expiresAt) VALUES (@xValue, @yValue, NOW(), @expiresAt)
RETURNING id, user_id, x_value, y_value, datetime, lifetime, expires_at RETURNING id, x_value, y_value, last_update, expires_at
'''), '''),
parameters: { parameters: {
'userId': userId,
'xValue': x, 'xValue': x,
'yValue': y, 'yValue': y,
'lifetime': _toInterval(lifetime),
'expiresAt': expiresAt.toIso8601String(), 'expiresAt': expiresAt.toIso8601String(),
}, },
); );
@@ -264,25 +252,22 @@ class DatabaseProvider {
final row = results.first; final row = results.first;
return Geoposition( return Geoposition(
id: int.parse(row[0].toString()), id: int.parse(row[0].toString()),
userId: int.parse(row[1].toString()), xValue: double.parse(row[1].toString()),
xValue: double.parse(row[2].toString()), yValue: double.parse(row[2].toString()),
yValue: double.parse(row[3].toString()), lastUpdate: DateTime.parse(row[3].toString()),
datetime: DateTime.parse(row[4].toString()), expiresAt: DateTime.parse(row[4].toString()),
lifetime: lifetime,
expiresAt: DateTime.parse(row[6].toString()),
); );
} }
Future<Geoposition?> getLatestPosition(int userId) async { Future<Geoposition?> getLatestPosition() async {
final results = await _dbConnection.execute( final results = await _dbConnection.execute(
Sql.named(''' Sql.named('''
SELECT id, user_id, x_value, y_value, datetime, lifetime, expires_at SELECT id, x_value, y_value, last_update, expires_at
FROM geopositions FROM geopositions
WHERE user_id = @userId AND expires_at > NOW() WHERE expires_at > NOW()
ORDER BY datetime DESC ORDER BY last_update DESC
LIMIT 1 LIMIT 1
'''), '''),
parameters: {'userId': userId},
); );
if (results.isEmpty) return null; if (results.isEmpty) return null;
@@ -290,13 +275,10 @@ class DatabaseProvider {
final row = results.first; final row = results.first;
return Geoposition( return Geoposition(
id: int.parse(row[0].toString()), id: int.parse(row[0].toString()),
userId: int.parse(row[1].toString()), xValue: double.parse(row[1].toString()),
xValue: double.parse(row[2].toString()), yValue: double.parse(row[2].toString()),
yValue: double.parse(row[3].toString()), lastUpdate: DateTime.parse(row[3].toString()),
datetime: DateTime.parse(row[4].toString()), expiresAt: DateTime.parse(row[4].toString()),
lifetime: Duration(
seconds: int.tryParse(row[5].toString()) ?? 0),
expiresAt: DateTime.parse(row[6].toString()),
); );
} }
@@ -308,14 +290,14 @@ class DatabaseProvider {
// ==================== Share operations ==================== // ==================== Share operations ====================
String createShareId(int userId) { String createShareId() {
final uniqueId = _uuid.v4(); final uniqueId = _uuid.v4();
_shareLinks[uniqueId] = userId; _shareLinks[uniqueId] = true;
return uniqueId; return uniqueId;
} }
int? getUserIdByShareId(String uniqueId) { bool isValidShareId(String uniqueId) {
return _shareLinks[uniqueId]; return _shareLinks[uniqueId] == true;
} }
// ==================== Log operations ==================== // ==================== Log operations ====================
@@ -349,8 +331,4 @@ class DatabaseProvider {
.toList(); .toList();
} }
String _toInterval(Duration duration) {
final seconds = duration.inSeconds;
return '$seconds seconds';
} }
}
+1 -3
View File
@@ -6,11 +6,9 @@ CREATE TABLE IF NOT EXISTS users (
CREATE TABLE IF NOT EXISTS geopositions ( CREATE TABLE IF NOT EXISTS geopositions (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
x_value DOUBLE PRECISION NOT NULL, x_value DOUBLE PRECISION NOT NULL,
y_value DOUBLE PRECISION NOT NULL, y_value DOUBLE PRECISION NOT NULL,
datetime TIMESTAMP NOT NULL DEFAULT NOW(), last_update TIMESTAMP NOT NULL DEFAULT NOW(),
lifetime INTERVAL NOT NULL,
expires_at TIMESTAMP NOT NULL expires_at TIMESTAMP NOT NULL
); );
+29
View File
@@ -0,0 +1,29 @@
import 'package:shelf/shelf.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dotenv/dotenv.dart';
class AuthMiddleware {
final Handler handler;
AuthMiddleware(this.handler);
Future<Response> call(Request request) async {
final authorization = request.headers['authorization'];
if (authorization == null || !authorization.startsWith('Bearer ')) {
return Response(401, body: 'Authorization header missing or invalid');
}
final token = authorization.substring(7);
try {
final dotenv = DotEnv();
final secret = dotenv['JWT_SECRET'] ?? '';
final decoded = JWT.verify(token, SecretKey(secret));
return handler(request);
} catch (e) {
return Response(401, body: 'Invalid or expired token');
}
}
}
+4 -10
View File
@@ -1,19 +1,15 @@
class Geoposition { class Geoposition {
final int id; final int id;
final int userId;
final double xValue; final double xValue;
final double yValue; final double yValue;
final DateTime datetime; final DateTime lastUpdate;
final Duration lifetime;
final DateTime expiresAt; final DateTime expiresAt;
Geoposition({ Geoposition({
required this.id, required this.id,
required this.userId,
required this.xValue, required this.xValue,
required this.yValue, required this.yValue,
required this.datetime, required this.lastUpdate,
required this.lifetime,
required this.expiresAt, required this.expiresAt,
}); });
@@ -22,7 +18,7 @@ class Geoposition {
'id': id, 'id': id,
'x': xValue, 'x': xValue,
'y': yValue, 'y': yValue,
'datetime': datetime.toIso8601String(), 'lastUpdate': lastUpdate.toIso8601String(),
'remainingSeconds': expiresAt.difference(DateTime.now()).inSeconds, 'remainingSeconds': expiresAt.difference(DateTime.now()).inSeconds,
}; };
} }
@@ -30,11 +26,9 @@ class Geoposition {
factory Geoposition.fromMap(Map<String, dynamic> map) { factory Geoposition.fromMap(Map<String, dynamic> map) {
return Geoposition( return Geoposition(
id: map['id'], id: map['id'],
userId: map['user_id'],
xValue: map['x_value'].toDouble(), xValue: map['x_value'].toDouble(),
yValue: map['y_value'].toDouble(), yValue: map['y_value'].toDouble(),
datetime: map['datetime'] as DateTime, lastUpdate: map['last_update'] as DateTime,
lifetime: Duration(seconds: map['lifetime']),
expiresAt: map['expires_at'] as DateTime, expiresAt: map['expires_at'] as DateTime,
); );
} }
+31 -6
View File
@@ -1,7 +1,10 @@
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart'; import 'package:shelf_router/shelf_router.dart';
import 'package:bcrypt/bcrypt.dart'; import 'package:bcrypt/bcrypt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dotenv/dotenv.dart';
import '../database/database_provider.dart'; import '../database/database_provider.dart';
import '../middleware/auth_middleware.dart';
import 'dart:convert'; import 'dart:convert';
class AuthRoutes { class AuthRoutes {
@@ -12,7 +15,8 @@ class AuthRoutes {
Router get routes { Router get routes {
final router = Router(); final router = Router();
router.post('/login', _login); router.post('/login', _login);
router.get('/watch', _watch); router.post('/reg', _register);
router.get('/watch', AuthMiddleware(_watch).call);
return router; return router;
} }
@@ -29,19 +33,40 @@ class AuthRoutes {
return Response(401, body: 'Invalid credentials'); return Response(401, body: 'Invalid credentials');
} }
return Response(200, body: jsonEncode({'user': user.toMap()})); // Генерация JWT токена
final dotenv = DotEnv();
final secret = dotenv['JWT_SECRET'] ?? '';
final jwt = JWT(
{'user_id': user.id, 'login': user.login},
issuer: 'family_safety_tracker'
);
final token = jwt.sign(SecretKey(secret));
return Response(200, body: jsonEncode({
'user': user.toMap(),
'token': token
}));
}
Future<Response> _register(Request request) async {
final body = await request.readAsString();
final data = jsonDecode(body);
final login = data['login'];
final password = data['password'];
final user = await database.createUser(login, password);
return Response(201, body: jsonEncode(user.toMap()));
} }
Future<Response> _watch(Request request) async { Future<Response> _watch(Request request) async {
final uniqueId = request.url.queryParameters['unique_id']; final uniqueId = request.url.queryParameters['unique_id'];
final userId = database.getUserIdByShareId(uniqueId!); if (!database.isValidShareId(uniqueId!)) {
if (userId == null) {
return Response(404, body: 'Share link not found'); return Response(404, body: 'Share link not found');
} }
final position = await database.getLatestPosition(userId); final position = await database.getLatestPosition();
if (position == null) { if (position == null) {
return Response(404, body: 'No position available'); return Response(404, body: 'No position available');
+7 -16
View File
@@ -1,6 +1,7 @@
import 'package:shelf/shelf.dart'; 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 'dart:convert'; import 'dart:convert';
class GeoRoutes { class GeoRoutes {
@@ -10,9 +11,9 @@ class GeoRoutes {
Router get routes { Router get routes {
final router = Router(); final router = Router();
router.post('/geo', _createPosition); router.post('/geo', AuthMiddleware(_createPosition).call);
router.put('/geo', _updatePosition); router.put('/geo', AuthMiddleware(_updatePosition).call);
router.post('/share', _createShare); router.post('/share', AuthMiddleware(_createShare).call);
return router; return router;
} }
@@ -20,13 +21,10 @@ class GeoRoutes {
final body = await request.readAsString(); final body = await request.readAsString();
final data = jsonDecode(body); final data = jsonDecode(body);
final userId = data['user_id'];
final x = data['x']; final x = data['x'];
final y = data['y']; final y = data['y'];
final lifetimeSeconds = data['lifetime'];
final lifetime = Duration(seconds: lifetimeSeconds);
final position = await database.createPosition(userId, x, y, lifetime); final position = await database.createPosition(x, y);
return Response(201, body: position.toJson()); return Response(201, body: position.toJson());
} }
@@ -34,22 +32,15 @@ class GeoRoutes {
final body = await request.readAsString(); final body = await request.readAsString();
final data = jsonDecode(body); final data = jsonDecode(body);
final userId = data['user_id'];
final x = data['x']; final x = data['x'];
final y = data['y']; final y = data['y'];
final lifetimeSeconds = data['lifetime'];
final lifetime = Duration(seconds: lifetimeSeconds);
final position = await database.updatePosition(userId, x, y, lifetime); final position = await database.updatePosition(x, y);
return Response(200, body: position.toJson()); return Response(200, body: position.toJson());
} }
Future<Response> _createShare(Request request) async { Future<Response> _createShare(Request request) async {
final body = await request.readAsString(); final shareId = database.createShareId();
final data = jsonDecode(body);
final userId = data['user_id'];
final shareId = database.createShareId(userId);
return Response(200, body: jsonEncode({'share_id': shareId})); return Response(200, body: jsonEncode({'share_id': shareId}));
} }
-54
View File
@@ -1,54 +0,0 @@
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../database/database_provider.dart';
import 'dart:convert';
class UserRoutes {
final DatabaseProvider database;
UserRoutes(this.database);
Router get routes {
final router = Router();
router.get('/user', _getAllUsers);
router.post('/user', _createUser);
router.put('/user/<id>', _updateUser);
router.delete('/user/<id>', _deleteUser);
return router;
}
Future<Response> _getAllUsers(Request request) async {
final users = await database.getAllUsers();
return Response(200, body: jsonEncode(users.map((u) => u.toMap()).toList()));
}
Future<Response> _createUser(Request request) async {
final body = await request.readAsString();
final data = jsonDecode(body);
final login = data['login'];
final password = data['password'];
final user = await database.createUser(login, password);
return Response(201, body: jsonEncode(user.toMap()));
}
Future<Response> _updateUser(Request request) async {
final id = int.parse(request.params['id']!);
final body = await request.readAsString();
final data = jsonDecode(body);
final login = data['login'];
final password = data['password'];
final user = await database.updateUser(id, login, password);
return Response(200, body: jsonEncode(user.toMap()));
}
Future<Response> _deleteUser(Request request) async {
final id = int.parse(request.params['id']!);
await database.deleteUser(id);
return Response(204);
}
}
-3
View File
@@ -7,7 +7,6 @@ import 'package:shelf_router/shelf_router.dart';
import 'database/database_provider.dart'; import 'database/database_provider.dart';
import 'routes/auth_routes.dart'; import 'routes/auth_routes.dart';
import 'routes/user_routes.dart';
import 'routes/geo_routes.dart'; import 'routes/geo_routes.dart';
void main(List<String> args) async { void main(List<String> args) async {
@@ -19,12 +18,10 @@ void main(List<String> args) async {
}); });
final authRoutes = AuthRoutes(database); final authRoutes = AuthRoutes(database);
final userRoutes = UserRoutes(database);
final geoRoutes = GeoRoutes(database); final geoRoutes = GeoRoutes(database);
final router = Router() final router = Router()
..mount('', authRoutes.routes.call) ..mount('', authRoutes.routes.call)
..mount('', userRoutes.routes.call)
..mount('', geoRoutes.routes.call) ..mount('', geoRoutes.routes.call)
..get('/', (Request req) => Response.ok('Family Safety Tracker API\n')); ..get('/', (Request req) => Response.ok('Family Safety Tracker API\n'));