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 _shareLinks = {}; final _uuid = const Uuid(); Future 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, x_value DOUBLE PRECISION NOT NULL, y_value DOUBLE PRECISION NOT NULL, last_update TIMESTAMP NOT NULL DEFAULT NOW(), 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 close() async { await _dbConnection.close(); } // ==================== User operations ==================== Future 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 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 deleteUser(int id) async { await _dbConnection.execute( Sql.named('DELETE FROM users WHERE id = @id'), parameters: {'id': id}, ); } Future 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> 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 createPosition( double x, double y, ) async { final expiresAt = DateTime.now().add(const Duration(hours: 24)); final results = await _dbConnection.execute( Sql.named(''' 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: { 'xValue': x, 'yValue': y, 'expiresAt': expiresAt.toIso8601String(), }, ); final row = results.first; return Geoposition( id: int.parse(row[0].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 id, double x, double y, ) async { final expiresAt = DateTime.now().add(const Duration(hours: 24)); final results = await _dbConnection.execute( Sql.named(''' UPDATE geopositions SET x_value = @xValue, y_value = @yValue, last_update = NOW(), expires_at = @expiresAt WHERE id = @id RETURNING id, x_value, y_value, last_update, expires_at '''), parameters: { 'id': id, 'xValue': x, 'yValue': y, 'expiresAt': expiresAt.toIso8601String(), }, ); final row = results.first; return Geoposition( id: int.parse(row[0].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() async { final results = await _dbConnection.execute( Sql.named(''' SELECT id, x_value, y_value, last_update, expires_at FROM geopositions WHERE expires_at > NOW() ORDER BY last_update DESC LIMIT 1 '''), ); if (results.isEmpty) return null; final row = results.first; return Geoposition( id: int.parse(row[0].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 cleanupExpired() async { await _dbConnection.execute( Sql.named('DELETE FROM geopositions WHERE expires_at < NOW()'), ); } // ==================== Share operations ==================== String createShareId() { final uniqueId = _uuid.v4(); _shareLinks[uniqueId] = true; return uniqueId; } bool isValidShareId(String uniqueId) { return _shareLinks[uniqueId] == true; } // ==================== Log operations ==================== Future 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> 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(); } }