diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/bin/database/database_provider.dart b/bin/database/database_provider.dart index d634746..86bda3c 100644 --- a/bin/database/database_provider.dart +++ b/bin/database/database_provider.dart @@ -12,7 +12,7 @@ import '../models/log.dart'; class DatabaseProvider { late Connection _dbConnection; - final Map _shareLinks = {}; + final Map _shareLinks = {}; final _uuid = const Uuid(); Future 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 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 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 getLatestPosition(int userId) async { + Future 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'; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/bin/database/migration.sql b/bin/database/migration.sql index 240ce06..a7740d9 100644 --- a/bin/database/migration.sql +++ b/bin/database/migration.sql @@ -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 ); diff --git a/bin/middleware/auth_middleware.dart b/bin/middleware/auth_middleware.dart new file mode 100644 index 0000000..92d4576 --- /dev/null +++ b/bin/middleware/auth_middleware.dart @@ -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 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'); + } + } +} \ No newline at end of file diff --git a/bin/models/geoposition.dart b/bin/models/geoposition.dart index a9e08b3..396b9c6 100644 --- a/bin/models/geoposition.dart +++ b/bin/models/geoposition.dart @@ -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 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, ); } diff --git a/bin/routes/auth_routes.dart b/bin/routes/auth_routes.dart index 4ce4420..5e7235f 100644 --- a/bin/routes/auth_routes.dart +++ b/bin/routes/auth_routes.dart @@ -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 _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 _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'); diff --git a/bin/routes/geo_routes.dart b/bin/routes/geo_routes.dart index 0f3d808..ddd8337 100644 --- a/bin/routes/geo_routes.dart +++ b/bin/routes/geo_routes.dart @@ -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 _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})); } diff --git a/bin/routes/user_routes.dart b/bin/routes/user_routes.dart deleted file mode 100644 index 0ca124a..0000000 --- a/bin/routes/user_routes.dart +++ /dev/null @@ -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/', _updateUser); - router.delete('/user/', _deleteUser); - return router; - } - - Future _getAllUsers(Request request) async { - final users = await database.getAllUsers(); - return Response(200, body: jsonEncode(users.map((u) => u.toMap()).toList())); - } - - Future _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 _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 _deleteUser(Request request) async { - final id = int.parse(request.params['id']!); - await database.deleteUser(id); - return Response(204); - } -} \ No newline at end of file diff --git a/bin/server.dart b/bin/server.dart index 14ab860..cb22ad1 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -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 args) async { @@ -19,12 +18,10 @@ void main(List 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'));