diff --git a/.github/actions/setup-jdk/action.yml b/.github/actions/setup-jdk/action.yml new file mode 100644 index 0000000..209dfce --- /dev/null +++ b/.github/actions/setup-jdk/action.yml @@ -0,0 +1,12 @@ +name: 'Setup JDK 25' +description: 'Sets up Java 25 with Temurin and Maven cache' + +runs: + using: "composite" + steps: + - name: Set up JDK 25 + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'temurin' + cache: maven \ No newline at end of file diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml deleted file mode 100644 index 8a04a22..0000000 --- a/.github/workflows/maven.yml +++ /dev/null @@ -1,35 +0,0 @@ -# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Java CI with Maven - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up JDK 25 - uses: actions/setup-java@v4 - with: - java-version: '25' - distribution: 'temurin' - cache: maven - - name: Build with Maven - run: mvn -B package --file pom.xml - - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6 diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..f2b4b73 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,36 @@ +name: PR pipeline + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + + +jobs: + build: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK + uses: ./.github/actions/setup-jdk + - name: Build with Maven + run: mvn -B package --file pom.xml + - name: Update dependency graph + uses: advanced-security/maven-dependency-submission-action@v5 + continue-on-error: true + with: + directory: . + maven-args: "-DmergeClassifiers=true" + + format: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up JDK + uses: ./.github/actions/setup-jdk + - name: Check formatting + run: mvn -B spotless:check --file pom.xml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 04d477a..16714c0 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,67 @@ buildNumber.properties # JDT-specific (Eclipse Java Development Tools) .classpath -.vscode \ No newline at end of file +.vscode +.idea + +tools/scenicview/ + +# General +.DS_Store +.localized +__MACOSX/ +.AppleDouble +.LSOverride +Icon[ +] + +# Resource forks +._* + +# Files and directories that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.com.apple.timemachine.supported +.PKInstallSandboxManager +.PKInstallSandboxManager-SystemSoftware +.hotfiles.btree +.vol +.file +.disk_label* +lost+found +.HFS+ Private Directory Data[ +] + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Mac OS 6 to 9 +Desktop DB +Desktop DF +TheFindByContentFolder +TheVolumeSettingsFolder +.FBCIndex +.FBCSemaphoreFile +.FBCLockFolder + +# Quota system +.quota.group +.quota.user +.quota.ops.group +.quota.ops.user + +# TimeMachine +Backups.backupdb +.MobileBackups +.MobileBackups.trash +MobileBackups.trash +tmbootpicker.efi diff --git a/README.md b/README.md index accc46e..c6f75b3 100644 --- a/README.md +++ b/README.md @@ -1 +1,5 @@ -# DesktopApplicationClientJava \ No newline at end of file +# DesktopApplicationClientJava + +```bash +git pull --rebase +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 002b18a..b224d9e 100644 --- a/pom.xml +++ b/pom.xml @@ -12,9 +12,7 @@ DesktopApplicationClientJava DesktopApplicationClientJava - - https://github.com/vladcheck/DesktopApplicationClientJava - + https://github.com/vladcheck/DesktopApplicationClientJava UTF-8 @@ -25,10 +23,57 @@ + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.17.0 + + + org.testfx + openjfx-monocle + 21.0.2 + test + + + org.testfx + testfx-junit5 + 4.0.18 + test + + + org.testfx + testfx-core + 4.0.18 + test + + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + + + org.hamcrest + hamcrest + 2.2 + test + org.openjfx javafx-controls - 11 + 27 + + + org.openjfx + javafx-fxml + 27 org.projectlombok @@ -36,15 +81,79 @@ 1.18.48 provided + + com.dlsc.formsfx + formsfx-core + 11.6.0 + + + org.openjfx + * + + + + + net.synedra + validatorfx + 0.6.1 + + + org.openjfx + * + + + + + org.kordamp.bootstrapfx + bootstrapfx-core + 0.4.0 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.11.0 + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + -Dtestfx.headless=true + -Dtestfx.robot=glass + -Dglass.platform=Monocle + -Dmonocle.platform=Headless + -Dprism.order=sw + -Dprism.text=t2k + -Djava.awt.headless=true + --enable-native-access=ALL-UNNAMED + + + org.apache.maven.plugins maven-compiler-plugin 3.14.1 + + + org.projectlombok + lombok + 1.18.48 + + + + -proc:full + 25 @@ -56,6 +165,30 @@ com.DesktopApplicationClientJava.Program + + com.diffplug.spotless + spotless-maven-plugin + 3.4.0 + + + + 1.35.0 + + + + + + + + + spotless-check + verify + + check + + + + \ No newline at end of file diff --git a/scenicView.properties b/scenicView.properties new file mode 100644 index 0000000..8309a28 --- /dev/null +++ b/scenicView.properties @@ -0,0 +1,19 @@ +#ScenicView properties +#Sun Sep 27 00:49:41 MSK 2026 +autoRefreshStyleSheets=false +automaticScenegraphStructureRefreshing=true +collapseContainerControls=false +collapseControls=true +ignoreMouseTransparentNodes=true +registerShortcuts=true +showBaseline=false +showBounds=true +showCSSProperties=false +showDefaultProperties=true +showFilteredNodesInTree=true +showInvisibleNodes=false +showNodesIdInTree=false +showSearchBar=true +splitPaneDividerPosition=0.308125 +stageHeight=800.0 +stageWidth=800.0 diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java b/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java new file mode 100644 index 0000000..66afc37 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java @@ -0,0 +1,543 @@ +package com.DesktopApplicationClientJava.Api; + +import com.DesktopApplicationClientJava.Api.Dto.error.ErrorResponse; +import com.DesktopApplicationClientJava.Api.Dto.request.Admin.RegisterRequest; +import com.DesktopApplicationClientJava.Api.Dto.request.Admin.UpdateUserRequest; +import com.DesktopApplicationClientJava.Api.Dto.request.Admin.UserFilterRequest; +import com.DesktopApplicationClientJava.Api.Dto.request.Auth.LoginRequest; +import com.DesktopApplicationClientJava.Api.Dto.request.Auth.RefreshTokenRequest; +import com.DesktopApplicationClientJava.Api.Dto.request.Resource.CreateResourceRequest; +import com.DesktopApplicationClientJava.Api.Dto.request.Resource.ResourceFilterRequest; +import com.DesktopApplicationClientJava.Api.Dto.request.Resource.UpdateResourceRequest; +import com.DesktopApplicationClientJava.Api.Dto.response.FileDto; +import com.DesktopApplicationClientJava.Api.Dto.response.Jwt.JwtTokenResponse; +import com.DesktopApplicationClientJava.Api.Dto.response.ResourceDto; +import com.DesktopApplicationClientJava.Api.Dto.response.UserDto; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class ApiClient { + private final HttpClient httpClient; + private final ObjectMapper objectMapper; + private final String baseUrl; + + private String accessToken; + private String refreshToken; + + public ApiClient(String baseUrl) { + this.baseUrl = baseUrl; + this.httpClient = HttpClient.newHttpClient(); + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new JavaTimeModule()); + } + + // Вход в систему + public JwtTokenResponse login(LoginRequest request) throws IOException, InterruptedException { + String jsonBody = objectMapper.writeValueAsString(request); + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/auth/login")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + JwtTokenResponse tokenResponse = parseResponse(response, JwtTokenResponse.class); + this.accessToken = tokenResponse.accessToken(); + this.refreshToken = tokenResponse.refreshToken(); + return tokenResponse; + } + + public JwtTokenResponse refresh() throws IOException, InterruptedException { + refreshInternal(); + return new JwtTokenResponse(accessToken, refreshToken); + } + + private void refreshInternal() throws IOException, InterruptedException { + if (refreshToken == null) { + throw new ApiException(new ErrorResponse("Ошибка: нет refresh токена", 401)); + } + + String jsonBody = objectMapper.writeValueAsString(new RefreshTokenRequest(refreshToken)); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/auth/refresh")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() >= 400) { + throw new ApiException( + new ErrorResponse("Refresh failed: " + response.body(), response.statusCode())); + } + + JwtTokenResponse tokens = objectMapper.readValue(response.body(), JwtTokenResponse.class); + this.accessToken = tokens.accessToken(); + this.refreshToken = tokens.refreshToken(); + } + + // Выход из системы + public void logout() throws IOException, InterruptedException { + if (accessToken == null) { + return; + } + String jsonBody = objectMapper.writeValueAsString(new RefreshTokenRequest(refreshToken)); + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/auth/logout")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + this.accessToken = null; + this.refreshToken = null; + } + + // Получение информации пользователя о себе + public UserDto getMe() throws IOException, InterruptedException { + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/user/me")) + .header("Authorization", "Bearer " + accessToken) + .GET() + .build(); + + HttpResponse response = send(httpRequest); + checkError(response); + return parseResponse(response, UserDto.class); + } + + // Работа с Ресурсом(Документом) + public ResourceDto createResource(CreateResourceRequest request) + throws IOException, InterruptedException { + String jsonBody = objectMapper.writeValueAsString(request); + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/resource")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + return parseResponse(response, ResourceDto.class); + } + + // Поиск ресурса по Id + public ResourceDto findResourceById(UUID id) throws IOException, InterruptedException { + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/resource/" + id)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .GET() + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + return parseResponse(response, ResourceDto.class); + } + + // Поиск ресурса по параметрам + public List findResourcesByFilters( + ResourceFilterRequest filter, Long offset, Long count, String sortBy, String sortDir) + throws IOException, InterruptedException { + List params = new ArrayList<>(); + if (filter != null) { + if (filter.title() != null && !filter.title().isBlank()) + params.add("title=" + encode(filter.title())); + if (filter.description() != null && !filter.description().isBlank()) + params.add("description=" + encode(filter.description())); + // createdAt/updatedAt пока не отправляем — при необходимости добавим формат + } + if (offset != null) params.add("offset=" + offset); + if (count != null) params.add("count=" + count); + if (sortBy != null) params.add("sortBy=" + sortBy); + if (sortDir != null) params.add("sortDir=" + sortDir); + String url = + baseUrl + "/api/v1/resource" + (params.isEmpty() ? "" : "?" + String.join("&", params)); + + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Authorization", "Bearer " + accessToken) + .GET() + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + + ResourceDto[] arr = objectMapper.readValue(response.body(), ResourceDto[].class); + return Arrays.asList(arr); + } + + // Обновление ресурса + public ResourceDto updateResource(UUID uuid, UpdateResourceRequest request) + throws IOException, InterruptedException { + String jsonBody = objectMapper.writeValueAsString(request); + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/resource/" + uuid)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .PUT(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + + HttpResponse response = send(httpRequest); + checkError(response); + return parseResponse(response, ResourceDto.class); + } + + // Удаление ресурса + public void deleteResource(UUID uuid) throws IOException, InterruptedException { + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/resource/" + uuid)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .DELETE() + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + } + + // Создание пользователя + public UserDto createUser(RegisterRequest request) throws IOException, InterruptedException { + String jsonBody = objectMapper.writeValueAsString(request); + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/admin")) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + return parseResponse(response, UserDto.class); + } + + // Поиск пользователя по ID + public UserDto getUserById(UUID uuid) throws IOException, InterruptedException { + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/admin/" + uuid)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .GET() + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + return parseResponse(response, UserDto.class); + } + + // Поиск пользователя по параметрам + public List getUsersByFilters( + UserFilterRequest filter, Long offset, Long count, String sortBy, String sortDir) + throws IOException, InterruptedException { + List params = new ArrayList<>(); + if (filter != null) { + if (filter.email() != null && !filter.email().isBlank()) + params.add("email=" + encode(filter.email())); + if (filter.firstName() != null && !filter.firstName().isBlank()) + params.add("firstName=" + encode(filter.firstName())); + if (filter.lastName() != null && !filter.lastName().isBlank()) + params.add("lastName=" + encode(filter.lastName())); + if (filter.role() != null) params.add("role=" + encode(filter.role().name())); + if (filter.enabled() != null) params.add("enabled=" + filter.enabled()); + } + + if (offset != null) params.add("offset=" + offset); + if (count != null) params.add("count=" + count); + if (sortBy != null) params.add("sortBy=" + encode(sortBy)); + if (sortDir != null) params.add("sortDir=" + encode(sortDir)); + + String url = + baseUrl + "/api/v1/admin" + (params.isEmpty() ? "" : "?" + String.join("&", params)); + + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Authorization", "Bearer " + accessToken) + .GET() + .build(); + + HttpResponse response = send(httpRequest); + checkError(response); + UserDto[] users = objectMapper.readValue(response.body(), UserDto[].class); + return Arrays.asList(users); + } + + // Обновление пользователя + public UserDto updateUser(UUID uuid, UpdateUserRequest request) + throws IOException, InterruptedException { + String jsonBody = objectMapper.writeValueAsString(request); + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/admin/" + uuid)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .PUT(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + return parseResponse(response, UserDto.class); + } + + // Удаление пользователя + public void deleteUser(UUID uuid) throws IOException, InterruptedException { + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/admin/" + uuid)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .DELETE() + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + } + + public FileDto uploadFile(UUID resourceUuid, Path filePath) + throws IOException, InterruptedException { + String boundary = "----JavaPksBoundary" + System.currentTimeMillis(); + byte[] fileBytes = Files.readAllBytes(filePath); + String filename = filePath.getFileName().toString(); + String contentType = + Optional.ofNullable(Files.probeContentType(filePath)).orElse("application/octet-stream"); + + ByteArrayOutputStream body = new ByteArrayOutputStream(); + + body.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8)); + body.write( + ("Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\n") + .getBytes(StandardCharsets.UTF_8)); + body.write(("Content-Type: " + contentType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); + body.write(fileBytes); + body.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8)); + + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/resource/" + resourceUuid + "/file")) + .header("Content-Type", "multipart/form-data; boundary=" + boundary) + .header("Authorization", "Bearer " + accessToken) + .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray())) + .build(); + + HttpResponse response = send(httpRequest); + checkError(response); + return parseResponse(response, FileDto.class); + } + + public Path downloadFile(UUID resourceUuid, UUID fileUuid, Path destinationDir) + throws IOException, InterruptedException { + + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/resource/" + resourceUuid + "/file/" + fileUuid)) + .header("Authorization", "Bearer " + accessToken) + .GET() + .build(); + + HttpResponse response = + executeWithRetry(httpRequest, HttpResponse.BodyHandlers.ofInputStream()); + + if (response.statusCode() >= 400) { + String body = new String(response.body().readAllBytes(), StandardCharsets.UTF_8); + response.body().close(); + try { + ErrorResponse error = objectMapper.readValue(body, ErrorResponse.class); + throw new ApiException(error); + } catch (ApiException e) { + throw e; + } catch (Exception parseFail) { + throw new ApiException(new ErrorResponse("Ошибка сервера: " + body, response.statusCode())); + } + } + + String filename = + parseFilename(response.headers().firstValue("Content-Disposition").orElse(null)); + if (filename == null || filename.isBlank()) { + filename = fileUuid.toString(); + } + + Files.createDirectories(destinationDir); + Path target = destinationDir.resolve(filename); + + try (InputStream in = response.body()) { + Files.copy(in, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + return target; + } + + public void deleteFile(UUID resourceUuid, UUID fileUuid) + throws IOException, InterruptedException { + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/api/v1/resource/" + resourceUuid + "/file/" + fileUuid)) + .header("Authorization", "Bearer " + accessToken) + .DELETE() + .build(); + HttpResponse response = send(httpRequest); + checkError(response); + } + + private static String parseFilename(String cd) { + if (cd == null) return null; + + int starIdx = cd.indexOf("filename*="); + if (starIdx >= 0) { + String val = cd.substring(starIdx + "filename*=".length()).trim(); + int semi = val.indexOf(';'); + if (semi >= 0) val = val.substring(0, semi).trim(); + int encIdx = val.indexOf("''"); + if (encIdx >= 0) { + String encoded = val.substring(encIdx + 2); + return URLDecoder.decode(encoded, StandardCharsets.UTF_8); + } + } + + int idx = cd.indexOf("filename="); + if (idx >= 0) { + String val = cd.substring(idx + "filename=".length()).trim(); + if (val.startsWith("\"")) { + int end = val.indexOf('"', 1); + if (end > 0) return val.substring(1, end); + } else { + int semi = val.indexOf(';'); + if (semi >= 0) val = val.substring(0, semi); + return val.trim(); + } + } + return null; + } + + // Выгрузка данных (пользователей, документов и их файлов) + public byte[] exportUsers() throws IOException, InterruptedException { + return downloadBinary("/api/v1/admin/users/export"); + } + + public byte[] exportResources() throws IOException, InterruptedException { + return downloadBinary("/api/v1/admin/resource/export"); + } + + public byte[] exportResourceFiles(UUID resourceId) throws IOException, InterruptedException { + return downloadBinary("/api/v1/admin/resource/" + resourceId + "/files/export"); + } + + private byte[] downloadBinary(String path) throws IOException, InterruptedException { + HttpRequest httpRequest = + HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Authorization", "Bearer " + accessToken) + .GET() + .build(); + + HttpResponse response = + executeWithRetry(httpRequest, HttpResponse.BodyHandlers.ofByteArray()); + + if (response.statusCode() >= 400) { + String body = new String(response.body(), StandardCharsets.UTF_8); + try { + ErrorResponse error = objectMapper.readValue(body, ErrorResponse.class); + throw new ApiException(error); + } catch (ApiException e) { + throw e; + } catch (Exception parseFail) { + throw new ApiException(new ErrorResponse("Ошибка сервера: " + body, response.statusCode())); + } + } + return response.body(); + } + + private HttpResponse send(HttpRequest request) throws IOException, InterruptedException { + return executeWithRetry(request, HttpResponse.BodyHandlers.ofString()); + } + + // Проверка ошибок сервера + private void checkError(HttpResponse response) { + if (response.statusCode() < 400) return; + try { + ErrorResponse error = objectMapper.readValue(response.body(), ErrorResponse.class); + throw new ApiException(error); + } catch (ApiException e) { + throw e; + } catch (Exception parseFail) { + throw new ApiException( + new ErrorResponse("Ошибка сервера: " + response.body(), response.statusCode())); + } + } + + // Парсер ответа + private T parseResponse(HttpResponse response, Class clazz) throws IOException { + if (response.body() == null || response.body().isBlank()) { + return null; + } + return objectMapper.readValue(response.body(), clazz); + } + + private HttpResponse executeWithRetry( + HttpRequest request, HttpResponse.BodyHandler handler) + throws IOException, InterruptedException { + + HttpResponse response = httpClient.send(request, handler); + + if (response.statusCode() != 401) return response; + + if (request.uri().getPath().startsWith("/api/v1/auth/")) return response; + + if (refreshToken == null) return response; + + try { + refreshInternal(); + } catch (Exception e) { + // refresh не удался — отдаём оригинальный 401, вызывающий код разберётся + return response; + } + + // повторяем исходный запрос с новым access-токеном + HttpRequest retried = + HttpRequest.newBuilder(request, (name, value) -> !name.equalsIgnoreCase("Authorization")) + .header("Authorization", "Bearer " + accessToken) + .build(); + + return httpClient.send(retried, handler); + } + + private static String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + public String getAccessToken() { + return accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public HttpClient getHttpClient() { + return httpClient; + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public String getBaseUrl() { + return baseUrl; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/ApiConfig.java b/src/main/java/com/DesktopApplicationClientJava/Api/ApiConfig.java new file mode 100644 index 0000000..e434338 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/ApiConfig.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.Api; + +public class ApiConfig {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/ApiException.java b/src/main/java/com/DesktopApplicationClientJava/Api/ApiException.java new file mode 100644 index 0000000..06ba69f --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/ApiException.java @@ -0,0 +1,20 @@ +package com.DesktopApplicationClientJava.Api; + +import com.DesktopApplicationClientJava.Api.Dto.error.ErrorResponse; + +public class ApiException extends RuntimeException { + private final ErrorResponse errorResponse; + + public ApiException(ErrorResponse errorResponse) { + super(errorResponse.message()); + this.errorResponse = errorResponse; + } + + public ErrorResponse getErrorResponse() { + return errorResponse; + } + + public int getStatus() { + return errorResponse.status(); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/error/ErrorResponse.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/error/ErrorResponse.java new file mode 100644 index 0000000..9a563cb --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/error/ErrorResponse.java @@ -0,0 +1,7 @@ +package com.DesktopApplicationClientJava.Api.Dto.error; + +public record ErrorResponse(String message, int status, long timestamp) { + public ErrorResponse(String message, int status) { + this(message, status, System.currentTimeMillis()); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/RegisterRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/RegisterRequest.java new file mode 100644 index 0000000..43d8021 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/RegisterRequest.java @@ -0,0 +1,10 @@ +package com.DesktopApplicationClientJava.Api.Dto.request.Admin; + +import com.DesktopApplicationClientJava.entities.Role; + +public record RegisterRequest( + String email, + String password, + Role role, // "USER", "MODER" или "ADMIN" + String firstname, + String lastname) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UpdateUserRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UpdateUserRequest.java new file mode 100644 index 0000000..4ac2708 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UpdateUserRequest.java @@ -0,0 +1,6 @@ +package com.DesktopApplicationClientJava.Api.Dto.request.Admin; + +import com.DesktopApplicationClientJava.entities.Role; + +public record UpdateUserRequest( + String email, Boolean enabled, Role role, String firstname, String lastname) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UserFilterRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UserFilterRequest.java new file mode 100644 index 0000000..cb3af19 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Admin/UserFilterRequest.java @@ -0,0 +1,13 @@ +package com.DesktopApplicationClientJava.Api.Dto.request.Admin; + +import com.DesktopApplicationClientJava.entities.Role; +import java.time.LocalDateTime; + +public record UserFilterRequest( + String email, + Role role, + String firstName, + String lastName, + Boolean enabled, + LocalDateTime createdAt, + LocalDateTime updatedAt) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/LoginRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/LoginRequest.java new file mode 100644 index 0000000..95cddd9 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/LoginRequest.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.Api.Dto.request.Auth; + +public record LoginRequest(String email, String password) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/RefreshTokenRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/RefreshTokenRequest.java new file mode 100644 index 0000000..7543162 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Auth/RefreshTokenRequest.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.Api.Dto.request.Auth; + +public record RefreshTokenRequest(String refreshToken) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/CreateResourceRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/CreateResourceRequest.java new file mode 100644 index 0000000..19672e1 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/CreateResourceRequest.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.Api.Dto.request.Resource; + +public record CreateResourceRequest(String title, String description) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/ResourceFilterRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/ResourceFilterRequest.java new file mode 100644 index 0000000..3333123 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/ResourceFilterRequest.java @@ -0,0 +1,6 @@ +package com.DesktopApplicationClientJava.Api.Dto.request.Resource; + +import java.time.LocalDateTime; + +public record ResourceFilterRequest( + String title, String description, LocalDateTime createdAt, LocalDateTime updatedAt) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/UpdateResourceRequest.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/UpdateResourceRequest.java new file mode 100644 index 0000000..ceffbc2 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/request/Resource/UpdateResourceRequest.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.Api.Dto.request.Resource; + +public record UpdateResourceRequest(String title, String description) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/FileDto.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/FileDto.java new file mode 100644 index 0000000..5facf5e --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/FileDto.java @@ -0,0 +1,5 @@ +package com.DesktopApplicationClientJava.Api.Dto.response; + +import java.util.UUID; + +public record FileDto(UUID uuid, String name, String contentType, Long size) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/Jwt/JwtTokenResponse.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/Jwt/JwtTokenResponse.java new file mode 100644 index 0000000..5e4b709 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/Jwt/JwtTokenResponse.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.Api.Dto.response.Jwt; + +public record JwtTokenResponse(String accessToken, String refreshToken) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/ResourceDto.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/ResourceDto.java new file mode 100644 index 0000000..cb4d46e --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/ResourceDto.java @@ -0,0 +1,13 @@ +package com.DesktopApplicationClientJava.Api.Dto.response; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +public record ResourceDto( + UUID uuid, + String title, + String description, + LocalDateTime createdAt, + LocalDateTime updatedAt, + List files) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/StoredFile.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/StoredFile.java new file mode 100644 index 0000000..383680b --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/StoredFile.java @@ -0,0 +1,15 @@ +package com.DesktopApplicationClientJava.Api.Dto.response; + +import java.io.IOException; +import java.io.InputStream; + +public record StoredFile(InputStream content, String contentType, long size) + implements AutoCloseable { + + @Override + public void close() throws IOException { + if (content != null) { + content.close(); + } + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java new file mode 100644 index 0000000..ea2d3a9 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java @@ -0,0 +1,15 @@ +package com.DesktopApplicationClientJava.Api.Dto.response; + +import com.DesktopApplicationClientJava.entities.Role; +import java.time.LocalDateTime; +import java.util.UUID; + +public record UserDto( + UUID uuid, + String email, + Role role, + String firstName, + String lastName, + Boolean enabled, + LocalDateTime createdAt, + LocalDateTime updatedAt) {} diff --git a/src/main/java/com/DesktopApplicationClientJava/Main.java b/src/main/java/com/DesktopApplicationClientJava/Main.java new file mode 100644 index 0000000..249f023 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/Main.java @@ -0,0 +1,9 @@ +package com.DesktopApplicationClientJava; + +import javafx.application.Application; + +public class Main { + public static void main(String[] args) { + Application.launch(Program.class, args); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/Program.java b/src/main/java/com/DesktopApplicationClientJava/Program.java index deea9b6..c5bbe90 100644 --- a/src/main/java/com/DesktopApplicationClientJava/Program.java +++ b/src/main/java/com/DesktopApplicationClientJava/Program.java @@ -1,30 +1,38 @@ package com.DesktopApplicationClientJava; +import com.DesktopApplicationClientJava.session.Session; +import com.DesktopApplicationClientJava.utils.ControllerWiring; +import java.io.IOException; import javafx.application.Application; -import javafx.scene.Group; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; import javafx.scene.Scene; -import javafx.scene.text.Text; import javafx.stage.Stage; public class Program extends Application { - public static void main(String[] args) { - launch(args); - } + private final Session session = new Session(); + private static final double windowWidthPx = 400.0; + private static final double windowHeightPx = 400.0; - @Override - public void start(Stage stage) { - Text text = new Text("Hello JAVAFX!"); - text.setLayoutX(80); - text.setLayoutY(80); + @Override + public void start(Stage stage) throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/LoginForm.fxml")); + Parent root = loader.load(); + Scene scene = new Scene(root); + loadStyleSheet(scene, "/styles.css"); - Group group = new Group(text); - Scene scene = new Scene(group); + ControllerWiring.wire(loader.getController(), stage, session); - stage.setScene(scene); - stage.setTitle("JavaFX Application"); - stage.setWidth(300); - stage.setHeight(250); + stage.setScene(scene); + stage.setTitle("JavaFX Application"); + stage.setWidth(windowWidthPx); + stage.setHeight(windowHeightPx); + stage.centerOnScreen(); - stage.show(); - } -} \ No newline at end of file + stage.show(); + } + + private void loadStyleSheet(Scene scene, String path) { + scene.getStylesheets().add(getClass().getResource(path).toExternalForm()); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java new file mode 100644 index 0000000..d8baf11 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/LoginFormController.java @@ -0,0 +1,108 @@ +package com.DesktopApplicationClientJava.controllers; + +import com.DesktopApplicationClientJava.controllers.utils.Controller; +import com.DesktopApplicationClientJava.entities.User; +import com.DesktopApplicationClientJava.utils.FieldError; +import com.DesktopApplicationClientJava.validation.EmailValidator; +import com.DesktopApplicationClientJava.validation.FirstNameValidator; +import com.DesktopApplicationClientJava.validation.LastNameValidator; +import com.DesktopApplicationClientJava.validation.PasswordValidator; +import com.DesktopApplicationClientJava.validation.ThirdNameValidator; +import com.DesktopApplicationClientJava.validation.utils.Validator; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; + +public class LoginFormController extends Controller { + @FXML private TextField firstNameField; + @FXML private Label firstNameErrorLabel; + private FieldError firstNameError; + + @FXML private TextField lastNameField; + @FXML private Label lastNameErrorLabel; + private FieldError lastNameError; + + @FXML private TextField thirdNameField; + @FXML private Label thirdNameErrorLabel; + private FieldError thirdNameError; + + @FXML private TextField emailField; + @FXML private Label emailErrorLabel; + private FieldError emailError; + + @FXML private PasswordField passwordField; + @FXML private Label passwordErrorLabel; + private FieldError passwordError; + + @FXML private Button submitButton; + + @FXML + private void initialize() { + firstNameError = new FieldError(firstNameErrorLabel); + lastNameError = new FieldError(lastNameErrorLabel); + thirdNameError = new FieldError(thirdNameErrorLabel); + emailError = new FieldError(emailErrorLabel); + passwordError = new FieldError(passwordErrorLabel); + + emailField.textProperty().addListener((o, a, b) -> emailError.hide()); + passwordField.textProperty().addListener((o, a, b) -> passwordError.hide()); + } + + @FXML + private void onSubmit(ActionEvent event) { + if (!isValid()) return; + + User user = + authenticate(firstNameField.getText(), lastNameField.getText(), emailField.getText()); + if (user == null) return; + + session.setCurrentUser(user); + navigator.goTo("/fxml/Search.fxml"); + } + + private boolean isValid() { + return isFirstNameValid() + & isLastNameValid() + & isThirdNameValid() + & isEmailValid() + & isPasswordValid(); + } + + private boolean isFirstNameValid() { + return isFieldValid(firstNameField, new FirstNameValidator(), firstNameError); + } + + private boolean isLastNameValid() { + return isFieldValid(lastNameField, new LastNameValidator(), lastNameError); + } + + private boolean isThirdNameValid() { + return isFieldValid(thirdNameField, new ThirdNameValidator(), thirdNameError); + } + + private boolean isEmailValid() { + return isFieldValid(emailField, new EmailValidator(), emailError); + } + + private boolean isPasswordValid() { + return isFieldValid(passwordField, new PasswordValidator(), passwordError); + } + + private boolean isFieldValid(TextField field, Validator validator, FieldError errorLabel) { + String input = field.getText(); + var error = validator.isValid(input); + if (error == null) { + errorLabel.hide(); + return true; + } + errorLabel.show(error); + return false; + } + + private User authenticate(String firstName, String lastName, String email) { + return new User(firstName, lastName, email); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/SearchController.java b/src/main/java/com/DesktopApplicationClientJava/controllers/SearchController.java new file mode 100644 index 0000000..9112fdd --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/SearchController.java @@ -0,0 +1,5 @@ +package com.DesktopApplicationClientJava.controllers; + +import com.DesktopApplicationClientJava.controllers.utils.Controller; + +public class SearchController extends Controller {} diff --git a/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java new file mode 100644 index 0000000..34b2bc4 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/controllers/utils/Controller.java @@ -0,0 +1,19 @@ +package com.DesktopApplicationClientJava.controllers.utils; + +import com.DesktopApplicationClientJava.navigation.Navigator; +import com.DesktopApplicationClientJava.navigation.NavigatorAware; +import com.DesktopApplicationClientJava.session.Session; +import com.DesktopApplicationClientJava.session.SessionAware; + +public abstract class Controller implements SessionAware, NavigatorAware { + protected Session session; + protected Navigator navigator; + + public void setSession(Session session) { + this.session = session; + } + + public void setNavigator(Navigator navigator) { + this.navigator = navigator; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/entities/Role.java b/src/main/java/com/DesktopApplicationClientJava/entities/Role.java new file mode 100644 index 0000000..992a4ed --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/entities/Role.java @@ -0,0 +1,7 @@ +package com.DesktopApplicationClientJava.entities; + +public enum Role { + USER, + MODER, + ADMIN +} diff --git a/src/main/java/com/DesktopApplicationClientJava/entities/User.java b/src/main/java/com/DesktopApplicationClientJava/entities/User.java new file mode 100644 index 0000000..dfc6d99 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/entities/User.java @@ -0,0 +1,15 @@ +package com.DesktopApplicationClientJava.entities; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** User */ +@AllArgsConstructor +@Getter +@Setter +public class User { + private String firstName; + private String lastName; + private String email; +} diff --git a/src/main/java/com/DesktopApplicationClientJava/navigation/Navigator.java b/src/main/java/com/DesktopApplicationClientJava/navigation/Navigator.java new file mode 100644 index 0000000..3f84fc7 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/navigation/Navigator.java @@ -0,0 +1,38 @@ +package com.DesktopApplicationClientJava.navigation; + +import com.DesktopApplicationClientJava.session.Session; +import com.DesktopApplicationClientJava.session.SessionAware; +import java.io.IOException; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.stage.Stage; +import lombok.AllArgsConstructor; + +/** Navigator */ +@AllArgsConstructor +public class Navigator { + private final Stage stage; + private final Session session; + + public void goTo(String fxmlFile) { + var url = Navigator.class.getResource(fxmlFile); + if (url == null) { + throw new IllegalArgumentException("FXML not found on classpath: " + fxmlFile); + } + + FXMLLoader loader = new FXMLLoader(url); + try { + Parent root = loader.load(); + var controller = loader.getController(); + if (controller instanceof SessionAware sa) { + sa.setSession(session); + } + if (controller instanceof NavigatorAware na) { + na.setNavigator(this); + } + stage.getScene().setRoot(root); + } catch (IOException e) { + throw new RuntimeException("Failed to load FXML: " + fxmlFile, e); + } + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/navigation/NavigatorAware.java b/src/main/java/com/DesktopApplicationClientJava/navigation/NavigatorAware.java new file mode 100644 index 0000000..f4a4bc8 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/navigation/NavigatorAware.java @@ -0,0 +1,5 @@ +package com.DesktopApplicationClientJava.navigation; + +public interface NavigatorAware { + void setNavigator(Navigator navigator); +} diff --git a/src/main/java/com/DesktopApplicationClientJava/services/AuthService.java b/src/main/java/com/DesktopApplicationClientJava/services/AuthService.java new file mode 100644 index 0000000..40667ff --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/services/AuthService.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.services; + +public class AuthService {} diff --git a/src/main/java/com/DesktopApplicationClientJava/services/FileService.java b/src/main/java/com/DesktopApplicationClientJava/services/FileService.java new file mode 100644 index 0000000..6f616cf --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/services/FileService.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.services; + +public class FileService {} diff --git a/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java b/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java new file mode 100644 index 0000000..cabf91c --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/services/ResourceService.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.services; + +public class ResourceService {} diff --git a/src/main/java/com/DesktopApplicationClientJava/services/UsersService.java b/src/main/java/com/DesktopApplicationClientJava/services/UsersService.java new file mode 100644 index 0000000..018573e --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/services/UsersService.java @@ -0,0 +1,3 @@ +package com.DesktopApplicationClientJava.services; + +public class UsersService {} diff --git a/src/main/java/com/DesktopApplicationClientJava/session/Session.java b/src/main/java/com/DesktopApplicationClientJava/session/Session.java new file mode 100644 index 0000000..352b636 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/session/Session.java @@ -0,0 +1,21 @@ +package com.DesktopApplicationClientJava.session; + +import com.DesktopApplicationClientJava.entities.User; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class Session { + private User currentUser; + + public User getCurrentUser() { + return currentUser; + } + + public void setCurrentUser(User user) { + this.currentUser = user; + } + + public boolean isLoggedIn() { + return currentUser != null; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/session/SessionAware.java b/src/main/java/com/DesktopApplicationClientJava/session/SessionAware.java new file mode 100644 index 0000000..83a8e4f --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/session/SessionAware.java @@ -0,0 +1,5 @@ +package com.DesktopApplicationClientJava.session; + +public interface SessionAware { + void setSession(Session session); +} diff --git a/src/main/java/com/DesktopApplicationClientJava/utils/ControllerWiring.java b/src/main/java/com/DesktopApplicationClientJava/utils/ControllerWiring.java new file mode 100644 index 0000000..f986f72 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/utils/ControllerWiring.java @@ -0,0 +1,16 @@ +package com.DesktopApplicationClientJava.utils; + +import com.DesktopApplicationClientJava.navigation.Navigator; +import com.DesktopApplicationClientJava.navigation.NavigatorAware; +import com.DesktopApplicationClientJava.session.Session; +import com.DesktopApplicationClientJava.session.SessionAware; +import javafx.stage.Stage; + +public final class ControllerWiring { + private ControllerWiring() {} + + public static void wire(Object controller, Stage stage, Session session) { + if (controller instanceof SessionAware sa) sa.setSession(session); + if (controller instanceof NavigatorAware na) na.setNavigator(new Navigator(stage, session)); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/utils/FieldError.java b/src/main/java/com/DesktopApplicationClientJava/utils/FieldError.java new file mode 100644 index 0000000..cafa2bf --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/utils/FieldError.java @@ -0,0 +1,28 @@ +package com.DesktopApplicationClientJava.utils; + +import javafx.scene.control.Label; + +public class FieldError { + private final Label label; + + public FieldError(Label label) { + this.label = label; + } + + public void show(String message) { + if (label == null) return; + label.setText(message); + label.setVisible(true); + label.setManaged(true); + } + + public void hide() { + if (label == null) return; + label.setVisible(false); + label.setManaged(false); + } + + public boolean isShowing() { + return label.isVisible(); + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/EmailValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/EmailValidator.java new file mode 100644 index 0000000..75e0d99 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/EmailValidator.java @@ -0,0 +1,21 @@ +package com.DesktopApplicationClientJava.validation; + +import com.DesktopApplicationClientJava.validation.utils.AbstractValidator; +import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; +import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers; + +public class EmailValidator extends AbstractValidator { + public static final String FIELD_NAME = "Почта"; + public static final int MIN_EMAIL_LENGTH = 5; + public static final int MAX_EMAIL_LENGTH = 100; + + public String isValid(String input) { + if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required; + if (ValidatorHelpers.isShorterThan(input, MIN_EMAIL_LENGTH)) + return ErrorMessages.MinLength(FIELD_NAME, MIN_EMAIL_LENGTH); + if (ValidatorHelpers.isLongerThan(input, MAX_EMAIL_LENGTH)) + return ErrorMessages.MaxLength(FIELD_NAME, MAX_EMAIL_LENGTH); + if (!ValidatorHelpers.includes(input, "@")) return ErrorMessages.IncorrectFormat; + return null; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/FirstNameValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/FirstNameValidator.java new file mode 100644 index 0000000..5f9516f --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/FirstNameValidator.java @@ -0,0 +1,17 @@ +package com.DesktopApplicationClientJava.validation; + +import com.DesktopApplicationClientJava.validation.utils.AbstractValidator; +import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; +import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers; + +public class FirstNameValidator extends AbstractValidator { + public static final String FIELD_NAME = "Имя"; + private static final int MIN_LENGTH = 2; + + public String isValid(String input) { + if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required; + if (ValidatorHelpers.isShorterThan(input, MIN_LENGTH)) + return ErrorMessages.MinLength(FIELD_NAME, MIN_LENGTH); + return null; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/LastNameValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/LastNameValidator.java new file mode 100644 index 0000000..933e658 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/LastNameValidator.java @@ -0,0 +1,17 @@ +package com.DesktopApplicationClientJava.validation; + +import com.DesktopApplicationClientJava.validation.utils.AbstractValidator; +import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; +import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers; + +public class LastNameValidator extends AbstractValidator { + public static final String FIELD_NAME = "Фамилия"; + public static final int MIN_LENGTH = 2; + + public String isValid(String input) { + if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required; + if (ValidatorHelpers.isShorterThan(input, MIN_LENGTH)) + return ErrorMessages.MinLength(FIELD_NAME, MIN_LENGTH); + return null; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/PasswordValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/PasswordValidator.java new file mode 100644 index 0000000..a9e2c97 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/PasswordValidator.java @@ -0,0 +1,20 @@ +package com.DesktopApplicationClientJava.validation; + +import com.DesktopApplicationClientJava.validation.utils.AbstractValidator; +import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; +import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers; + +public class PasswordValidator extends AbstractValidator { + public static final String FIELD_NAME = "Пароль"; + public static final int MIN_LENGTH = 8; + public static final int MAX_LENGTH = 64; + + public String isValid(String input) { + if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required; + if (ValidatorHelpers.isShorterThan(input, MIN_LENGTH)) + return ErrorMessages.MinLength(FIELD_NAME, MIN_LENGTH); + if (ValidatorHelpers.isLongerThan(input, MAX_LENGTH)) + return ErrorMessages.MaxLength(FIELD_NAME, MAX_LENGTH); + return null; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/ThirdNameValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/ThirdNameValidator.java new file mode 100644 index 0000000..fd3aa2f --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/ThirdNameValidator.java @@ -0,0 +1,15 @@ +package com.DesktopApplicationClientJava.validation; + +import com.DesktopApplicationClientJava.validation.utils.AbstractValidator; +import com.DesktopApplicationClientJava.validation.utils.ErrorMessages; +import com.DesktopApplicationClientJava.validation.utils.ValidatorHelpers; + +public class ThirdNameValidator extends AbstractValidator { + public static final String FIELD_NAME = "Отчество"; + + @Override + public String isValid(String input) { + if (ValidatorHelpers.isEmpty(input)) return ErrorMessages.Required; + return null; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/utils/AbstractValidator.java b/src/main/java/com/DesktopApplicationClientJava/validation/utils/AbstractValidator.java new file mode 100644 index 0000000..3a921e8 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/utils/AbstractValidator.java @@ -0,0 +1,5 @@ +package com.DesktopApplicationClientJava.validation.utils; + +public abstract class AbstractValidator implements Validator { + public abstract String isValid(String input); +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/utils/ErrorMessages.java b/src/main/java/com/DesktopApplicationClientJava/validation/utils/ErrorMessages.java new file mode 100644 index 0000000..51a3b71 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/utils/ErrorMessages.java @@ -0,0 +1,14 @@ +package com.DesktopApplicationClientJava.validation.utils; + +public class ErrorMessages { + public static final String Required = "Это поле обязательно для заполнения"; + public static final String IncorrectFormat = "Неверный формат"; + + public static final String MinLength(String fieldName, int minLength) { + return fieldName + " не может быть короче, чем " + minLength + " символов"; + } + + public static final String MaxLength(String fieldName, int maxLength) { + return fieldName + " не может быть длиннее, чем " + maxLength + " символов"; + } +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/utils/Validator.java b/src/main/java/com/DesktopApplicationClientJava/validation/utils/Validator.java new file mode 100644 index 0000000..081eef7 --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/utils/Validator.java @@ -0,0 +1,6 @@ +package com.DesktopApplicationClientJava.validation.utils; + +@FunctionalInterface +public interface Validator { + String isValid(String input); +} diff --git a/src/main/java/com/DesktopApplicationClientJava/validation/utils/ValidatorHelpers.java b/src/main/java/com/DesktopApplicationClientJava/validation/utils/ValidatorHelpers.java new file mode 100644 index 0000000..6db826b --- /dev/null +++ b/src/main/java/com/DesktopApplicationClientJava/validation/utils/ValidatorHelpers.java @@ -0,0 +1,25 @@ +package com.DesktopApplicationClientJava.validation.utils; + +public class ValidatorHelpers { + public static boolean isEmpty(String input) { + return input == null || input.isBlank(); + } + + public static boolean isShorterThan(String input, int min) { + return input.length() < min; + } + + public static boolean isLongerThan(String input, int max) { + return input.length() > max; + } + + /** + * @param input input string + * @param sub substring + * @return true if input includes a substring sub, + * false otherwise + */ + public static boolean includes(String input, String sub) { + return input.contains(sub); + } +} diff --git a/src/main/resources/fxml/LoginForm.fxml b/src/main/resources/fxml/LoginForm.fxml new file mode 100644 index 0000000..34fbb3c --- /dev/null +++ b/src/main/resources/fxml/LoginForm.fxml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + +