Najnovší príspevok
+ +Všetky príspevky
+ +diff --git a/zadanie 1/README.md b/zadanie 1/README.md new file mode 100755 index 0000000..bf5367a --- /dev/null +++ b/zadanie 1/README.md @@ -0,0 +1,156 @@ +# Blog – Dokumentacia + +Osobný blog nasadený pomocou Docker Compose. Sklada sa zo styroch sluzieb beziacich v samostatnych kontajneroch. + + + +# Popis aplikacie + +Webová aplikacia umoznuje: +- prezerať zoznam blogových príspevkov +- citat jednotlive prispevky +- vytvarat a upravovat nove príspevky +- spravovat databazu cez pgAdmin webove rozhranie + + + +# Podmienky nasadenia + + Softver | Minimálna verzia + Linux (Ubuntu/Debian/Fedora) + Docker Engine | 24.0 + Docker Compose (plugin) | 2.20 + +Overenie instalacie: +```bash +docker --version +docker compose version +``` + + + +# Architektura – prehlad sluzieb + +``` + Priehliadac -> Nginx(proxy - port 80) -> Backend(Node.js) -> DB(Postgres) <- pgAdmin + | ^ + | | + ------------------------------------------------------------ + port 8080 + +# Zoznam kontajnerov + +| Kontajner | Obraz | Port | Popis | +| `blog-nginx` | `nginx:alpine` | `80` | Webserver pre frontend + reverse proxy na backend | +|`blog-backend` | vlastny build | interny `3000` | Node.js REST API, komunikuje s DB | +| `blog-db` | `postgres:16-alpine`| interny `5432` | PostgreSQL databaza, uchovava prispevky | +| `blog-pgadmin` | `dpage/pgadmin4` | `8080` | Webove rozhranie pre spravu databazy | + + + +## Virtuálne siete + +# `blog-network` +- Interna Docker siet, do ktorej su zapojene vsetky styri kontajnery +- Kontajnery sa navzajom adresuju cez db +- Z hostovského pocitaca su zvonka dostupne len služby s mapovanym portom (`nginx` na 80, `pgadmin` na 8080) + + + +## Trvale zvazky + +# `db-data` +- Typ: pomenovay zvazok (managed volume) +- Pripojeny na: `/var/lib/postgresql/data` v kontajneri `blog-db` +- ucel: data PostgreSQL databazy (prispevky blogu) preziju restart alebo zmazanie kontajnera +- Zvazok sa zachova pri `stop-app.sh`, zmaze sa len pri `remove-app.sh` + + + +# Konfigurácia kontajnerov + +## Nginx +- Staticke subory frontendu su namontovane z `./frontend` do `/usr/share/nginx/html` (read-only) +- Vlastna konfiguracia z `nginx.conf` nahradza predvolenu +- Vsetky requesty na `/api/*` presmeruje na `http://backend:3000` (reverse proxy, v praxi to zlepsuje bezpecnost + navstevnika stranky resp. uzivatela) + +# Backend (Node.js) +- Zostavany z `./backend/Dockerfile` na zaklade obrazu `node:20-alpine` +- Konfiguracia databazy sa predava cez premenne prostredia (`DB_HOST`, `DB_USER`, atd.) +- Startuje az po tom, co DB kontajner hlasi `healthy` (healthcheck) + +# PostgreSQL +- pri prvom starte automaticky spusti `./backend/init.sql` – vytvori tabulku a vlozi ukazkovy prispevok +- data uklada do zvazku `db-data` + +# pgAdmin +- oredvolene prihlasenie: `admin@blog.local` / `admin` +- po prihlaseni je potrebne manualne pridat server s udajmi: host `db`, port `5432`, db `blog`, + user `blog_user`, heslo `blog_pass` + + + +## Navod na manazment aplikacie + +# Priprava (jednorazovo) +``` +./prepare-app.sh +``` + +# Spustenie +``` +./start-app.sh +``` + +# Zastavenie (ulozi sa stav) +``` +./stop-app.sh +``` + +# Opatovne spustenie +``` +./start-app.sh +``` + +# Uplne odstranenie +``` +./remove-app.sh +``` + + + +# Pristup cez prehliadac + +| URL | Popis | +|---|---| +| `http://localhost` | Blog – zoznam prispevkov | +| `http://localhost/new-post.html` | Formular na novy prispevok | +| `http://localhost:8080` | pgAdmin – sprava databazy | + + + +## Pouzite zdroje + +- [Docker dokumentácia](https://docs.docker.com/) +- [Docker Compose dokumentácia](https://docs.docker.com/compose/) +- [Express.js dokumentácia](https://expressjs.com/) +- [node-postgres (pg) dokumentácia](https://node-postgres.com/) +- [Nginx dokumentácia](https://nginx.org/en/docs/) +- [PostgreSQL dokumentácia](https://www.postgresql.org/docs/) + + + +## Použitie umelej inteligencie + +Pri tvorbe projektu bol použitý AI asistent Claude (Anthropic, claude.ai). + +Použitie: +- Navrh architektúry aplikácie a výber technológií +- Generovanie kostry kodu (`server.js`, `db.js`, HTML stránky) +- Úprava Docker konfigurácie (`docker-compose.yaml`, `nginx.conf`, `Dockerfile`) +- Generovanie grafickej úpravy tejto dokumentácie +- interne CSS v index.html + +Všetok vygenerovaný kód bol preštudovaný, pochopený a upravovaný. Komentáre v kóde odrážajú +vlastné porozumenie danej problematiky. diff --git a/zadanie 1/backend/Dockerfile b/zadanie 1/backend/Dockerfile new file mode 100755 index 0000000..ed12b11 --- /dev/null +++ b/zadanie 1/backend/Dockerfile @@ -0,0 +1,24 @@ +# ============================================================= +# Dockerfile – Inštrukcie pre zostavenie Docker obrazu backendu +# ============================================================= + +# Začneme od oficiálneho Node.js obrazu (verzia LTS = dlhodobá podpora) +FROM node:20-alpine + +# Nastavíme pracovný adresár vnútri kontajnera +WORKDIR /app + +# Skopírujeme package.json a nainštalujeme závislosti +# Robíme to PRED kopírovaním zvyšku kódu – Docker cachuje vrstvy +# Ak sa zmení len server.js, npm install sa znova nespustí +COPY package.json . +RUN npm install --omit=dev + +# Skopírujeme zvyšok kódu do kontajnera +COPY . . + +# Informujeme Docker, že kontajner počúva na tomto porte +EXPOSE 3000 + +# Príkaz, ktorý sa spustí keď kontajner naštartuje +CMD ["node", "server.js"] diff --git a/zadanie 1/backend/db.js b/zadanie 1/backend/db.js new file mode 100755 index 0000000..e17f3c1 --- /dev/null +++ b/zadanie 1/backend/db.js @@ -0,0 +1,37 @@ +// ============================================================= +// db.js – Pripojenie k PostgreSQL databáze +// ============================================================= +// Tento modul exportuje funkciu query(), ktorú voláme v server.js +// Namiesto priameho pripájania pri každom requeste používame +// "connection pool" – znovupoužiteľné pripojenia (efektívnejšie) +// ============================================================= + +const { Pool } = require('pg'); + +// Pool – skupina otvorených pripojení k databáze +// Keď príde request, vezme voľné pripojenie z poolu a vráti ho späť +// Konfigurácia sa číta z premenných prostredia (environment variables) +// Tie nastavíme v Docker Compose – takto kód nevie o konkrétnom hesle +const pool = new Pool({ + host: process.env.DB_HOST || 'localhost', // adresa DB servera + port: parseInt(process.env.DB_PORT) || 5432, // štandardný PostgreSQL port + database: process.env.DB_NAME || 'blog', // názov databázy + user: process.env.DB_USER || 'blog_user', // používateľ DB + password: process.env.DB_PASSWORD || 'blog_pass', // heslo +}); + +// Otestujeme pripojenie hneď pri štarte servera +pool.connect((err, client, release) => { + if (err) { + console.error('Chyba pri pripájaní k databáze:', err.message); + } else { + console.log('Úspešne pripojený k PostgreSQL databáze'); + release(); // vrátime pripojenie späť do poolu + } +}); + +// Exportujeme pomocnú funkciu query() +// Ostatné súbory ju volajú ako: db.query('SELECT ...', [param1, ...]) +module.exports = { + query: (text, params) => pool.query(text, params), +}; diff --git a/zadanie 1/backend/init.sql b/zadanie 1/backend/init.sql new file mode 100755 index 0000000..0f7f575 --- /dev/null +++ b/zadanie 1/backend/init.sql @@ -0,0 +1,28 @@ +-- ============================================================= +-- init.sql – Inicializácia databázy +-- ============================================================= +-- Tento skript sa spustí automaticky pri prvom štarte PostgreSQL +-- kontajnera (Docker ho hľadá v /docker-entrypoint-initdb.d/) +-- ============================================================= + +-- Vytvoríme tabuľku pre príspevky +-- IF NOT EXISTS – skript môžeme spustiť aj viackrát bez chyby +CREATE TABLE IF NOT EXISTS posts ( + id SERIAL PRIMARY KEY, + title VARCHAR(200) NOT NULL, + content TEXT NOT NULL, + category VARCHAR(100), + excerpt VARCHAR(300), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP +); +-- Vložíme ukážkový príspevok, aby blog nebol prázdny pri prvom spustení +INSERT INTO posts (title, content, category, excerpt) VALUES +( + 'Vitajte na mojom blogu', + 'Toto je môj prvý príspevok. Blog beží na Docker infraštruktúre skladajúcej sa z Nginx webservera, Node.js backendu a PostgreSQL databázy. + +Každá služba beží vo vlastnom kontajneri a komunikujú medzi sebou cez virtuálnu Docker sieť.', + 'Technológie', + 'Prvý príspevok – predstavenie blogu a jeho technickej infraštruktúry.' +); diff --git a/zadanie 1/backend/package.json b/zadanie 1/backend/package.json new file mode 100755 index 0000000..27b5026 --- /dev/null +++ b/zadanie 1/backend/package.json @@ -0,0 +1,14 @@ +{ + "name": "blog-backend", + "version": "1.0.0", + "description": "Backend API pre osobný blog", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "pg": "^8.11.3" + } +} diff --git a/zadanie 1/backend/server.js b/zadanie 1/backend/server.js new file mode 100755 index 0000000..1743ff4 --- /dev/null +++ b/zadanie 1/backend/server.js @@ -0,0 +1,181 @@ + +// server.js – Blog API server + +// Používame Express.js – minimálny webový framework pre Node.js +// Počúva na porte 3000 a odpovedá na HTTP požiadavky od frontendu + + +const express = require('express'); +const cors = require('cors'); +const db = require('./db'); + +const app = express(); +const PORT = process.env.PORT || 3000; + + +// MIDDLEWARE +// Middleware sú funkcie, ktoré spracujú každý request +// predtým, než sa dostane k nášmu kódu (route handleru) + + +// Povolíme príjem JSON v tele requestu (napr. pri POST) +app.use(express.json()); + +// CORS – Cross-Origin Resource Sharing +// Povolíme frontendu (Nginx na porte 80) volať náš backend (port 3000) +// Bez toho by prehliadač zablokoval požiadavky z inej domény/portu +app.use(cors()); + + +// ROUTES – definujeme čo sa stane pri každej URL + + +// GET /health – jednoduchý endpoint na overenie, že server beží +// Používa sa napr. v Dockeri ako "healthcheck" +app.get('/health', (req, res) => { + res.json({ status: 'ok' }); +}); + +// GET /api/posts – vráti zoznam všetkých príspevkov +// Príspevky sú zoradené od najnovšieho po najstarší (ORDER BY created_at DESC) +app.get('/api/posts', async (req, res) => { + try { + const result = await db.query( + 'SELECT * FROM posts ORDER BY created_at DESC' + ); + // result.rows je pole objektov – každý riadok z DB je jeden objekt + res.json(result.rows); + } catch (err) { + console.error('Chyba pri načítaní príspevkov:', err.message); + res.status(500).json({ error: 'Interná chyba servera' }); + } +}); + +// GET /api/posts/:id – vráti jeden konkrétny príspevok podľa ID +// :id je parameter v URL – napr. /api/posts/5 → id = 5 +app.get('/api/posts/:id', async (req, res) => { + const id = parseInt(req.params.id); // načítame ID z URL + + // Základná validácia – ID musí byť číslo + if (isNaN(id)) { + return res.status(400).json({ error: 'ID musí byť číslo' }); + } + + try { + // $1 je placeholder – ochrana pred SQL injection útokom + // Nikdy nevkladáme premennú priamo do SQL reťazca! + const result = await db.query( + 'SELECT * FROM posts WHERE id = $1', + [id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Príspevok nenájdený' }); + } + + res.json(result.rows[0]); + } catch (err) { + console.error('Chyba pri načítaní príspevku:', err.message); + res.status(500).json({ error: 'Interná chyba servera' }); + } +}); + +// POST /api/posts – vytvorí nový príspevok +// Dáta prídu v tele requestu ako JSON +app.post('/api/posts', async (req, res) => { + // Destructuring – vyberieme len polia, ktoré potrebujeme + const { title, content, category, excerpt } = req.body; + + // Validácia vstupných dát + if (!title || !content) { + return res.status(400).json({ error: 'Nadpis a obsah sú povinné' }); + } + + try { + // RETURNING * – PostgreSQL vráti vložený riadok aj s vygenerovaným ID a dátumom + const result = await db.query( + `INSERT INTO posts (title, content, category, excerpt) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [title.trim(), content.trim(), category || null, excerpt?.trim() || null] + ); + + // HTTP 201 Created – signalizuje, že bol vytvorený nový zdroj + res.status(201).json(result.rows[0]); + } catch (err) { + console.error('Chyba pri vytváraní príspevku:', err.message); + res.status(500).json({ error: 'Interná chyba servera' }); + } +}); + + +app.put('/api/posts/:id', async (req, res) => { + const id = parseInt(req.params.id); + + if (isNaN(id)) { + return res.status(400).json({ error: 'ID musí byť číslo' }); + } + + const { title, content, category, excerpt } = req.body; + + if (!title || !content) { + return res.status(400).json({ error: 'Nadpis a obsah sú povinné' }); + } + + try { + const result = await db.query( + `UPDATE posts + SET title = $1, content = $2, category = $3, excerpt = $4, updated_at = NOW() + WHERE id = $5 + RETURNING *`, + [title.trim(), content.trim(), category || null, excerpt?.trim() || null, id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Príspevok nenájdený' }); + } + + res.json(result.rows[0]); + } catch (err) { + console.error('Chyba pri úprave príspevku:', err.message); + res.status(500).json({ error: 'Interná chyba servera' }); + } +}); + + + +// DELETE /api/posts/:id – zmaže príspevok podľa ID +app.delete('/api/posts/:id', async (req, res) => { + const id = parseInt(req.params.id); + + if (isNaN(id)) { + return res.status(400).json({ error: 'ID musí byť číslo' }); + } + + try { + const result = await db.query( + 'DELETE FROM posts WHERE id = $1 RETURNING id', + [id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Príspevok nenájdený' }); + } + + // HTTP 200 s potvrdením zmazania + res.json({ message: 'Príspevok bol zmazaný', id }); + } catch (err) { + console.error('Chyba pri mazaní príspevku:', err.message); + res.status(500).json({ error: 'Interná chyba servera' }); + } +}); + + +// SPUSTENIE SERVERA +// app.listen() spustí server a začne počúvať na danom porte +// '0.0.0.0' znamená: počúvaj na všetkých sieťových rozhraniach +// (potrebné v Dockeri, inak by počúval len na localhost) + +app.listen(PORT, '0.0.0.0', () => { + console.log(`Backend server beží na porte ${PORT}`); +}); diff --git a/zadanie 1/docker-compose.yaml b/zadanie 1/docker-compose.yaml new file mode 100755 index 0000000..43eee18 --- /dev/null +++ b/zadanie 1/docker-compose.yaml @@ -0,0 +1,130 @@ +# ============================================================= +# docker-compose.yaml – Konfigurácia všetkých služieb +# ============================================================= +# Docker Compose číta tento súbor a spustí všetky kontajnery +# naraz, so správnym prepojením a konfiguráciou. +# +# SLUŽBY: +# db – PostgreSQL databáza (port 5432) +# backend – Node.js API server (port 3000, interný) +# nginx – Webserver + proxy (port 80, verejný) +# pgadmin – Webové rozhranie DB (port 8080) +# +# SIEŤ: blog-network (interná Docker sieť) +# ZVÄZOK: db-data (trvalé uloženie dát databázy) +# ============================================================= + +services: + + # ── 1. DATABÁZA ───────────────────────────────────────────── + db: + image: postgres:16-alpine # oficiálny obraz, alpine = menší + container_name: blog-db + restart: unless-stopped # reštartuj po páde, nie po ručnom zastavení + + environment: + POSTGRES_DB: blog # názov databázy ktorá sa vytvorí + POSTGRES_USER: blog_user # používateľ + POSTGRES_PASSWORD: blog_pass # heslo + ports: + - "5432:5432" + volumes: + # Trvalý zväzok – dáta v DB prežijú reštart kontajnera + - db-data:/var/lib/postgresql/data + # Init skript – PostgreSQL ho spustí pri prvom štarte + - ./backend/init.sql:/docker-entrypoint-initdb.d/init.sql + + networks: + - blog-network + + # Healthcheck – Docker kontroluje či je DB pripravená + # Backend nesmie štartovať skôr, než DB prijíma spojenia + healthcheck: + test: ["CMD-SHELL", "pg_isready -U blog_user -d blog"] + interval: 5s # kontroluj každých 5 sekúnd + timeout: 5s # čakaj max 5 sekúnd na odpoveď + retries: 5 # po 5 neúspechoch = unhealthy + + # ── 2. BACKEND ────────────────────────────────────────────── + backend: + build: ./backend # zostav obraz z ./backend/Dockerfile + container_name: blog-backend + restart: unless-stopped + + environment: + # Tieto premenné číta db.js – takto heslo nie je v kóde + DB_HOST: db # názov DB služby = hostname v Docker sieti + DB_PORT: 5432 + DB_NAME: blog + DB_USER: blog_user + DB_PASSWORD: blog_pass + PORT: 3000 + + networks: + - blog-network + + # Čakaj kým DB je zdravá – až potom spusti backend + depends_on: + db: + condition: service_healthy + + # ── 3. NGINX (webserver + reverse proxy) ──────────────────── + nginx: + image: nginx:alpine + container_name: blog-nginx + restart: unless-stopped + + ports: + # Mapovanie portov: HOST:KONTAJNER + # Prehliadač sa pripojí na localhost:80 + - "80:80" + + volumes: + # Statické súbory frontendu + - ./frontend:/usr/share/nginx/html:ro # :ro = read-only + # Naša konfigurácia namiesto predvolenej + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + + networks: + - blog-network + + depends_on: + - backend + + # ── 4. PGADMIN (webové rozhranie pre databázu) ─────────────── + pgadmin: + image: dpage/pgadmin4:latest + container_name: blog-pgadmin + restart: unless-stopped + + environment: + PGADMIN_DEFAULT_EMAIL: admin@blog.local + PGADMIN_DEFAULT_PASSWORD: admin + + ports: + # pgAdmin dostupný na localhost:8080 + - "8080:80" + + networks: + - blog-network + + depends_on: + - db + +# ============================================================= +# SIETE +# ============================================================= +# Interná virtuálna sieť – kontajnery sa navzájom vidia +# cez názov služby (napr. "db", "backend") +# Zvonka (z hosťovského počítača) sú viditeľné len cez ports: +networks: + blog-network: + driver: bridge + +# ============================================================= +# ZVÄZKY +# ============================================================= +# Pomenovaný zväzok – Docker ho spravuje samostatne +# Dáta prežijú aj keď sa kontajner zmaže a znova vytvorí +volumes: + db-data: diff --git a/zadanie 1/frontend/index.html b/zadanie 1/frontend/index.html new file mode 100755 index 0000000..f873a3d --- /dev/null +++ b/zadanie 1/frontend/index.html @@ -0,0 +1,494 @@ + + +
+ + +
+ Najnovší príspevok
+ +Všetky príspevky
+ +Nový príspevok
+Zobrazí sa v zozname príspevkov. Ak nevyplníte, použije sa začiatok textu.
+