From f73e133782a9979d4a1578ae50e4e4e3d1300f13 Mon Sep 17 00:00:00 2001 From: Iakov Lysenko Date: Sun, 9 Nov 2025 00:06:47 +0300 Subject: [PATCH] frontend added --- docker-compose.yml | 17 +- frontend/app.js | 201 ++++++++++++++++ frontend/index.html | 223 ++++++++++++++++++ .../userservice/configuration/CorsConfig.java | 23 ++ 4 files changed, 454 insertions(+), 10 deletions(-) create mode 100644 frontend/app.js create mode 100644 frontend/index.html create mode 100644 user-service/user-service-impl/src/main/java/ru/project/iakov/userservice/configuration/CorsConfig.java diff --git a/docker-compose.yml b/docker-compose.yml index ffff7c7..0d224f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" @@ -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: @@ -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: diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..9dc18af --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,201 @@ +const API_BASE_URL = 'http://localhost:8080/users'; + +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 = ` + + Нет данных. Создайте нового пользователя или обновите список. + + `; + return; + } + + dom.usersBody.innerHTML = users.map((user) => ` + + ${user.id} + ${user.name} + ${user.email} + ${user.age} + + `).join(''); +}; + +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(() => {}); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..990eeb9 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,223 @@ + + + + + + User Service UI + + + +
+
+

User Service CRUD

+

Набор простых форм для проверки API: создавайте, просматривайте, обновляйте и удаляйте пользователей.

+
+ +
+
+ +
+
+ + + + + + + + + + + + + + +
IDИмяEmailВозраст
Нажмите «Загрузить всех пользователей», чтобы получить данные.
+
+ +
+

Найти пользователя

+
+ + +
+
+

+    
+ +
+

Создать пользователя

+
+ + + + +
+
+
+ +
+

Обновить пользователя

+
+ + + + + +
+
+
+ +
+

Удалить пользователя

+
+ + +
+
+
+
+ + + + diff --git a/user-service/user-service-impl/src/main/java/ru/project/iakov/userservice/configuration/CorsConfig.java b/user-service/user-service-impl/src/main/java/ru/project/iakov/userservice/configuration/CorsConfig.java new file mode 100644 index 0000000..83a63c6 --- /dev/null +++ b/user-service/user-service-impl/src/main/java/ru/project/iakov/userservice/configuration/CorsConfig.java @@ -0,0 +1,23 @@ +package ru.project.iakov.userservice.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Enables CORS for local frontend testing. + * + * @author Iakov Lysenko + */ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("http://localhost:*", "http://127.0.0.1:*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(false); + } +}