| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- using System.Data;
- using System.Security.Cryptography;
- using Npgsql;
- namespace AutorentWinForms;
- public sealed record UserSession(int UserId, string Login, string FullName, string Role);
- public sealed class Database : IDisposable
- {
- private readonly string _connectionString;
- private UserSession? _session;
- public Database(string connectionString)
- {
- _connectionString = connectionString;
- }
- public void Initialize()
- {
- ExecuteNonQuery(SchemaSql);
- Seed();
- SyncSequences();
- }
- public void SetSession(UserSession session)
- {
- _session = session;
- ExecuteNonQuery(
- """
- INSERT INTO app_context(key, value) VALUES('current_user', @value)
- ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value
- """,
- new() { ["@value"] = session.Login });
- }
- public UserSession? Authenticate(string login, string password)
- {
- return login.Trim() switch
- {
- "garanin_gi" when password == "6qw#6xUyG%#" =>
- new UserSession(1, "garanin_gi", "Администратор PostgreSQL", "admin"),
- "manager" when password == "Manager123!" =>
- new UserSession(2, "manager", "Менеджер проката", "manager"),
- "shagavaliev_iv" when password == "75&n057$380" =>
- new UserSession(3, "shagavaliev_iv", "Пользователь просмотра", "viewer"),
- _ => null
- };
- }
- public DataTable Query(string sql, Dictionary<string, object?>? parameters = null)
- {
- using var connection = OpenConnection();
- using var command = new NpgsqlCommand(sql, connection);
- AddParameters(command, parameters);
- using var reader = command.ExecuteReader();
- var table = new DataTable();
- table.Load(reader);
- return table;
- }
- public int ExecuteNonQuery(string sql, Dictionary<string, object?>? parameters = null)
- {
- using var connection = OpenConnection();
- using var command = new NpgsqlCommand(sql, connection);
- AddParameters(command, parameters);
- return command.ExecuteNonQuery();
- }
- public int ExecuteScalarInt(string sql, Dictionary<string, object?>? parameters = null)
- {
- using var connection = OpenConnection();
- using var command = new NpgsqlCommand(sql, connection);
- AddParameters(command, parameters);
- var value = command.ExecuteScalar();
- return value is null or DBNull ? 0 : Convert.ToInt32(value);
- }
- public void Audit(string action, string entity, int? entityId, string details)
- {
- ExecuteNonQuery(
- """
- INSERT INTO audit_log(user_login, action, entity, entity_id, details)
- VALUES(@u, @a, @e, @id, @d)
- """,
- new()
- {
- ["@u"] = _session?.Login ?? "system",
- ["@a"] = action,
- ["@e"] = entity,
- ["@id"] = entityId,
- ["@d"] = details
- });
- }
- public static string HashPassword(string password, string saltHex)
- {
- var salt = Convert.FromHexString(saltHex);
- var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, 120_000, HashAlgorithmName.SHA256, 32);
- return Convert.ToHexString(hash);
- }
- public static string NewSalt()
- {
- Span<byte> salt = stackalloc byte[16];
- RandomNumberGenerator.Fill(salt);
- return Convert.ToHexString(salt);
- }
- private NpgsqlConnection OpenConnection()
- {
- var connection = new NpgsqlConnection(_connectionString);
- connection.Open();
- return connection;
- }
- private static void AddParameters(NpgsqlCommand command, Dictionary<string, object?>? parameters)
- {
- if (parameters is null) return;
- foreach (var (name, value) in parameters)
- {
- var parameterName = name.StartsWith('@') ? name : "@" + name;
- command.Parameters.AddWithValue(parameterName, value ?? DBNull.Value);
- }
- }
- private void Seed()
- {
- if (ExecuteScalarInt("SELECT COUNT(*) FROM brand") == 0)
- {
- ExecuteNonQuery("""
- INSERT INTO brand(name,country) VALUES
- ('Toyota','Япония'),('BMW','Германия'),('Lada','Россия'),('Hyundai','Корея'),
- ('Mercedes-Benz','Германия'),('Kia','Корея'),('Ford','США'),('Skoda','Чехия');
- INSERT INTO car(license_plate,brand_id,year,daily_rate,status) VALUES
- ('А123БВ77',1,2021,3500,'Свободен'),('В456ГД77',2,2020,5500,'В аренде'),
- ('Е789ЖЗ99',3,2019,1800,'Свободен'),('К321ЛМ50',4,2022,2900,'На ТО'),
- ('М001НО77',5,2023,7200,'Свободен'),('Р555СТ77',6,2021,2600,'Свободен'),
- ('У888ФХ50',1,2020,3200,'В аренде'),('Х111ЦЧ77',7,2018,3000,'Свободен'),
- ('Ш222ЩЭ99',8,2022,2400,'На ТО'),('Ю333ЯА77',4,2023,3100,'Свободен');
- INSERT INTO client(full_name,phone,address,license_no) VALUES
- ('Иванов Алексей Петрович','+7(916)123-45-67','Москва, ул. Ленина, 10','77АА123456'),
- ('Смирнова Елена Игоревна','+7(903)987-65-43','Москва, пр. Мира, 55','77ББ654321'),
- ('Козлов Дмитрий Владимирович','+7(925)555-11-22','Мытищи, ул. Садовая, 3','50ВВ112233'),
- ('Новикова Анна Сергеевна','+7(499)321-00-11','Москва, ул. Тверская, 22','77ГГ445566'),
- ('Фёдоров Игорь Николаевич','+7(926)777-88-99','Люберцы, ул. Парковая, 7','50ДД778899'),
- ('Белова Марина Андреевна','+7(915)100-20-30','Москва, ул. Арбат, 15','77ЕЕ001122'),
- ('Тимофеев Сергей Юрьевич','+7(977)333-44-55','Химки, ул. Речная, 21','50ЖЖ334455'),
- ('Орлова Юлия Викторовна','+7(968)666-77-88','Москва, пр. Вернадского, 88','77ЗЗ667788');
- INSERT INTO rental(car_id,client_id,start_date,end_date,actual_return,total_cost,status) VALUES
- (2,1,'2025-04-01','2025-04-07','2025-04-07',33000,'Завершена'),
- (1,2,'2025-04-15','2025-04-20','2025-04-20',17500,'Завершена'),
- (3,3,'2025-04-10','2025-04-15','2025-04-15',9000,'Завершена'),
- (2,4,'2025-05-01','2025-05-07',NULL,38500,'Активна'),
- (7,5,'2025-05-10','2025-05-17',NULL,22400,'Активна'),
- (5,1,'2025-03-20','2025-03-25','2025-03-25',36000,'Завершена'),
- (6,6,'2025-04-22','2025-04-28','2025-04-28',15600,'Завершена'),
- (8,7,'2025-05-12','2025-05-15',NULL,9000,'Активна'),
- (1,8,'2025-02-01','2025-02-05','2025-02-06',17500,'Завершена'),
- (3,2,'2025-05-18','2025-05-23',NULL,9000,'Активна');
- """);
- }
- if (ExecuteScalarInt("SELECT COUNT(*) FROM app_user") == 0)
- {
- CreateUser("admin", "Администратор БД", "admin", "Admin123!");
- CreateUser("manager", "Менеджер проката", "manager", "Manager123!");
- CreateUser("viewer", "Аналитик", "viewer", "Viewer123!");
- }
- }
- private void SyncSequences()
- {
- ExecuteNonQuery("""
- SELECT setval(pg_get_serial_sequence('brand', 'brand_id'), COALESCE((SELECT MAX(brand_id) FROM brand), 1), true)
- WHERE pg_get_serial_sequence('brand', 'brand_id') IS NOT NULL;
- SELECT setval(pg_get_serial_sequence('car', 'car_id'), COALESCE((SELECT MAX(car_id) FROM car), 1), true)
- WHERE pg_get_serial_sequence('car', 'car_id') IS NOT NULL;
- SELECT setval(pg_get_serial_sequence('client', 'client_id'), COALESCE((SELECT MAX(client_id) FROM client), 1), true)
- WHERE pg_get_serial_sequence('client', 'client_id') IS NOT NULL;
- SELECT setval(pg_get_serial_sequence('rental', 'rental_id'), COALESCE((SELECT MAX(rental_id) FROM rental), 1), true)
- WHERE pg_get_serial_sequence('rental', 'rental_id') IS NOT NULL;
- SELECT setval(pg_get_serial_sequence('app_user', 'user_id'), COALESCE((SELECT MAX(user_id) FROM app_user), 1), true)
- WHERE pg_get_serial_sequence('app_user', 'user_id') IS NOT NULL;
- """);
- }
- private void CreateUser(string login, string fullName, string role, string password)
- {
- var salt = NewSalt();
- ExecuteNonQuery(
- """
- INSERT INTO app_user(login, full_name, role, password_hash, password_salt)
- VALUES(@l, @n, @r, @h, @s)
- """,
- new()
- {
- ["@l"] = login,
- ["@n"] = fullName,
- ["@r"] = role,
- ["@h"] = HashPassword(password, salt),
- ["@s"] = salt
- });
- }
- public void Dispose()
- {
- }
- private const string SchemaSql = """
- SET search_path TO garanin_gi, public;
- CREATE TABLE IF NOT EXISTS app_context(
- key TEXT PRIMARY KEY,
- value TEXT NOT NULL
- );
- INSERT INTO app_context(key,value) VALUES('current_user','system')
- ON CONFLICT(key) DO NOTHING;
- CREATE TABLE IF NOT EXISTS brand(
- brand_id SERIAL PRIMARY KEY,
- name VARCHAR(50) NOT NULL UNIQUE,
- country VARCHAR(50)
- );
- CREATE TABLE IF NOT EXISTS car(
- car_id SERIAL PRIMARY KEY,
- license_plate VARCHAR(15) NOT NULL UNIQUE,
- brand_id INT NOT NULL REFERENCES brand(brand_id) ON UPDATE CASCADE ON DELETE RESTRICT,
- year SMALLINT CHECK(year BETWEEN 1990 AND 2030),
- daily_rate NUMERIC(10,2) NOT NULL CHECK(daily_rate > 0),
- status VARCHAR(20) NOT NULL DEFAULT 'Свободен' CHECK(status IN ('Свободен','В аренде','На ТО'))
- );
- CREATE TABLE IF NOT EXISTS client(
- client_id SERIAL PRIMARY KEY,
- full_name VARCHAR(100) NOT NULL,
- phone VARCHAR(20) UNIQUE,
- address TEXT,
- license_no VARCHAR(20) UNIQUE,
- created_at TIMESTAMPTZ NOT NULL DEFAULT now()
- );
- CREATE TABLE IF NOT EXISTS rental(
- rental_id SERIAL PRIMARY KEY,
- car_id INT NOT NULL REFERENCES car(car_id) ON UPDATE CASCADE ON DELETE RESTRICT,
- client_id INT NOT NULL REFERENCES client(client_id) ON UPDATE CASCADE ON DELETE RESTRICT,
- start_date DATE NOT NULL,
- end_date DATE NOT NULL CHECK(end_date >= start_date),
- actual_return DATE,
- total_cost NUMERIC(10,2) CHECK(total_cost >= 0),
- status VARCHAR(20) NOT NULL DEFAULT 'Активна' CHECK(status IN ('Активна','Завершена'))
- );
- CREATE INDEX IF NOT EXISTS idx_car_brand_id ON car(brand_id);
- CREATE INDEX IF NOT EXISTS idx_rental_car_id ON rental(car_id);
- CREATE INDEX IF NOT EXISTS idx_rental_client_id ON rental(client_id);
- CREATE INDEX IF NOT EXISTS idx_rental_dates ON rental(start_date, end_date);
- CREATE INDEX IF NOT EXISTS idx_rental_status ON rental(status);
- CREATE TABLE IF NOT EXISTS app_user(
- user_id SERIAL PRIMARY KEY,
- login VARCHAR(50) NOT NULL UNIQUE,
- full_name VARCHAR(100) NOT NULL,
- role VARCHAR(20) NOT NULL CHECK(role IN ('admin','manager','viewer')),
- password_hash TEXT NOT NULL,
- password_salt TEXT NOT NULL,
- is_active BOOLEAN NOT NULL DEFAULT true
- );
- CREATE TABLE IF NOT EXISTS audit_log(
- audit_id BIGSERIAL PRIMARY KEY,
- changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
- user_login TEXT NOT NULL DEFAULT 'system',
- action TEXT NOT NULL,
- entity TEXT NOT NULL,
- entity_id INT,
- details TEXT
- );
- CREATE OR REPLACE VIEW v_rentals_full AS
- SELECT r.rental_id, c.full_name AS client_name, c.phone AS client_phone,
- ca.license_plate, b.name AS brand_name, r.start_date, r.end_date,
- r.actual_return, r.total_cost, r.status
- FROM rental r
- JOIN client c ON c.client_id = r.client_id
- JOIN car ca ON ca.car_id = r.car_id
- JOIN brand b ON b.brand_id = ca.brand_id;
- CREATE OR REPLACE VIEW v_available_cars AS
- SELECT ca.car_id, ca.license_plate, b.name AS brand, b.country, ca.year, ca.daily_rate
- FROM car ca JOIN brand b ON b.brand_id = ca.brand_id
- WHERE ca.status = 'Свободен';
- CREATE OR REPLACE VIEW v_client_stats AS
- SELECT c.client_id, c.full_name, COUNT(r.rental_id) AS total_rentals,
- COALESCE(SUM(r.total_cost), 0) AS total_spent
- FROM client c LEFT JOIN rental r ON r.client_id = c.client_id
- GROUP BY c.client_id, c.full_name;
- CREATE OR REPLACE FUNCTION fn_autorent_current_user()
- RETURNS TEXT
- LANGUAGE sql
- AS $$
- SELECT COALESCE((SELECT value FROM app_context WHERE key = 'current_user'), current_user::text)
- $$;
- CREATE OR REPLACE FUNCTION fn_autorent_audit()
- RETURNS TRIGGER
- LANGUAGE plpgsql
- AS $$
- DECLARE
- pk_name TEXT := TG_ARGV[0];
- row_id INT;
- info TEXT;
- BEGIN
- IF TG_OP = 'INSERT' THEN
- row_id := (to_jsonb(NEW)->>pk_name)::INT;
- info := to_jsonb(NEW)::TEXT;
- INSERT INTO audit_log(user_login, action, entity, entity_id, details)
- VALUES(fn_autorent_current_user(), TG_OP, TG_TABLE_NAME, row_id, info);
- RETURN NEW;
- ELSIF TG_OP = 'UPDATE' THEN
- row_id := (to_jsonb(NEW)->>pk_name)::INT;
- info := jsonb_build_object('old', to_jsonb(OLD), 'new', to_jsonb(NEW))::TEXT;
- INSERT INTO audit_log(user_login, action, entity, entity_id, details)
- VALUES(fn_autorent_current_user(), TG_OP, TG_TABLE_NAME, row_id, info);
- RETURN NEW;
- ELSE
- row_id := (to_jsonb(OLD)->>pk_name)::INT;
- info := to_jsonb(OLD)::TEXT;
- INSERT INTO audit_log(user_login, action, entity, entity_id, details)
- VALUES(fn_autorent_current_user(), TG_OP, TG_TABLE_NAME, row_id, info);
- RETURN OLD;
- END IF;
- END;
- $$;
- DROP TRIGGER IF EXISTS trg_audit_brand ON brand;
- CREATE TRIGGER trg_audit_brand AFTER INSERT OR UPDATE OR DELETE ON brand
- FOR EACH ROW EXECUTE FUNCTION fn_autorent_audit('brand_id');
- DROP TRIGGER IF EXISTS trg_audit_car ON car;
- CREATE TRIGGER trg_audit_car AFTER INSERT OR UPDATE OR DELETE ON car
- FOR EACH ROW EXECUTE FUNCTION fn_autorent_audit('car_id');
- DROP TRIGGER IF EXISTS trg_audit_client ON client;
- CREATE TRIGGER trg_audit_client AFTER INSERT OR UPDATE OR DELETE ON client
- FOR EACH ROW EXECUTE FUNCTION fn_autorent_audit('client_id');
- DROP TRIGGER IF EXISTS trg_audit_rental ON rental;
- CREATE TRIGGER trg_audit_rental AFTER INSERT OR UPDATE OR DELETE ON rental
- FOR EACH ROW EXECUTE FUNCTION fn_autorent_audit('rental_id');
- """;
- }
|