358 lines
9.6 KiB
Dart
358 lines
9.6 KiB
Dart
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, String> _shareLinks = {};
|
|
final _uuid = const Uuid();
|
|
|
|
Future<void> initialize() async {
|
|
final dotenv = DotEnv();
|
|
|
|
final host = dotenv['POSTGRES_HOST'] ?? 'db';
|
|
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'] ?? 'postgres';
|
|
|
|
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('CREATE DATABASE $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 UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
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<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(
|
|
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: 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<Geoposition> updatePosition(
|
|
String 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: 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<Geoposition?> 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: 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<Geoposition?> getPositionById(String id) async {
|
|
final results = await _dbConnection.execute(
|
|
Sql.named('''
|
|
SELECT id, x_value, y_value, last_update, expires_at
|
|
FROM geopositions
|
|
WHERE id = @id AND expires_at > NOW()
|
|
'''),
|
|
parameters: {'id': id},
|
|
);
|
|
|
|
if (results.isEmpty) return null;
|
|
|
|
final row = results.first;
|
|
return Geoposition(
|
|
id: 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<void> cleanupExpired() async {
|
|
await _dbConnection.execute(
|
|
Sql.named('DELETE FROM geopositions WHERE expires_at < NOW()'),
|
|
);
|
|
}
|
|
|
|
// ==================== Share operations ====================
|
|
|
|
String createShareId(String geoId) {
|
|
final uniqueId = _uuid.v4();
|
|
_shareLinks[uniqueId] = geoId;
|
|
return uniqueId;
|
|
}
|
|
|
|
String? getGeoIdByShareId(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();
|
|
}
|
|
|
|
} |