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
+31 -53
View File
@@ -12,7 +12,7 @@ import '../models/log.dart';
class DatabaseProvider {
late Connection _dbConnection;
final Map<String, int> _shareLinks = {};
final Map<String, bool> _shareLinks = {};
final _uuid = const Uuid();
Future<void> initialize() async {
@@ -79,11 +79,9 @@ class DatabaseProvider {
Sql.named('''
CREATE TABLE IF NOT EXISTS geopositions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
x_value DOUBLE PRECISION NOT NULL,
y_value DOUBLE PRECISION NOT NULL,
datetime TIMESTAMP NOT NULL DEFAULT NOW(),
lifetime INTERVAL NOT NULL,
last_update TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL
)
'''),
@@ -204,24 +202,20 @@ class DatabaseProvider {
// ==================== Geoposition operations ====================
Future<Geoposition> createPosition(
int userId,
double x,
double y,
Duration lifetime,
) async {
final expiresAt = DateTime.now().add(lifetime);
final expiresAt = DateTime.now().add(const Duration(hours: 24));
final results = await _dbConnection.execute(
Sql.named('''
INSERT INTO geopositions (user_id, x_value, y_value, datetime, lifetime, expires_at)
VALUES (@userId, @xValue, @yValue, NOW(), @lifetime, @expiresAt)
RETURNING id, user_id, x_value, y_value, datetime, lifetime, expires_at
INSERT INTO geopositions (x_value, y_value, last_update, expires_at)
VALUES (@xValue, @yValue, NOW(), @expiresAt)
RETURNING id, x_value, y_value, last_update, expires_at
'''),
parameters: {
'userId': userId,
'xValue': x,
'yValue': y,
'lifetime': _toInterval(lifetime),
'expiresAt': expiresAt.toIso8601String(),
},
);
@@ -229,34 +223,28 @@ class DatabaseProvider {
final row = results.first;
return Geoposition(
id: int.parse(row[0].toString()),
userId: int.parse(row[1].toString()),
xValue: double.parse(row[2].toString()),
yValue: double.parse(row[3].toString()),
datetime: DateTime.parse(row[4].toString()),
lifetime: lifetime,
expiresAt: DateTime.parse(row[6].toString()),
xValue: double.parse(row[1].toString()),
yValue: double.parse(row[2].toString()),
lastUpdate: DateTime.parse(row[3].toString()),
expiresAt: DateTime.parse(row[4].toString()),
);
}
Future<Geoposition> updatePosition(
int userId,
double x,
double y,
Duration lifetime,
) async {
final expiresAt = DateTime.now().add(lifetime);
final expiresAt = DateTime.now().add(const Duration(hours: 24));
final results = await _dbConnection.execute(
Sql.named('''
INSERT INTO geopositions (user_id, x_value, y_value, datetime, lifetime, expires_at)
VALUES (@userId, @xValue, @yValue, NOW(), @lifetime, @expiresAt)
RETURNING id, user_id, x_value, y_value, datetime, lifetime, expires_at
INSERT INTO geopositions (x_value, y_value, last_update, expires_at)
VALUES (@xValue, @yValue, NOW(), @expiresAt)
RETURNING id, x_value, y_value, last_update, expires_at
'''),
parameters: {
'userId': userId,
'xValue': x,
'yValue': y,
'lifetime': _toInterval(lifetime),
'expiresAt': expiresAt.toIso8601String(),
},
);
@@ -264,25 +252,22 @@ class DatabaseProvider {
final row = results.first;
return Geoposition(
id: int.parse(row[0].toString()),
userId: int.parse(row[1].toString()),
xValue: double.parse(row[2].toString()),
yValue: double.parse(row[3].toString()),
datetime: DateTime.parse(row[4].toString()),
lifetime: lifetime,
expiresAt: DateTime.parse(row[6].toString()),
xValue: double.parse(row[1].toString()),
yValue: double.parse(row[2].toString()),
lastUpdate: DateTime.parse(row[3].toString()),
expiresAt: DateTime.parse(row[4].toString()),
);
}
Future<Geoposition?> getLatestPosition(int userId) async {
Future<Geoposition?> getLatestPosition() async {
final results = await _dbConnection.execute(
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
WHERE user_id = @userId AND expires_at > NOW()
ORDER BY datetime DESC
WHERE expires_at > NOW()
ORDER BY last_update DESC
LIMIT 1
'''),
parameters: {'userId': userId},
);
if (results.isEmpty) return null;
@@ -290,13 +275,10 @@ class DatabaseProvider {
final row = results.first;
return Geoposition(
id: int.parse(row[0].toString()),
userId: int.parse(row[1].toString()),
xValue: double.parse(row[2].toString()),
yValue: double.parse(row[3].toString()),
datetime: DateTime.parse(row[4].toString()),
lifetime: Duration(
seconds: int.tryParse(row[5].toString()) ?? 0),
expiresAt: DateTime.parse(row[6].toString()),
xValue: double.parse(row[1].toString()),
yValue: double.parse(row[2].toString()),
lastUpdate: DateTime.parse(row[3].toString()),
expiresAt: DateTime.parse(row[4].toString()),
);
}
@@ -308,14 +290,14 @@ class DatabaseProvider {
// ==================== Share operations ====================
String createShareId(int userId) {
String createShareId() {
final uniqueId = _uuid.v4();
_shareLinks[uniqueId] = userId;
_shareLinks[uniqueId] = true;
return uniqueId;
}
int? getUserIdByShareId(String uniqueId) {
return _shareLinks[uniqueId];
bool isValidShareId(String uniqueId) {
return _shareLinks[uniqueId] == true;
}
// ==================== Log operations ====================
@@ -349,8 +331,4 @@ class DatabaseProvider {
.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 (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
x_value DOUBLE PRECISION NOT NULL,
y_value DOUBLE PRECISION NOT NULL,
datetime TIMESTAMP NOT NULL DEFAULT NOW(),
lifetime INTERVAL NOT NULL,
last_update TIMESTAMP NOT NULL DEFAULT NOW(),
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 {
final int id;
final int userId;
final double xValue;
final double yValue;
final DateTime datetime;
final Duration lifetime;
final DateTime lastUpdate;
final DateTime expiresAt;
Geoposition({
required this.id,
required this.userId,
required this.xValue,
required this.yValue,
required this.datetime,
required this.lifetime,
required this.lastUpdate,
required this.expiresAt,
});
@@ -22,7 +18,7 @@ class Geoposition {
'id': id,
'x': xValue,
'y': yValue,
'datetime': datetime.toIso8601String(),
'lastUpdate': lastUpdate.toIso8601String(),
'remainingSeconds': expiresAt.difference(DateTime.now()).inSeconds,
};
}
@@ -30,11 +26,9 @@ class Geoposition {
factory Geoposition.fromMap(Map<String, dynamic> map) {
return Geoposition(
id: map['id'],
userId: map['user_id'],
xValue: map['x_value'].toDouble(),
yValue: map['y_value'].toDouble(),
datetime: map['datetime'] as DateTime,
lifetime: Duration(seconds: map['lifetime']),
lastUpdate: map['last_update'] as DateTime,
expiresAt: map['expires_at'] as DateTime,
);
}
+31 -6
View File
@@ -1,7 +1,10 @@
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:bcrypt/bcrypt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:dotenv/dotenv.dart';
import '../database/database_provider.dart';
import '../middleware/auth_middleware.dart';
import 'dart:convert';
class AuthRoutes {
@@ -12,7 +15,8 @@ class AuthRoutes {
Router get routes {
final router = Router();
router.post('/login', _login);
router.get('/watch', _watch);
router.post('/reg', _register);
router.get('/watch', AuthMiddleware(_watch).call);
return router;
}
@@ -29,19 +33,40 @@ class AuthRoutes {
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 {
final uniqueId = request.url.queryParameters['unique_id'];
final userId = database.getUserIdByShareId(uniqueId!);
if (userId == null) {
if (!database.isValidShareId(uniqueId!)) {
return Response(404, body: 'Share link not found');
}
final position = await database.getLatestPosition(userId);
final position = await database.getLatestPosition();
if (position == null) {
return Response(404, body: 'No position available');
+7 -16
View File
@@ -1,6 +1,7 @@
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../database/database_provider.dart';
import '../middleware/auth_middleware.dart';
import 'dart:convert';
class GeoRoutes {
@@ -10,9 +11,9 @@ class GeoRoutes {
Router get routes {
final router = Router();
router.post('/geo', _createPosition);
router.put('/geo', _updatePosition);
router.post('/share', _createShare);
router.post('/geo', AuthMiddleware(_createPosition).call);
router.put('/geo', AuthMiddleware(_updatePosition).call);
router.post('/share', AuthMiddleware(_createShare).call);
return router;
}
@@ -20,13 +21,10 @@ class GeoRoutes {
final body = await request.readAsString();
final data = jsonDecode(body);
final userId = data['user_id'];
final x = data['x'];
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());
}
@@ -34,22 +32,15 @@ class GeoRoutes {
final body = await request.readAsString();
final data = jsonDecode(body);
final userId = data['user_id'];
final x = data['x'];
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());
}
Future<Response> _createShare(Request request) async {
final body = await request.readAsString();
final data = jsonDecode(body);
final userId = data['user_id'];
final shareId = database.createShareId(userId);
final shareId = database.createShareId();
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 'routes/auth_routes.dart';
import 'routes/user_routes.dart';
import 'routes/geo_routes.dart';
void main(List<String> args) async {
@@ -19,12 +18,10 @@ void main(List<String> args) async {
});
final authRoutes = AuthRoutes(database);
final userRoutes = UserRoutes(database);
final geoRoutes = GeoRoutes(database);
final router = Router()
..mount('', authRoutes.routes.call)
..mount('', userRoutes.routes.call)
..mount('', geoRoutes.routes.call)
..get('/', (Request req) => Response.ok('Family Safety Tracker API\n'));