Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@ services:
image: confluentinc/cp-kafka:7.5.0
container_name: kafka
restart: unless-stopped
depends_on:
zookeeper:
condition: service_healthy
ports:
- "9092:9092"
- "9093:9093"
Expand All @@ -58,6 +55,13 @@ services:
interval: 10s
timeout: 10s
retries: 5
redis:
image: redis:8.2.1
restart: on-failure
ports:
- "6379:6379"
volumes:
- redis_data:/data
user-service:
image: user-service:latest
build:
Expand All @@ -77,13 +81,6 @@ services:
- KAFKA_BOOTSTRAP_SERVERS=kafka:9093
env_file:
- .env
redis:
image: redis:8.2.1
restart: on-failure
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
pgdata:
redis_data:
201 changes: 201 additions & 0 deletions frontend/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
const API_BASE_URL = 'http://localhost:8080/users';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Backend Unreachable: Port Mismatch in Docker

The API_BASE_URL points to http://localhost:8080/users, but the user-service runs on port 8082 when using Docker (as configured in docker-compose.yml and application-docker.yml). The frontend won't be able to connect to the backend service, causing all API requests to fail.

Fix in Cursor Fix in Web


const dom = {
usersBody: document.querySelector('#users-body'),
listStatus: document.querySelector('#list-status'),
getForm: document.querySelector('#get-form'),
getId: document.querySelector('#get-id'),
getStatus: document.querySelector('#get-status'),
getResult: document.querySelector('#get-result'),
createForm: document.querySelector('#create-form'),
createName: document.querySelector('#create-name'),
createEmail: document.querySelector('#create-email'),
createAge: document.querySelector('#create-age'),
createStatus: document.querySelector('#create-status'),
updateForm: document.querySelector('#update-form'),
updateId: document.querySelector('#update-id'),
updateName: document.querySelector('#update-name'),
updateEmail: document.querySelector('#update-email'),
updateAge: document.querySelector('#update-age'),
updateStatus: document.querySelector('#update-status'),
deleteForm: document.querySelector('#delete-form'),
deleteId: document.querySelector('#delete-id'),
deleteStatus: document.querySelector('#delete-status'),
refreshBtn: document.querySelector('#refresh-btn')
};

const defaultHeaders = {
'Content-Type': 'application/json',
Accept: 'application/json'
};

const setStatus = (el, message, isError = false) => {
if (!el) {
return;
}
el.style.color = isError ? '#ef4444' : '#38bdf8';
el.textContent = message ?? '';
};

const request = async (path = '', options = {}) => {
const response = await fetch(`${API_BASE_URL}${path}`, {
...options,
headers: {
...defaultHeaders,
...(options.headers ?? {})
}
});

if (!response.ok) {
let errorMessage = `Ошибка ${response.status}`;
try {
const errorBody = await response.json();
if (errorBody?.message) {
errorMessage += `: ${errorBody.message}`;
}
} catch {
// ignore parsing errors
}
throw new Error(errorMessage);
}

if (response.status === 204) {
return null;
}

return response.json();
};

const renderUsers = (users = []) => {
if (!Array.isArray(users) || !users.length) {
dom.usersBody.innerHTML = `
<tr>
<td colspan="4">Нет данных. Создайте нового пользователя или обновите список.</td>
</tr>
`;
return;
}

dom.usersBody.innerHTML = users.map((user) => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
<td>${user.age}</td>
</tr>
`).join('');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: XSS: Unsanitized User Data Injection

User data (user.id, user.name, user.email, user.age) is directly interpolated into HTML via innerHTML without sanitization. If any user field contains malicious HTML or JavaScript, it will execute in the browser, creating an XSS vulnerability.

Fix in Cursor Fix in Web

};

const refreshUsers = async () => {
setStatus(dom.listStatus, 'Загрузка пользователей...');
try {
const users = await request('');
renderUsers(users);
setStatus(dom.listStatus, `Получено записей: ${users.length}`);
} catch (error) {
renderUsers([]);
setStatus(dom.listStatus, error.message);
}
};

const handleGetUser = async (event) => {
event.preventDefault();
const id = dom.getId.value.trim();
if (!id) {
setStatus(dom.getStatus, 'Укажите ID пользователя', true);
return;
}

setStatus(dom.getStatus, 'Запрос пользователя...');
dom.getResult.textContent = '';
try {
const user = await request(`/${id}`);
dom.getResult.textContent = JSON.stringify(user, null, 2);
setStatus(dom.getStatus, 'Готово');
} catch (error) {
dom.getResult.textContent = '';
setStatus(dom.getStatus, error.message, true);
}
};

const handleCreateUser = async (event) => {
event.preventDefault();
const payload = {
name: dom.createName.value.trim(),
email: dom.createEmail.value.trim(),
age: Number(dom.createAge.value)
};

if (!payload.name || !payload.email || Number.isNaN(payload.age)) {
setStatus(dom.createStatus, 'Заполните все поля', true);
return;
}

setStatus(dom.createStatus, 'Создание пользователя...');
try {
const created = await request('', {
method: 'POST',
body: JSON.stringify(payload)
});
dom.createForm.reset();
setStatus(dom.createStatus, `Пользователь создан (ID: ${created.id})`);
await refreshUsers();
} catch (error) {
setStatus(dom.createStatus, error.message, true);
}
};

const handleUpdateUser = async (event) => {
event.preventDefault();
const id = dom.updateId.value.trim();
const payload = {
name: dom.updateName.value.trim(),
email: dom.updateEmail.value.trim(),
age: Number(dom.updateAge.value)
};

if (!id || !payload.name || !payload.email || Number.isNaN(payload.age)) {
setStatus(dom.updateStatus, 'Заполните все поля', true);
return;
}

setStatus(dom.updateStatus, 'Обновление пользователя...');
try {
await request(`/${id}`, {
method: 'PUT',
body: JSON.stringify(payload)
});
setStatus(dom.updateStatus, 'Пользователь обновлён');
await refreshUsers();
} catch (error) {
setStatus(dom.updateStatus, error.message, true);
}
};

const handleDeleteUser = async (event) => {
event.preventDefault();
const id = dom.deleteId.value.trim();
if (!id) {
setStatus(dom.deleteStatus, 'Укажите ID пользователя', true);
return;
}

setStatus(dom.deleteStatus, 'Удаление пользователя...');
try {
await request(`/${id}`, { method: 'DELETE' });
dom.deleteForm.reset();
setStatus(dom.deleteStatus, 'Пользователь удалён');
await refreshUsers();
} catch (error) {
setStatus(dom.deleteStatus, error.message, true);
}
};

dom.refreshBtn?.addEventListener('click', refreshUsers);
dom.getForm?.addEventListener('submit', handleGetUser);
dom.createForm?.addEventListener('submit', handleCreateUser);
dom.updateForm?.addEventListener('submit', handleUpdateUser);
dom.deleteForm?.addEventListener('submit', handleDeleteUser);

// Попытаться получить пользователей сразу, но ошибки не показывать пользователю
refreshUsers().catch(() => {});
Loading
Loading