Refactor database layer: convert to DatabaseProvider class with initialization
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bcrypt/bcrypt.dart';
|
||||
import 'package:dotenv/dotenv.dart';
|
||||
import 'package:postgres/postgres.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../models/user.dart';
|
||||
import '../models/geoposition.dart';
|
||||
import '../models/log.dart';
|
||||
|
||||
class DatabaseProvider {
|
||||
late Connection _dbConnection;
|
||||
|
||||
final Map<String, int> _shareLinks = {};
|
||||
final _uuid = const Uuid();
|
||||
|
||||
Future<void> initialize() async {
|
||||
final dotenv = DotEnv();
|
||||
|
||||
final host = dotenv['POSTGRES_HOST'] ?? 'localhost';
|
||||
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'] ?? '';
|
||||
|
||||
try {
|
||||
final defaultConnection = await Connection.open(
|
||||
settings: ConnectionSettings(sslMode: SslMode.disable),
|
||||
Endpoint(
|
||||
host: host,
|
||||
port: port,
|
||||
database: 'postgres',
|
||||
username: username,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
|
||||
final results = await defaultConnection.execute(
|
||||
Sql.named('SELECT 1 FROM pg_database WHERE datname = @dbName'),
|
||||
parameters: {'dbName': databaseName},
|
||||
);
|
||||
|
||||
if (results.isEmpty) {
|
||||
await defaultConnection.execute(
|
||||
Sql.named('CREATE DATABASE @dbName'),
|
||||
parameters: {'dbName': databaseName},
|
||||
);
|
||||
print('Database $databaseName created.');
|
||||
} else {
|
||||
print('Database $databaseName already exists.');
|
||||
}
|
||||
|
||||
await defaultConnection.close();
|
||||
|
||||
_dbConnection = await Connection.open(
|
||||
settings: ConnectionSettings(sslMode: SslMode.disable),
|
||||
Endpoint(
|
||||
host: host,
|
||||
port: port,
|
||||
database: databaseName,
|
||||
username: username,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
print('Connected to database $databaseName.');
|
||||
|
||||
await _dbConnection.execute(
|
||||
Sql.named('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
login VARCHAR(255) UNIQUE NOT NULL,
|
||||
pwd_hash VARCHAR(255) NOT NULL
|
||||
)
|
||||
'''),
|
||||
);
|
||||
|
||||
await _dbConnection.execute(
|
||||
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,
|
||||
expires_at TIMESTAMP NOT NULL
|
||||
)
|
||||
'''),
|
||||
);
|
||||
|
||||
await _dbConnection.execute(
|
||||
Sql.named('''
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
action VARCHAR(255) NOT NULL,
|
||||
datetime TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
)
|
||||
'''),
|
||||
);
|
||||
|
||||
print('All tables ensured to exist.');
|
||||
} catch (e, stackTrace) {
|
||||
stderr.writeln('Database initialization error: $e');
|
||||
stderr.writeln(stackTrace);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
await _dbConnection.close();
|
||||
}
|
||||
|
||||
// ==================== User operations ====================
|
||||
|
||||
Future<User?> findUserByLogin(String login) async {
|
||||
final results = await _dbConnection.execute(
|
||||
Sql.named('SELECT id, login, pwd_hash FROM users WHERE login = @login'),
|
||||
parameters: {'login': login},
|
||||
);
|
||||
|
||||
if (results.isEmpty) return null;
|
||||
final row = results.first;
|
||||
return User(
|
||||
id: int.parse(row[0].toString()),
|
||||
login: row[1] as String,
|
||||
pwdHash: row[2] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Future<User> createUser(String login, String password) async {
|
||||
final hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt());
|
||||
|
||||
final results = await _dbConnection.execute(
|
||||
Sql.named('''
|
||||
INSERT INTO users (login, pwd_hash) VALUES (@login, @pwdHash)
|
||||
RETURNING id, login, pwd_hash
|
||||
'''),
|
||||
parameters: {
|
||||
'login': login,
|
||||
'pwdHash': hashedPassword,
|
||||
},
|
||||
);
|
||||
|
||||
final row = results.first;
|
||||
return User(
|
||||
id: int.parse(row[0].toString()),
|
||||
login: row[1] as String,
|
||||
pwdHash: row[2] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteUser(int id) async {
|
||||
await _dbConnection.execute(
|
||||
Sql.named('DELETE FROM users WHERE id = @id'),
|
||||
parameters: {'id': id},
|
||||
);
|
||||
}
|
||||
|
||||
Future<User> updateUser(int id, String newLogin, String? newPassword) async {
|
||||
if (newPassword != null) {
|
||||
final hashedPassword = BCrypt.hashpw(newPassword, BCrypt.gensalt());
|
||||
await _dbConnection.execute(
|
||||
Sql.named('''
|
||||
UPDATE users SET login = @login, pwd_hash = @pwdHash WHERE id = @id
|
||||
'''),
|
||||
parameters: {
|
||||
'login': newLogin,
|
||||
'pwdHash': hashedPassword,
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await _dbConnection.execute(
|
||||
Sql.named('UPDATE users SET login = @login WHERE id = @id'),
|
||||
parameters: {
|
||||
'login': newLogin,
|
||||
'id': id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final user = await findUserByLogin(newLogin);
|
||||
return user!;
|
||||
}
|
||||
|
||||
Future<List<User>> getAllUsers() async {
|
||||
final results = await _dbConnection.execute(
|
||||
Sql.named('SELECT id, login, pwd_hash FROM users'),
|
||||
);
|
||||
|
||||
return results
|
||||
.map(
|
||||
(row) => User(
|
||||
id: int.parse(row[0].toString()),
|
||||
login: row[1] as String,
|
||||
pwdHash: row[2] as String,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ==================== Geoposition operations ====================
|
||||
|
||||
Future<Geoposition> createPosition(
|
||||
int userId,
|
||||
double x,
|
||||
double y,
|
||||
Duration lifetime,
|
||||
) async {
|
||||
final expiresAt = DateTime.now().add(lifetime);
|
||||
|
||||
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
|
||||
'''),
|
||||
parameters: {
|
||||
'userId': userId,
|
||||
'xValue': x,
|
||||
'yValue': y,
|
||||
'lifetime': _toInterval(lifetime),
|
||||
'expiresAt': expiresAt.toIso8601String(),
|
||||
},
|
||||
);
|
||||
|
||||
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()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Geoposition> updatePosition(
|
||||
int userId,
|
||||
double x,
|
||||
double y,
|
||||
Duration lifetime,
|
||||
) async {
|
||||
final expiresAt = DateTime.now().add(lifetime);
|
||||
|
||||
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
|
||||
'''),
|
||||
parameters: {
|
||||
'userId': userId,
|
||||
'xValue': x,
|
||||
'yValue': y,
|
||||
'lifetime': _toInterval(lifetime),
|
||||
'expiresAt': expiresAt.toIso8601String(),
|
||||
},
|
||||
);
|
||||
|
||||
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()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Geoposition?> getLatestPosition(int userId) async {
|
||||
final results = await _dbConnection.execute(
|
||||
Sql.named('''
|
||||
SELECT id, user_id, x_value, y_value, datetime, lifetime, expires_at
|
||||
FROM geopositions
|
||||
WHERE user_id = @userId AND expires_at > NOW()
|
||||
ORDER BY datetime DESC
|
||||
LIMIT 1
|
||||
'''),
|
||||
parameters: {'userId': userId},
|
||||
);
|
||||
|
||||
if (results.isEmpty) return null;
|
||||
|
||||
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()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> cleanupExpired() async {
|
||||
await _dbConnection.execute(
|
||||
Sql.named('DELETE FROM geopositions WHERE expires_at < NOW()'),
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Share operations ====================
|
||||
|
||||
String createShareId(int userId) {
|
||||
final uniqueId = _uuid.v4();
|
||||
_shareLinks[uniqueId] = userId;
|
||||
return uniqueId;
|
||||
}
|
||||
|
||||
int? getUserIdByShareId(String uniqueId) {
|
||||
return _shareLinks[uniqueId];
|
||||
}
|
||||
|
||||
// ==================== Log operations ====================
|
||||
|
||||
Future<void> createLog(String username, String action) async {
|
||||
await _dbConnection.execute(
|
||||
Sql.named('''
|
||||
INSERT INTO logs (username, action) VALUES (@username, @action)
|
||||
'''),
|
||||
parameters: {
|
||||
'username': username,
|
||||
'action': action,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Log>> getAllLogs() async {
|
||||
final results = await _dbConnection.execute(
|
||||
Sql.named('SELECT id, username, action, datetime FROM logs'),
|
||||
);
|
||||
|
||||
return results
|
||||
.map(
|
||||
(row) => Log(
|
||||
id: int.parse(row[0].toString()),
|
||||
username: row[1] as String,
|
||||
action: row[2] as String,
|
||||
datetime: DateTime.parse(row[3].toString()),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
String _toInterval(Duration duration) {
|
||||
final seconds = duration.inSeconds;
|
||||
return '$seconds seconds';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
login VARCHAR(255) UNIQUE NOT NULL,
|
||||
pwd_hash VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
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,
|
||||
expires_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
action VARCHAR(255) NOT NULL,
|
||||
datetime TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
class Geoposition {
|
||||
final int id;
|
||||
final int userId;
|
||||
final double xValue;
|
||||
final double yValue;
|
||||
final DateTime datetime;
|
||||
final Duration lifetime;
|
||||
final DateTime expiresAt;
|
||||
|
||||
Geoposition({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.xValue,
|
||||
required this.yValue,
|
||||
required this.datetime,
|
||||
required this.lifetime,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'x': xValue,
|
||||
'y': yValue,
|
||||
'datetime': datetime.toIso8601String(),
|
||||
'remainingSeconds': expiresAt.difference(DateTime.now()).inSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
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']),
|
||||
expiresAt: map['expires_at'] as DateTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
class Log {
|
||||
final int id;
|
||||
final String username;
|
||||
final String action;
|
||||
final DateTime datetime;
|
||||
|
||||
Log({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.action,
|
||||
required this.datetime,
|
||||
});
|
||||
|
||||
factory Log.fromMap(Map<String, dynamic> map) {
|
||||
return Log(
|
||||
id: map['id'],
|
||||
username: map['username'],
|
||||
action: map['action'],
|
||||
datetime: map['datetime'] as DateTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
class User {
|
||||
final int id;
|
||||
final String login;
|
||||
final String pwdHash;
|
||||
|
||||
User({
|
||||
required this.id,
|
||||
required this.login,
|
||||
required this.pwdHash,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'login': login,
|
||||
};
|
||||
}
|
||||
|
||||
factory User.fromMap(Map<String, dynamic> map) {
|
||||
return User(
|
||||
id: map['id'],
|
||||
login: map['login'],
|
||||
pwdHash: map['pwd_hash'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
import 'package:bcrypt/bcrypt.dart';
|
||||
import '../database/database_provider.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class AuthRoutes {
|
||||
final DatabaseProvider database;
|
||||
|
||||
AuthRoutes(this.database);
|
||||
|
||||
Router get routes {
|
||||
final router = Router();
|
||||
router.post('/login', _login);
|
||||
router.get('/watch', _watch);
|
||||
return router;
|
||||
}
|
||||
|
||||
Future<Response> _login(Request request) async {
|
||||
final body = await request.readAsString();
|
||||
final data = jsonDecode(body);
|
||||
|
||||
final login = data['login'];
|
||||
final password = data['password'];
|
||||
|
||||
final user = await database.findUserByLogin(login);
|
||||
|
||||
if (user == null || !BCrypt.checkpw(password, user.pwdHash)) {
|
||||
return Response(401, body: 'Invalid credentials');
|
||||
}
|
||||
|
||||
return Response(200, body: jsonEncode({'user': user.toMap()}));
|
||||
}
|
||||
|
||||
Future<Response> _watch(Request request) async {
|
||||
final uniqueId = request.url.queryParameters['unique_id'];
|
||||
|
||||
final userId = database.getUserIdByShareId(uniqueId!);
|
||||
|
||||
if (userId == null) {
|
||||
return Response(404, body: 'Share link not found');
|
||||
}
|
||||
|
||||
final position = await database.getLatestPosition(userId);
|
||||
|
||||
if (position == null) {
|
||||
return Response(404, body: 'No position available');
|
||||
}
|
||||
|
||||
return Response(200, body: position.toJson());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
import '../database/database_provider.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
class GeoRoutes {
|
||||
final DatabaseProvider database;
|
||||
|
||||
GeoRoutes(this.database);
|
||||
|
||||
Router get routes {
|
||||
final router = Router();
|
||||
router.post('/geo', _createPosition);
|
||||
router.put('/geo', _updatePosition);
|
||||
router.post('/share', _createShare);
|
||||
return router;
|
||||
}
|
||||
|
||||
Future<Response> _createPosition(Request request) async {
|
||||
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);
|
||||
return Response(201, body: position.toJson());
|
||||
}
|
||||
|
||||
Future<Response> _updatePosition(Request request) async {
|
||||
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);
|
||||
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);
|
||||
|
||||
return Response(200, body: jsonEncode({'share_id': shareId}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
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 {
|
||||
final database = DatabaseProvider();
|
||||
await database.initialize();
|
||||
|
||||
Timer.periodic(const Duration(minutes: 5), (timer) {
|
||||
database.cleanupExpired();
|
||||
});
|
||||
|
||||
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'));
|
||||
|
||||
final handler = Pipeline()
|
||||
.addMiddleware(logRequests())
|
||||
.addHandler(router.call);
|
||||
|
||||
final ip = InternetAddress.anyIPv4;
|
||||
final port = int.parse(Platform.environment['PORT'] ?? '8080');
|
||||
final server = await serve(handler, ip, port);
|
||||
print('Server listening on port ${server.port}');
|
||||
}
|
||||
Reference in New Issue
Block a user