# MiniHttpServer — Туристический сайт (Отели)
Проект представляет собой учебный туристический сайт с динамическими страницами отелей, собственной ORM, шаблонизатором, админ-панелью и самописным веб-сервером на `HttpListener`.
Проект полностью работает через PostgreSQL — все данные загружаются из реальной базы.
Вёрстка и клиентская логика выполнены на чистом **HTML**, **CSS** и **JavaScript**.
---
## 🚀 Возможности
- Публичный сайт (список отелей, детали отеля, галерея, поиск).
- Полностью динамическая загрузка данных из БД.
- AJAX-фильтрация и AJAX-авторизация (без перезагрузки страницы).
- Административная панель (CRUD отелей, описание, услуги, категории).
- Авторизация пользователей (Admin / Employee / User).
- Собственный HTML-шаблонизатор: переменные, условия, циклы.
- Собственная ORM: CRUD, `Where(Expression)`, маппинг объектов.
- Собственный веб-сервер на `HttpListener`: роутинг, статические файлы, сессии.
- Чистый JavaScript без фреймворков.
---
## 📁 Структура проекта
```text
MiniHttpServer/
│
├── Endpoints/ # Маршруты (аналог MVC контроллеров)
├── Model/ # Доменные сущности (Hotel, City, Country …)
├── DTOs/ # Объекты для запросов/ответов
├── Services/ # Прикладная логика
├── Validators/ # Серверная валидация
├── Template/ # .thtml-шаблоны страниц
├── Public/ # HTML, CSS, JS (клиентская часть)
│
├── MiniHttpServer.Framework/
│ ├── Server/ # Реализация HttpListener сервера
│ ├── Handlers/ # Роутинг, статика, обработка ошибок
│ ├── Attributes/ # HttpGet, HttpPost, Endpoint и т.п.
│ └── Settings/ # Настройки (порт, домен, БД)
│
├── MyORMContext/ # Собственная ORM
├── MiniHttpServer.Data/ # Репозитории (Repository/DAO)
└── Tests/ # Юнит-тестыCREATE DATABASE onetouche;Можно либо выполнить файл init.sql, либо воспользоваться этим SQL-скриптом:
-- This script was generated by the ERD tool in pgAdmin 4.
-- Please log an issue at https://github.com/pgadmin-org/pgadmin4/issues/new/choose if you find any bugs, including reproduction steps.
BEGIN;
CREATE TABLE IF NOT EXISTS public.cities
(
id serial NOT NULL,
name character varying(100) COLLATE pg_catalog."default" NOT NULL,
countryid integer NOT NULL,
CONSTRAINT cities_pkey PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public.countries
(
id serial NOT NULL,
name character varying(100) COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT countries_pkey PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public.hotel_categories
(
id serial NOT NULL,
name character varying(150) COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT hotel_categories_pkey PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public.hotel_category_map
(
hotelid integer NOT NULL,
categoryid integer NOT NULL,
CONSTRAINT hotel_category_map_pkey PRIMARY KEY (hotelid, categoryid)
);
CREATE TABLE IF NOT EXISTS public.hotel_mealplans
(
hotelid integer NOT NULL,
mealplanid integer NOT NULL,
CONSTRAINT hotel_mealplans_pkey PRIMARY KEY (hotelid, mealplanid)
);
CREATE TABLE IF NOT EXISTS public.hoteldescriptions
(
id serial NOT NULL,
hotelid integer NOT NULL,
yearopened integer,
yearrenovated integer,
totalareasquaremeters numeric(10, 2),
buildinginfo text COLLATE pg_catalog."default",
CONSTRAINT hoteldescriptions_pkey PRIMARY KEY (id),
CONSTRAINT hoteldescriptions_hotelid_key UNIQUE (hotelid)
);
CREATE TABLE IF NOT EXISTS public.hotelplaceinfos
(
id serial NOT NULL,
hotelid integer NOT NULL,
address character varying(255) COLLATE pg_catalog."default",
city character varying(100) COLLATE pg_catalog."default",
country character varying(100) COLLATE pg_catalog."default",
distancetoairport character varying(100) COLLATE pg_catalog."default",
distancetocenter character varying(100) COLLATE pg_catalog."default",
distancetobeach character varying(100) COLLATE pg_catalog."default",
CONSTRAINT hotelplaceinfos_pkey PRIMARY KEY (id),
CONSTRAINT hotelplaceinfos_hotelid_key UNIQUE (hotelid)
);
CREATE TABLE IF NOT EXISTS public.hotels
(
id serial NOT NULL,
name character varying(150) COLLATE pg_catalog."default" NOT NULL,
slug character varying(150) COLLATE pg_catalog."default" NOT NULL,
cityid integer NOT NULL,
hoteltype character varying(50) COLLATE pg_catalog."default" NOT NULL,
description text COLLATE pg_catalog."default" NOT NULL,
photourl character varying(255) COLLATE pg_catalog."default" NOT NULL,
mealplancode character varying(20) COLLATE pg_catalog."default",
price integer NOT NULL,
contacts text[] COLLATE pg_catalog."default",
CONSTRAINT hotels_pkey PRIMARY KEY (id),
CONSTRAINT hotels_slug_key UNIQUE (slug)
);
CREATE TABLE IF NOT EXISTS public.hotelservices
(
id serial NOT NULL,
hotelid integer NOT NULL,
category character varying(50) COLLATE pg_catalog."default" NOT NULL,
name character varying(150) COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT hotelservices_pkey PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public.mealplans
(
id serial NOT NULL,
code character varying(20) COLLATE pg_catalog."default" NOT NULL,
description character varying(255) COLLATE pg_catalog."default" NOT NULL,
smokingallowedinrestaurant boolean NOT NULL DEFAULT false,
CONSTRAINT mealplans_pkey PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public.roomtypes
(
id serial NOT NULL,
hotelid integer NOT NULL,
name character varying(100) COLLATE pg_catalog."default" NOT NULL,
view character varying(100) COLLATE pg_catalog."default" NOT NULL,
bedconfiguration character varying(150) COLLATE pg_catalog."default" NOT NULL,
maxoccupancy integer NOT NULL,
areasquaremeters integer NOT NULL,
CONSTRAINT roomtypes_pkey PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS public.users
(
id serial NOT NULL,
name character varying(100) COLLATE pg_catalog."default" NOT NULL,
email character varying(120) COLLATE pg_catalog."default" NOT NULL,
passwordhash character varying(255) COLLATE pg_catalog."default" NOT NULL,
role character varying(50) COLLATE pg_catalog."default" NOT NULL DEFAULT 'User'::character varying,
createdat timestamp with time zone NOT NULL DEFAULT now(),
login character varying(50) COLLATE pg_catalog."default" NOT NULL,
phone character varying(30) COLLATE pg_catalog."default" NOT NULL DEFAULT ''::character varying,
CONSTRAINT users_pkey PRIMARY KEY (id),
CONSTRAINT users_email_key UNIQUE (email),
CONSTRAINT users_login_key UNIQUE (login)
);
ALTER TABLE IF EXISTS public.cities
ADD CONSTRAINT cities_countryid_fkey FOREIGN KEY (countryid)
REFERENCES public.countries (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
CREATE INDEX IF NOT EXISTS ix_cities_countryid
ON public.cities(countryid);
ALTER TABLE IF EXISTS public.hotel_category_map
ADD CONSTRAINT hotel_category_map_category_id_fkey FOREIGN KEY (categoryid)
REFERENCES public.hotel_categories (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE;
ALTER TABLE IF EXISTS public.hotel_category_map
ADD CONSTRAINT hotel_category_map_hotel_id_fkey FOREIGN KEY (hotelid)
REFERENCES public.hotels (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE;
ALTER TABLE IF EXISTS public.hotel_mealplans
ADD CONSTRAINT hotel_mealplans_hotelid_fkey FOREIGN KEY (hotelid)
REFERENCES public.hotels (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE;
ALTER TABLE IF EXISTS public.hotel_mealplans
ADD CONSTRAINT hotel_mealplans_mealplanid_fkey FOREIGN KEY (mealplanid)
REFERENCES public.mealplans (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE;
ALTER TABLE IF EXISTS public.hoteldescriptions
ADD CONSTRAINT hoteldescriptions_hotelid_fkey FOREIGN KEY (hotelid)
REFERENCES public.hotels (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS hoteldescriptions_hotelid_key
ON public.hoteldescriptions(hotelid);
ALTER TABLE IF EXISTS public.hotelplaceinfos
ADD CONSTRAINT hotelplaceinfos_hotelid_fkey FOREIGN KEY (hotelid)
REFERENCES public.hotels (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS hotelplaceinfos_hotelid_key
ON public.hotelplaceinfos(hotelid);
ALTER TABLE IF EXISTS public.hotels
ADD CONSTRAINT hotels_cityid_fkey FOREIGN KEY (cityid)
REFERENCES public.cities (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION;
CREATE INDEX IF NOT EXISTS ix_hotels_cityid
ON public.hotels(cityid);
ALTER TABLE IF EXISTS public.hotelservices
ADD CONSTRAINT hotelservices_hotelid_fkey FOREIGN KEY (hotelid)
REFERENCES public.hotels (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS ix_hotelservices_hotelid
ON public.hotelservices(hotelid);
ALTER TABLE IF EXISTS public.roomtypes
ADD CONSTRAINT roomtypes_hotelid_fkey FOREIGN KEY (hotelid)
REFERENCES public.hotels (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE;
CREATE INDEX IF NOT EXISTS ix_roomtypes_hotelid
ON public.roomtypes(hotelid);
END;Файл:
MiniHttpServer.Framework/Settings/settings.json
Пример для локального PostgreSQL:
{
"Domain": "localhost",
"Port": "1337",
"ConectionString": "Host=localhost;Port=5432;Database=onetouche;Username=postgres;Password=PASSWORD"
}- Установить .NET 9 SDK
- Установить и запустить PostgreSQL
- Проверить правильность строки подключения
- В терминале выполнить:
cd MiniHttpServer
dotnet runСервер запустится по адресу:
http://localhost:1337/
| Описание | Путь |
|---|---|
| Главная страница | /Public/index.html |
| Авторизация | /Public/autorization.html |
| Список отелей | /hotels |
| Детали отеля | /hotels/{id} |
После авторизации:
/admin
Функции:
- Просмотр списка отелей
- Добавление / редактирование / удаление
- Редактирование описания, характеристик, услуг, категорий
- Проверка ролей (Admin / Employee)
- AJAX-авторизация (
POST /auth/login) - Фильтрация списка отелей (
/api/hotels/search) - Подгрузка данных через API
- Обновление интерфейса без перезагрузки страницы
Поддерживает:
${Hotel.Name}
@if(Model.HasDiscount) { ... }@foreach(var item in Model.Items) { ... }Запуск тестов:
dotnet testПокрыто тестами:
- движок шаблонизатора
- веб-сервер
В проекте есть docker-compose.yml, который поднимает:
- контейнер с PostgreSQL (
postgres) - контейнер с приложением (
mini-http-server)
- Установлен Docker Desktop (Windows / macOS / Linux).
- В терминале команды работают:
docker --version
docker compose versionИз корня решения (там, где лежит docker-compose.yml):
docker compose up --buildЧто происходит:
- собирается образ приложения из
MiniHttpServer/Dockerfile; - поднимается контейнер
postgres, выполняетсяinit.sql(создание схемы и начальные данные); - после готовности БД запускается контейнер
app(mini-http-server).
После запуска контейнеров:
http://localhost:1337/index.html
или
http://localhost:1337/Public/index.html
Админка:
http://localhost:1337/admin
Если изменили код, settings.json или Dockerfile:
docker compose down # остановить контейнеры
docker compose up --build # пересобрать образ и запустить зановоМожно просто:
docker compose up --buildDocker сам пересоберёт образ при изменениях.
PostgreSQL в Docker хранит данные в томе postgres_data.
Чтобы полностью сбросить БД и заново прогнать init.sql:
- Остановить и удалить контейнеры с удалением томов:
docker compose down -v- Запустить всё снова:
docker compose up --buildПри первом старте:
- создастся чистая БД
onetouche; - выполнится
init.sql(создаст таблицы, связи, тестовые данные и пользователя Admin).
Логи приложения:
docker logs mini-http-serverЛоги PostgreSQL:
docker logs postgres_db(имена контейнеров можно уточнить командой docker ps или в Docker Desktop).