zadanie 27

This commit is contained in:
Michal Utľák 2026-03-31 15:40:44 +02:00
parent 49998cbc37
commit a35bddc6cb
22 changed files with 3223 additions and 0 deletions

7
z2/.env Normal file
View File

@ -0,0 +1,7 @@
DB_HOST=db
DB_PORT=3306
DB_USER=todo_user
DB_PASSWORD=SilneHeslo123.
DB_NAME=zkt_zadanie
SESSION_SECRET=EsteSilnejsieHeslo123.
PORT=3000

1
z2/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.sh text eol=lf

23
z2/README.md Normal file
View File

@ -0,0 +1,23 @@
1. Na spustenie aplikácie je potrebný Docker a pre automatizáciu kontajnerizácie aj WSL prípade PC s nainštalovaným Linuxom + GIT.
2. Je to jednoduchá TO-DO aplikácia kde si rôzni užívatelia možu pridávať a mazať svoje pripomienky, označiť ich za hotové a filtrovať na základe dátumu.
3. Aplikácia využíva jeden pomenovaný zväzok todo_mysql_data, ktorý slúži na perzistetné uloženie dát z databázy.
4. Konfigurácia kontajnerov bola rozelená na tri samostatné služby a to frontend, backend a databáza, a sú spravované pomocou Docker compose súboru. Databázovy kontajner využíva obraz mysql, backend je postavený na obraze node:20 a frontend na nginx.
5. todo_backend - kontajner obsahujúci backend založený na Node.js.
todo_db - kontajner obsahuhúci databázovú vrstvu založenú na obraze MYSQL
todo_frontend - kotanjner obsahujúci frontend založený na HTML, Tailwind a JS. Samotný kontajner je postavný na obraze Nginx.
6. Ak máte systém Linux resp. WSL, stačí sa presunút na miesto do ktorého chcete naklonovať repozitár. Repozitár je verejný takže by s klonovaním nemal byť problém. Potom stačí zadať príkaz:
git clone git@git.kemt.fei.tuke.sk:mu590ku/zkt26.git <nazov_noveho_priecinka_do_ktoreho_sa_repozitar_naklonuje>
Po zadaní príkazu sa vytvorí priečinok a v ňom bude ďalší priečinok z1, ktorý už bude obsahovať ako aplikáciu tak skripty pre automatickú kontajnerizáciu. Stačí už len postupne spúšťať skripty ./prepare-app.sh, ./start-app.sh, ./stop-app.sh a ./remove-app.sh.
Pozn. ak by sa stalo, ze skripty nepojde spustit a vyhodi chybu typu "permission denied while trying to connect to docker deamon socket" , treba spustit skripty so superuser pravami, tj. napr. sudo ./prepare-app.sh a pod. Taktiez aplikacia vyuziva porty 8080 pre frontend, 3000 pre backend a 3307 pre databazu takze je potrebne sa uistit, ze tieto porty su volne inak sa kontajnery nezapnu
7. Aplikáciu si na webovom rozhraní pozriete cez port 8080, príklad: 127.0.0.1:8080
8. ChatGPT 5.4, StackOverflow, Reddit

12
z2/backend/Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY backend ./backend
EXPOSE 3000
CMD ["node", "backend/app.js"]

226
z2/backend/app.js Normal file
View File

@ -0,0 +1,226 @@
const express = require('express');
const mysql = require('mysql2/promise');
const bcrypt = require('bcryptjs');
const session = require('express-session');
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '../.env') });
const app = express();
const FRONTEND_DIR = path.join(__dirname, '../frontend');
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
app.get('/', (req, res) => {
if (req.session.user) {
return res.redirect('/index.html');
}
res.sendFile(path.join(FRONTEND_DIR, 'login.html'));
});
app.get('/register', (req, res) => {
res.sendFile(path.join(FRONTEND_DIR, 'register.html'));
});
app.get('/index.html', (req, res) => {
if (!req.session.user) {
return res.redirect('/');
}
res.sendFile(path.join(FRONTEND_DIR, 'index.html'));
});
app.post('/register', async (req, res) => {
const { username, password, password2 } = req.body;
if (!username || !password || !password2) {
return res.redirect('/register.html?error=' + encodeURIComponent('Vyplň všetky polia'));
}
if (password !== password2) {
return res.redirect('/register.html?error=' + encodeURIComponent('Heslá sa nezhodujú'));
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
await pool.execute(
'INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword]
);
return res.redirect('/');
} catch (err) {
console.error(err);
return res.redirect('/register.html?error=' + encodeURIComponent('Používateľ už existuje'));
}
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.redirect('/?error=Vyplň%20všetky%20polia');
}
try {
const [rows] = await pool.execute(
'SELECT * FROM users WHERE username = ?',
[username]
);
const row = rows[0];
if (!row) {
return res.redirect('/?error=Zlé%20meno%20alebo%20heslo');
}
const result = await bcrypt.compare(password, row.password);
if (!result) {
return res.redirect('/?error=Zlé%20meno%20alebo%20heslo');
}
req.session.user = {
id: row.id,
username: row.username
};
return res.redirect('/index.html');
} catch (err) {
console.error(err);
return res.redirect('/?error=Chyba%20databázy');
}
});
app.post('/logout', (req, res) => {
req.session.destroy(() => {
res.redirect('/');
});
});
app.get('/api/me', (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' });
}
res.json(req.session.user);
});
app.get('/api/tasks', async (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' });
}
try {
const [rows] = await pool.execute(
'SELECT * FROM tasks WHERE user_id = ? ORDER BY deadline ASC',
[req.session.user.id]
);
res.json(rows);
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Chyba pri načítaní úloh' });
}
});
app.post('/api/tasks', async (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' });
}
const { title, description, deadline } = req.body;
if (!title || !description || !deadline) {
return res.status(400).json({ error: 'Vyplň všetky polia' });
}
try {
const [result] = await pool.execute(
'INSERT INTO tasks (user_id, title, description, deadline) VALUES (?, ?, ?, ?)',
[req.session.user.id, title, description, deadline]
);
const [rows] = await pool.execute(
'SELECT * FROM tasks WHERE id = ?',
[result.insertId]
);
res.json(rows[0]);
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Chyba pri ukladaní úlohy' });
}
});
app.delete('/api/tasks/:id', async (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' });
}
try {
await pool.execute(
'DELETE FROM tasks WHERE id = ? AND user_id = ?',
[req.params.id, req.session.user.id]
);
res.json({ success: true });
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Chyba pri mazaní úlohy' });
}
});
app.patch('/api/tasks/:id/toggle', async (req, res) => {
if (!req.session.user) {
return res.status(401).json({ error: 'Neprihlásený používateľ' });
}
const { status } = req.body;
try {
await pool.execute(
'UPDATE tasks SET status = ? WHERE id = ? AND user_id = ?',
[status ? 1 : 0, req.params.id, req.session.user.id]
);
res.json({ success: true });
} catch (err) {
console.error(err);
return res.status(500).json({ error: 'Chyba pri zmene stavu úlohy' });
}
});
app.use(express.static(FRONTEND_DIR));
const PORT = process.env.PORT || 3000;
app.listen(PORT, async () => {
try {
const connection = await pool.getConnection();
console.log('Pripojenie na MySQL úspešné.');
connection.release();
} catch (err) {
console.error('Nepodarilo sa pripojiť na MySQL:', err.message);
}
console.log(`Server beží na http://localhost:${PORT}`);
});

15
z2/db/init.sql Normal file
View File

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
deadline DATE NOT NULL,
status TINYINT(1) NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

51
z2/deployment.yml Normal file
View File

@ -0,0 +1,51 @@
apiVersion: v1
kind: Namespace
metadata:
name: todo-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: todo-app
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: z2-backend:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
envFrom:
- secretRef:
name: backend-env
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: todo-app
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: z2-frontend:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

42
z2/docker-compose.yaml Normal file
View File

@ -0,0 +1,42 @@
services:
db:
image: mysql:8.0
container_name: todo_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: NajsilnejsieHeslo123.
MYSQL_DATABASE: zkt_zadanie
MYSQL_USER: todo_user
MYSQL_PASSWORD: SilneHeslo123.
volumes:
- todo_mysql_data:/var/lib/mysql
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "3307:3306"
backend:
build:
context: .
dockerfile: backend/Dockerfile
container_name: todo_backend
restart: always
env_file:
- .env
depends_on:
- db
ports:
- "3000:3000"
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
container_name: todo_frontend
restart: always
depends_on:
- backend
ports:
- "8080:80"
volumes:
todo_mysql_data:

9
z2/frontend/Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM nginx:alpine
COPY frontend/index.html /usr/share/nginx/html/index.html
COPY frontend/login.html /usr/share/nginx/html/login.html
COPY frontend/register.html /usr/share/nginx/html/register.html
COPY frontend/generovanie_karticiek.js /usr/share/nginx/html/generovanie_karticiek.js
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

View File

@ -0,0 +1,209 @@
const emptyMessage = document.getElementById("emptyMessage");
const cardContainer = document.getElementById("cardContainer");
const addCardBtn = document.getElementById("addCardBtn");
const taskModal = document.getElementById("taskModal");
const saveTaskBtn = document.getElementById("saveTaskBtn");
const cancelTaskBtn = document.getElementById("cancelTaskBtn");
const filterDate = document.getElementById("filterDate");
const loggedUser = document.getElementById("loggedUser");
let tasks = [];
function formatDate(isoDate) {
if (!isoDate) return "bez termínu";
return new Date(isoDate).toLocaleDateString("sk-SK", {
day: "2-digit",
month: "2-digit",
year: "numeric"
});
}
function updateEmptyVisibility() {
if (tasks.length === 0) {
emptyMessage.classList.remove("hidden");
} else {
emptyMessage.classList.add("hidden");
}
}
function createCard(task) {
const card = document.createElement("div");
card.className = "p-6 rounded-xl shadow-md border hover:shadow-lg transition-all duration-200";
card.dataset.deadline = task.deadline || "";
card.dataset.id = task.id;
if (task.status) {
card.classList.add("bg-green-100", "border-green-200");
} else {
card.classList.add("bg-yellow-100", "border-yellow-200");
}
card.innerHTML = `
<h3 class="font-bold text-lg mb-2 text-gray-800">${task.title}</h3>
<p class="text-gray-700 mb-4">${task.description}</p>
<p class="text-sm text-gray-600 mb-5">Termín: ${formatDate(task.deadline)}</p>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input type="checkbox" class="task-checkbox h-5 w-5 text-green-600 rounded border-gray-300 focus:ring-green-500" ${task.status ? "checked" : ""}>
<label class="ml-2 text-sm text-gray-700 cursor-pointer">Hotovo</label>
</div>
<button class="delete-btn px-5 py-2 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 transition">
Zmazať
</button>
</div>
`;
const checkbox = card.querySelector(".task-checkbox");
checkbox.addEventListener("change", async () => {
const newValue = checkbox.checked ? 1 : 0;
try {
const response = await fetch(`/api/tasks/${task.id}/toggle`, {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ status: newValue })
});
if (!response.ok) {
console.error("PATCH neprešiel:", response.status);
checkbox.checked = !checkbox.checked;
return;
}
task.status = newValue;
card.classList.remove(
"bg-yellow-100",
"border-yellow-200",
"bg-green-100",
"border-green-200"
);
if (newValue) {
card.classList.add("bg-green-100", "border-green-200");
} else {
card.classList.add("bg-yellow-100", "border-yellow-200");
}
} catch (error) {
console.error("Chyba fetchu:", error);
checkbox.checked = !checkbox.checked;
}
});
const deleteBtn = card.querySelector(".delete-btn");
deleteBtn.addEventListener("click", async () => {
try {
const response = await fetch(`/api/tasks/${task.id}`, {
method: "DELETE"
});
if (response.ok) {
tasks = tasks.filter(t => t.id !== task.id);
renderTasks();
}
} catch (error) {
console.error("Chyba pri mazaní úlohy:", error);
}
});
cardContainer.appendChild(card);
}
function renderTasks() {
const order = filterDate.value;
const sortedTasks = [...tasks].sort((a, b) => {
const da = a.deadline ? new Date(a.deadline) : new Date("9999-12-31");
const db = b.deadline ? new Date(b.deadline) : new Date("9999-12-31");
return order === "asc" ? da - db : db - da;
});
cardContainer.innerHTML = "";
sortedTasks.forEach(createCard);
updateEmptyVisibility();
}
function showModal() {
taskModal.classList.remove("hidden");
document.getElementById("taskTitle").value = "";
document.getElementById("taskDescription").value = "";
document.getElementById("taskDeadline").value = "";
}
function hideModal() {
taskModal.classList.add("hidden");
}
async function loadUser() {
try {
const response = await fetch("/api/me");
if (!response.ok) {
window.location.href = "/login.html";
return;
}
const user = await response.json();
loggedUser.textContent = `Prihlásený ako: ${user.username}`;
} catch (error) {
console.error("Chyba pri načítaní používateľa:", error);
window.location.href = "/login.html";
}
}
async function loadTasks() {
try {
const response = await fetch("/api/tasks");
if (!response.ok) {
window.location.href = "/login.html";
return;
}
tasks = await response.json();
renderTasks();
} catch (error) {
console.error("Chyba pri načítaní úloh:", error);
}
}
addCardBtn.addEventListener("click", showModal);
cancelTaskBtn.addEventListener("click", hideModal);
saveTaskBtn.addEventListener("click", async () => {
const title = document.getElementById("taskTitle").value.trim();
const description = document.getElementById("taskDescription").value.trim();
const deadline = document.getElementById("taskDeadline").value;
if (!title || !description || !deadline) {
return;
}
try {
const response = await fetch("/api/tasks", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ title, description, deadline })
});
if (response.ok) {
const newTask = await response.json();
tasks.push(newTask);
renderTasks();
hideModal();
}
} catch (error) {
console.error("Chyba pri pridávaní úlohy:", error);
}
});
filterDate.addEventListener("change", renderTasks);
loadUser();
loadTasks();

94
z2/frontend/index.html Normal file
View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="sk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>To-Do aplikácia</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
background-position: right 0.75rem center;
background-repeat: no-repeat;
background-size: 1.25em;
}
</style>
</head>
<body class="flex flex-col min-h-screen bg-gray-100">
<header class="bg-orange-500 h-20 flex justify-between items-center shadow-md px-6">
<h1 class="text-white font-bold text-2xl">To-Do aplikácia</h1>
<div class="flex items-center gap-4">
<span id="loggedUser" class="text-white font-medium"></span>
<form action="/logout" method="POST">
<button
type="submit"
class="bg-red-600 hover:bg-red-700 text-white font-medium px-4 py-2 rounded-lg transition"
>
Odhlásiť sa
</button>
</form>
</div>
</header>
<main class="flex-1 p-6 flex flex-col items-center bg-gray-50">
<div class="w-full max-w-3xl mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-3">
<div class="relative">
<select id="filterDate" class="pl-10 pr-4 py-2.5 bg-white border border-gray-300 rounded-lg shadow-sm focus:border-orange-400 focus:ring-orange-400 appearance-none">
<option value="asc">Najskôr najbližší termín</option>
<option value="desc">Najskôr najvzdialenejší termín</option>
</select>
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</div>
<button id="addCardBtn" class="px-7 py-3.5 bg-orange-600 hover:bg-orange-700 text-white font-semibold rounded-full shadow-lg transition-all transform hover:scale-105 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
Pridať úlohu
</button>
</div>
<div id="emptyMessage" class="hidden text-center py-16 w-full max-w-3xl">
<p class="text-2xl font-medium text-gray-500 mb-6">Zatiaľ žiadne úlohy</p>
</div>
<div id="cardContainer" class="w-full max-w-3xl space-y-5">
</div>
</main>
<footer class="bg-orange-500 h-16 flex justify-center items-center text-white font-medium shadow-inner">
<div>By Michal</div>
</footer>
<div id="taskModal" class="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center hidden z-50">
<div class="bg-white rounded-xl p-8 w-full max-w-md shadow-2xl">
<h2 class="text-2xl font-bold mb-6 text-gray-800">Nová úloha</h2>
<label for="taskTitle" class="block text-sm font-medium text-gray-700 mb-1">Názov</label>
<input id="taskTitle" type="text" class="w-full p-3 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-orange-400" placeholder="Zadajte názov úlohy">
<label for="taskDescription" class="block text-sm font-medium text-gray-700 mb-1">Popis</label>
<textarea id="taskDescription" class="w-full p-3 border border-gray-300 rounded-lg mb-4 focus:outline-none focus:ring-2 focus:ring-orange-400" rows="4" placeholder="Detailnejší popis..."></textarea>
<label for="taskDeadline" class="block text-sm font-medium text-gray-700 mb-1">Termín dokončenia</label>
<input id="taskDeadline" type="date" class="w-full p-3 border border-gray-300 rounded-lg mb-6 focus:outline-none focus:ring-2 focus:ring-orange-400">
<div class="flex justify-end gap-3">
<button id="cancelTaskBtn" class="px-5 py-2.5 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition">Zrušiť</button>
<button id="saveTaskBtn" class="px-5 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">Uložiť</button>
</div>
</div>
</div>
<script src="generovanie_karticiek.js"></script>
</body>
</html>

77
z2/frontend/login.html Normal file
View File

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="sk">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Prihlásenie</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
<div class="w-full max-w-sm bg-white rounded-xl shadow-lg p-8">
<h1 class="text-3xl font-bold text-center text-gray-800 mb-8">
Prihlásenie
</h1>
<form id="loginForm" action="/login" method="POST" class="space-y-6">
<div>
<label for="meno" class="block text-sm font-medium text-gray-700 mb-1">
Meno / používateľ
</label>
<input
type="text"
id="meno"
name="username"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="Zadaj svoje meno"
/>
</div>
<div>
<label for="heslo" class="block text-sm font-medium text-gray-700 mb-1">
Heslo
</label>
<input
type="password"
id="heslo"
name="password"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="••••••••"
/>
</div>
<button
type="submit"
class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 rounded-lg transition duration-200"
>
Prihlásiť sa
</button>
</form>
<div class="text-center mt-6 text-sm text-gray-600">
Ešte nemáš účet?
<a href="/register.html" class="text-orange-600 hover:underline font-medium">
Zaregistruj sa
</a>
</div>
<p id="error" class="hidden mt-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg px-4 py-3 text-center"></p>
</div>
<script>
const params = new URLSearchParams(window.location.search);
const error = params.get('error');
const errorBox = document.getElementById('error');
if (error) {
errorBox.textContent = error;
errorBox.classList.remove('hidden');
}
</script>
</body>
</html>

42
z2/frontend/nginx.conf Normal file
View File

@ -0,0 +1,42 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index login.html;
location = / {
try_files /login.html =404;
}
location /api/ {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /login {
proxy_pass http://backend:3000/login;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /register {
proxy_pass http://backend:3000/register;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /logout {
proxy_pass http://backend:3000/logout;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location / {
try_files $uri $uri/ =404;
}
}

91
z2/frontend/register.html Normal file
View File

@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="sk">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Registrácia</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen bg-gray-100 flex items-center justify-center p-4">
<div class="w-full max-w-sm bg-white rounded-xl shadow-lg p-8">
<h1 class="text-3xl font-bold text-center text-gray-800 mb-8">
Registrácia
</h1>
<form id="registerForm" action="/register" method="POST" class="space-y-6">
<div>
<label for="meno" class="block text-sm font-medium text-gray-700 mb-1">
Meno / používateľ
</label>
<input
type="text"
id="meno"
name="username"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="Zadaj svoje meno"
/>
</div>
<div>
<label for="heslo" class="block text-sm font-medium text-gray-700 mb-1">
Heslo
</label>
<input
type="password"
id="heslo"
name="password"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="••••••••"
/>
</div>
<div>
<label for="heslo2" class="block text-sm font-medium text-gray-700 mb-1">
Heslo znova
</label>
<input
type="password"
id="heslo2"
name="password2"
required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-orange-400"
placeholder="Potvrď heslo"
/>
</div>
<button
type="submit"
class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 rounded-lg transition duration-200"
>
Vytvoriť účet
</button>
</form>
<div class="text-center mt-6 text-sm text-gray-600">
Už máš účet?
<a href="/login.html" class="text-orange-600 hover:underline font-medium">
Prihlás sa
</a>
</div>
<p id="error" class="hidden mt-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg px-4 py-3 text-center"></p>
</div>
<script>
const params = new URLSearchParams(window.location.search);
const error = params.get('error');
const errorBox = document.getElementById('error');
if (error) {
errorBox.textContent = error;
errorBox.classList.remove('hidden');
}
</script>
</body>
</html>

2148
z2/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

11
z2/package.json Normal file
View File

@ -0,0 +1,11 @@
{
"dependencies": {
"bcryptjs": "^3.0.3",
"body-parser": "^2.2.2",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"express-session": "^1.19.0",
"mysql2": "^3.20.0",
"sqlite3": "^6.0.1"
}
}

16
z2/prepare-app.sh Normal file
View File

@ -0,0 +1,16 @@
#!/bin/bash
echo "Vytvaram namespace todo-app..."
kubectl create namespace todo-app --dry-run=client -o yaml | kubectl apply -f -
echo "Buildim backend image..."
docker build -t z2-backend:latest -f backend/Dockerfile .
echo "Buildim frontend image..."
docker build -t z2-frontend:latest -f frontend/Dockerfile .
echo "Vytvaram secret backend-env z .env..."
kubectl -n todo-app delete secret backend-env --ignore-not-found
kubectl -n todo-app create secret generic backend-env --from-env-file=.env
echo "Prepare hotovy."

9
z2/remove-app.sh Normal file
View File

@ -0,0 +1,9 @@
#!/bin/bash
kubectl delete -f service.yml
kubectl delete -f statefulset.yml
kubectl delete -f deployment.yml
kubectl delete secret backend-env -n todo-app
kubectl delete namespace todo-app
docker rmi z2-backend:latest
docker rmi z2-frontend:latest

39
z2/service.yml Normal file
View File

@ -0,0 +1,39 @@
apiVersion: v1
kind: Service
metadata:
name: db
namespace: todo-app
spec:
selector:
app: db
ports:
- port: 3306
targetPort: 3306
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: todo-app
spec:
selector:
app: backend
ports:
- port: 3000
targetPort: 3000
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: todo-app
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80
nodePort: 30080
type: NodePort

5
z2/start-app.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
kubectl apply -f deployment.yml
kubectl apply -f service.yml
kubectl apply -f statefulset.yml

91
z2/statefulset.yml Normal file
View File

@ -0,0 +1,91 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /tmp/todo-mysql-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
namespace: todo-app
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-init-script
namespace: todo-app
data:
init.sql: |
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
deadline DATE NOT NULL,
status TINYINT(1) NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
namespace: todo-app
spec:
serviceName: db
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: "NajsilnejsieHeslo123."
- name: MYSQL_DATABASE
value: "zkt_zadanie"
- name: MYSQL_USER
value: "todo_user"
- name: MYSQL_PASSWORD
value: "SilneHeslo123."
volumeMounts:
- name: mysql-storage
mountPath: /var/lib/mysql
- name: mysql-init-volume
mountPath: /docker-entrypoint-initdb.d/init.sql
subPath: init.sql
volumes:
- name: mysql-storage
persistentVolumeClaim:
claimName: mysql-pvc
- name: mysql-init-volume
configMap:
name: mysql-init-script

5
z2/stop-app.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
kubectl delete -f service.yml
kubectl delete -f statefulset.yml
kubectl delete -f deployment.yml