Initial commit

This commit is contained in:
deytenit committed 2025-02-18 12:36:26 +03:00
commit 98b777a9f6
32 files changed
+1950

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
# Environment
.env
# IntelliJ
.idea
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
+26
View File
@@ -0,0 +1,26 @@
# Use an official Maven image to build the application
FROM maven:3.9-eclipse-temurin-23-alpine AS build
# Set the working directory in the container
WORKDIR /app
# Copy the source code into the container
COPY . /app
# Run Maven to package the application (this will generate a JAR file in the target/ folder)
RUN mvn clean package -DskipTests
# Use a lightweight JDK image to run the app
FROM eclipse-temurin:23-alpine
# Set the working directory for the final image
WORKDIR /app
# Copy the JAR file from the build stage
COPY --from=build /app/target/*.jar /app/w-server.jar
# Expose port of Spring Boot app
EXPOSE 8090
# Run the Spring Boot application
ENTRYPOINT ["java", "-jar", "w-server.jar"]
+26
View File
@@ -0,0 +1,26 @@
version: '3.8'
services:
w-server:
build: .
ports:
- "80:8090"
env_file: .env
depends_on:
- mariadb
mariadb:
image: mariadb:10.5
container_name: mariadb
env_file: .env
ports:
- "3306:3306"
phpmyadmin:
image: phpmyadmin/phpmyadmin
restart: always
env_file: .env
ports:
- "8080:80"
depends_on:
- mariadb
+95
View File
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ermnvldmr.w</groupId>
<artifactId>w-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>W</name>
<description>Server for the W network</description>
<properties>
<java.version>23</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>properties-maven-plugin</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<phase>initialize</phase>
<goals>
<goal>read-project-properties</goal>
</goals>
<configuration>
<files>
<file>profile.properties</file>
</files>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
View File
Whitespace-only changes.
@@ -0,0 +1,34 @@
package com.ermnvldmr.w;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Component
@SuppressWarnings("unused")
public class DatabaseInitializer implements ApplicationRunner {
private final JdbcTemplate jdbcTemplate;
public DatabaseInitializer(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void run(ApplicationArguments args) {
String columnName = "password_sha";
String tableName = "user";
// Check if the column already exists
String checkColumnSql = "SELECT COUNT(*) FROM information_schema.COLUMNS " +
"WHERE TABLE_NAME = ? AND COLUMN_NAME = ?";
Integer count = jdbcTemplate.queryForObject(checkColumnSql, new Object[]{tableName, columnName}, Integer.class);
// If the column does not exist, add it
if (count != null && count == 0) {
String sql = "ALTER TABLE `" + tableName + "` ADD `" + columnName + "` VARCHAR(64) NULL AFTER `login`;";
jdbcTemplate.execute(sql);
}
}
}
@@ -0,0 +1,14 @@
package com.ermnvldmr.w;
import com.ermnvldmr.w.config.PropertiesConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(PropertiesConfig.class)
public class WApplication {
public static void main(String[] args) {
SpringApplication.run(com.ermnvldmr.w.WApplication.class, args);
}
}
@@ -0,0 +1,37 @@
package com.ermnvldmr.w.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "config")
@SuppressWarnings("unused")
public class PropertiesConfig {
private String mediaDir;
private String jwtSecret;
private String shaSalt;
public String getMediaDir() {
return mediaDir;
}
public void setMediaDir(String mediaDir) {
this.mediaDir = mediaDir;
}
public String getJwtSecret() {
return jwtSecret;
}
public void setJwtSecret(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
public String getShaSalt() {
return shaSalt;
}
public void setShaSalt(String shaSalt) {
this.shaSalt = shaSalt;
}
}
@@ -0,0 +1,164 @@
package com.ermnvldmr.w.controller;
import com.ermnvldmr.w.domain.Discussion;
import com.ermnvldmr.w.domain.Post;
import com.ermnvldmr.w.domain.User;
import com.ermnvldmr.w.service.DiscussionService;
import com.ermnvldmr.w.service.UserService;
import com.ermnvldmr.w.service.PostService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.ermnvldmr.w.utils.Utils.getValidationErrors;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@RestController
@RequestMapping("/api")
@SuppressWarnings("unused")
public class DiscussionController {
private final DiscussionService discussionService;
private final PostService postService;
private final UserService userService;
public DiscussionController(DiscussionService discussionService,
PostService postService,
UserService userService) {
this.discussionService = discussionService;
this.postService = postService;
this.userService = userService;
}
@Operation(summary = "Find discussions by post ID",
description = "Retrieves a list of discussions associated with the given post ID.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved discussions"),
@ApiResponse(responseCode = "404", description = "Post not found")
})
@GetMapping(value = {"/discussions/post/{id}"})
public ResponseEntity<List<Discussion>> findDiscussions(
@Parameter(description = "ID of the post to find discussions for", required = true)
@PathVariable long id) {
Post post = postService.findById(id);
if (post == null) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
List<Discussion> discussions = discussionService.findAllByPostId(post.getId());
return ResponseEntity.ok(discussions); // 200 OK
}
@Operation(summary = "Write a new discussion",
description = "Creates a new discussion related to a post. Optionally, it can be a reply to an existing discussion.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Discussion created successfully"),
@ApiResponse(responseCode = "400", description = "Validation errors or parent discussion not found"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "404", description = "Post not found")
})
@PostMapping(value = {"/discussions/post/{id}"})
public ResponseEntity<Object> writeDiscussion(
@Parameter(description = "ID of the post to write the discussion for", required = true)
@PathVariable long id,
@Parameter(description = "Discussion object to be created", required = true)
@Valid @RequestBody Discussion discussion,
@Parameter(description = "JWT token for user authentication", required = true)
@RequestParam String jwt,
@Parameter(description = "Optional parent discussion ID for nested discussions")
@RequestParam(required = false) String parentDiscussionId,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(getValidationErrors(bindingResult)); // 400 Bad Request
}
Post post = postService.findById(id);
if (post == null) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
User user = userService.findByJwt(jwt);
if (user == null) {
return ResponseEntity.status(401).body(null); // 401 Unauthorized
}
if (parentDiscussionId != null) {
Discussion parentDiscussion = discussionService.findById(Long.parseLong(parentDiscussionId));
if (parentDiscussion == null) {
return ResponseEntity.badRequest().build(); // 404 Not Found
}
discussion.setParentDiscussion(parentDiscussion);
}
discussion.setUser(user);
discussion.setPost(post);
Discussion savedDiscussion = discussionService.writeDiscussion(discussion);
return ResponseEntity.status(201).body(savedDiscussion); // 201 Created
}
@Operation(summary = "Delete a discussion",
description = "Deletes the specified discussion. Only the author or an admin can perform this action.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Discussion deleted successfully"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "403", description = "Forbidden action"),
@ApiResponse(responseCode = "404", description = "Discussion not found")
})
@DeleteMapping("discussions/{id}")
public ResponseEntity<Void> deleteDiscussion(
@Parameter(description = "ID of the discussion to delete", required = true)
@PathVariable long id,
@Parameter(description = "JWT token for authentication", required = true)
@RequestParam String jwt) {
User authenticatedUser = userService.findByJwt(jwt);
if (authenticatedUser == null) {
return ResponseEntity.status(401).build(); // 401 Unauthorized
}
Discussion discussionToDelete = discussionService.findById(id);
if (discussionToDelete == null) {
return ResponseEntity.status(404).build(); // 404 Not found
}
if (authenticatedUser.getId() != discussionToDelete.getUser().getId() && !authenticatedUser.isAdmin()) {
return ResponseEntity.status(403).build(); // 403 Forbidden
}
discussionService.deleteDiscussionById(id);
return ResponseEntity.ok().build(); // 200 Ok
}
@Operation(summary = "Retrieve all discussions by user ID",
description = "Fetches a list of discussions created by a specific user.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved user's discussions"),
@ApiResponse(responseCode = "400", description = "Invalid user ID provided"),
@ApiResponse(responseCode = "404", description = "No discussions found for the specified user")
})
@GetMapping("/discussions/user/{userId}")
public ResponseEntity<List<Discussion>> findDiscussionsByUserId(
@Parameter(description = "ID of the user whose discussions are to be retrieved", required = true)
@PathVariable Long userId) {
if (userId == null || userId <= 0) {
return ResponseEntity.badRequest().build(); // 400 Bad Request
}
List<Discussion> discussions = discussionService.findAllByUserId(userId);
if (discussions == null || discussions.isEmpty()) {
return ResponseEntity.status(404).build(); // 404 Not Found
}
return ResponseEntity.ok(discussions); // 200 OK
}
}
@@ -0,0 +1,166 @@
package com.ermnvldmr.w.controller;
import com.ermnvldmr.w.domain.Post;
import com.ermnvldmr.w.domain.User;
import com.ermnvldmr.w.form.WritePostForm;
import com.ermnvldmr.w.service.PostService;
import com.ermnvldmr.w.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.List;
import static com.ermnvldmr.w.utils.Utils.getValidationErrors;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@RestController
@RequestMapping("/api")
@SuppressWarnings("unused")
public class PostController {
private final PostService postService;
private final UserService userService;
public PostController(PostService postService, UserService userService) {
this.postService = postService;
this.userService = userService;
}
@Operation(summary = "Retrieve all posts",
description = "Fetches a list of all posts.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved posts")
})
@GetMapping("posts")
public ResponseEntity<List<Post>> findPosts() {
List<Post> posts = postService.findAll();
return ResponseEntity.ok(posts); // 200 OK
}
@Operation(summary = "Create a new post",
description = "Writes a new post. Requires user authentication via JWT.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Post created successfully"),
@ApiResponse(responseCode = "400", description = "Validation errors"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "500", description = "Internal server error due to media upload failure")
})
@PostMapping("posts")
public ResponseEntity<Object> writePost(
@Parameter(description = "Post data to create a new post", required = true)
@Valid @RequestBody WritePostForm postForm,
@Parameter(description = "JWT token for user authentication", required = true)
@RequestParam String jwt,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(getValidationErrors(bindingResult)); // 400 Bad Request
}
User user = userService.findByJwt(jwt);
if (user == null) {
return ResponseEntity.status(401).build(); // 401 Unauthorized
}
try {
Post createdPost = postService.writePost(postForm, user);
return ResponseEntity.status(201).body(createdPost); // 201 Created
} catch (IOException e) {
return ResponseEntity.status(500).body("Media upload failed."); // 500 Internal Server Error
}
}
@Operation(summary = "Find a post by ID",
description = "Retrieves a specific post by its ID, incrementing its view count.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved post"),
@ApiResponse(responseCode = "404", description = "Post not found")
})
@GetMapping(value = {"/posts/{id}"})
public ResponseEntity<Post> findPost(
@Parameter(description = "ID of the post to retrieve", required = true)
@PathVariable long id,
@Parameter(description = "JWT token for view mapping")
@RequestParam String jwt,
HttpServletRequest request) {
final String viewerIdentifier = getViewerIdentifier(jwt, request);
Post post = postService.getAndIncrementViewCount(id, viewerIdentifier);
if (post == null) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
return ResponseEntity.ok(post); // 200 OK
}
@Operation(summary = "Delete a post",
description = "Deletes the specified post. Only the author or an admin can perform this action.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Post deleted successfully"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "403", description = "Forbidden action"),
@ApiResponse(responseCode = "404", description = "Post not found")
})
@DeleteMapping("posts/{id}")
public ResponseEntity<Void> deletePost(
@Parameter(description = "ID of the post to delete", required = true)
@PathVariable long id,
@Parameter(description = "JWT token for authentication", required = true)
@RequestParam String jwt) {
User authenticatedUser = userService.findByJwt(jwt);
if (authenticatedUser == null) {
return ResponseEntity.status(401).build(); // 401 Unauthorized
}
Post postToDelete = postService.findById(id);
if (postToDelete == null) {
return ResponseEntity.status(404).build(); // 404 Not found
}
if (authenticatedUser.getId() != postToDelete.getUser().getId() && !authenticatedUser.isAdmin()) {
return ResponseEntity.status(403).build(); // 403 Forbidden
}
postService.deletePostById(id);
return ResponseEntity.ok().build(); // 200 OK
}
@Operation(summary = "Retrieve all posts by user ID",
description = "Fetches a list of posts created by a specific user.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved user's posts"),
@ApiResponse(responseCode = "400", description = "Invalid user ID provided"),
@ApiResponse(responseCode = "404", description = "No posts found for the specified user")
})
@GetMapping("/posts/user/{userId}")
public ResponseEntity<List<Post>> findPostsByUserId(
@Parameter(description = "ID of the user whose posts are to be retrieved", required = true)
@PathVariable Long userId) {
if (userId == null || userId <= 0) {
return ResponseEntity.badRequest().build(); // 400 Bad Request
}
List<Post> posts = postService.findAllByUserId(userId);
if (posts == null || posts.isEmpty()) {
return ResponseEntity.status(404).build(); // 404 Not Found
}
return ResponseEntity.ok(posts); // 200 OK
}
private String getViewerIdentifier(String jwt, HttpServletRequest request) {
if (jwt != null && !jwt.isBlank()) {
User user = userService.findByJwt(jwt);
if (user != null) {
return "user-" + user.getId();
}
}
return "ip-" + request.getRemoteAddr();
}
}
@@ -0,0 +1,193 @@
package com.ermnvldmr.w.controller;
import com.ermnvldmr.w.domain.User;
import com.ermnvldmr.w.form.UserCredentialsEditForm;
import com.ermnvldmr.w.form.UserCredentialsEnter;
import com.ermnvldmr.w.form.UserCredentialsRegister;
import com.ermnvldmr.w.service.UserService;
import com.ermnvldmr.w.form.validator.UserCredentialsEnterValidator;
import com.ermnvldmr.w.form.validator.UserCredentialsRegisterValidator;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static com.ermnvldmr.w.utils.Utils.getValidationErrors;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@RestController
@RequestMapping("/api")
@SuppressWarnings("unused")
public class UserController {
private final UserService userService;
private final UserCredentialsEnterValidator userCredentialsEnterValidator;
private final UserCredentialsRegisterValidator userCredentialsRegisterValidator;
public UserController(UserService userService,
UserCredentialsEnterValidator userCredentialsEnterValidator,
UserCredentialsRegisterValidator userCredentialsRegisterValidator) {
this.userService = userService;
this.userCredentialsEnterValidator = userCredentialsEnterValidator;
this.userCredentialsRegisterValidator = userCredentialsRegisterValidator;
}
@InitBinder("userCredentialsEnter")
public void initEnterBinder(WebDataBinder binder) {
binder.addValidators(userCredentialsEnterValidator);
}
@InitBinder("userCredentialsRegister")
public void initRegisterBinder(WebDataBinder binder) {
binder.addValidators(userCredentialsRegisterValidator);
}
@Operation(summary = "Authenticate user and retrieve JWT",
description = "Validates user credentials and returns a JWT if successful.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "JWT retrieved successfully"),
@ApiResponse(responseCode = "400", description = "Validation errors"),
@ApiResponse(responseCode = "401", description = "Wrong login or password")
})
@PostMapping("users/enter")
public ResponseEntity<Object> findUserJwt(
@Parameter(description = "User credentials for login", required = true)
@RequestBody @Valid UserCredentialsEnter userCredentials,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(getValidationErrors(bindingResult)); // 400 Bad Request
}
User user = userService.findByLoginAndPassword(userCredentials.getLogin(), userCredentials.getPassword());
if (user == null) {
return ResponseEntity.status(401).body("Wrong login or password."); // 401 Unauthorized
}
String jwt = userService.createUserJwt(user);
return ResponseEntity.ok(jwt); // 200 OK
}
@Operation(summary = "Retrieve user by JWT",
description = "Finds a user based on the provided JWT.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User retrieved successfully"),
@ApiResponse(responseCode = "404", description = "User not found")
})
@GetMapping("users/jwt")
public ResponseEntity<User> findUserByJwt(
@Parameter(description = "JWT for the user", required = true)
@RequestParam String jwt) {
User user = userService.findByJwt(jwt);
if (user == null) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
return ResponseEntity.ok(user); // 200 OK
}
@Operation(summary = "Retrieve all users",
description = "Fetches a list of all registered users.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successfully retrieved users")
})
@GetMapping("users")
public ResponseEntity<List<User>> findUsers() {
List<User> users = this.userService.findAll();
return ResponseEntity.ok(users); // 200 OK
}
@Operation(summary = "Register a new user",
description = "Creates a new user account using the provided credentials.")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "User created successfully"),
@ApiResponse(responseCode = "400", description = "Validation errors")
})
@PostMapping("users/register")
public ResponseEntity<Object> addUser(
@Parameter(description = "User credentials for registration", required = true)
@RequestBody @Valid UserCredentialsRegister userCredentials,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(getValidationErrors(bindingResult)); // 400 Bad Request
}
User newUser = userService.addUserByCredentials(userCredentials);
return ResponseEntity.status(201).body(newUser); // 201 Created
}
@Operation(summary = "Delete a user",
description = "Deletes the specified user. Only the user themselves or an admin can perform this action.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User deleted successfully"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "403", description = "Forbidden action"),
@ApiResponse(responseCode = "404", description = "User not found")
})
@DeleteMapping("users/{id}")
public ResponseEntity<Void> deleteUser(
@Parameter(description = "ID of the user to delete", required = true)
@PathVariable long id,
@Parameter(description = "JWT token for authentication", required = true)
@RequestParam String jwt) {
User authenticatedUser = userService.findByJwt(jwt);
if (authenticatedUser == null) {
return ResponseEntity.status(401).build(); // 41 Unauthorized
}
User userToDelete = userService.findById(id);
if (userToDelete == null) {
return ResponseEntity.status(404).build(); // 404 Not found
}
if (authenticatedUser.getId() != userToDelete.getId() && !authenticatedUser.isAdmin()) {
return ResponseEntity.status(403).build(); // 403 Forbidden
}
userService.deleteUserById(id);
return ResponseEntity.ok().build(); // 200 Ok
}
@Operation(summary = "Edit user profile",
description = "Allows a user to edit their profile or an admin to edit any user's profile.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "User profile updated successfully"),
@ApiResponse(responseCode = "400", description = "Validation errors"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "403", description = "Forbidden action"),
@ApiResponse(responseCode = "404", description = "User not found")
})
@PutMapping("users/{id}")
public ResponseEntity<Object> editUserProfile(
@Parameter(description = "ID of the user to edit", required = true) @PathVariable long id,
@Parameter(description = "Updated user profile", required = true) @RequestBody @Valid UserCredentialsEditForm userProfileEdit,
BindingResult bindingResult,
@Parameter(description = "JWT token for authentication", required = true) @RequestParam String jwt) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(getValidationErrors(bindingResult)); // 400 Bad Request
}
User authenticatedUser = userService.findByJwt(jwt);
if (authenticatedUser == null) {
return ResponseEntity.status(401).build(); // 401 Unauthorized
}
User userToEdit = userService.findById(id);
if (userToEdit == null) {
return ResponseEntity.status(404).build(); // 404 Not Found
}
if (authenticatedUser.getId() != userToEdit.getId() && !authenticatedUser.isAdmin()) {
return ResponseEntity.status(403).build(); // 403 Forbidden
}
userToEdit.setName(userProfileEdit.getName());
userService.saveUser(userToEdit);
return ResponseEntity.ok(userToEdit); // 200 OK
}
}
@@ -0,0 +1,155 @@
package com.ermnvldmr.w.controller;
import com.ermnvldmr.w.domain.Discussion;
import com.ermnvldmr.w.domain.Post;
import com.ermnvldmr.w.domain.User;
import com.ermnvldmr.w.domain.Vote;
import com.ermnvldmr.w.service.DiscussionService;
import com.ermnvldmr.w.service.PostService;
import com.ermnvldmr.w.service.UserService;
import com.ermnvldmr.w.service.VoteService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
@RestController
@RequestMapping("/api")
@SuppressWarnings("unused")
public class VoteController {
private final DiscussionService discussionService;
private final PostService postService;
private final UserService userService;
private final VoteService voteService;
public VoteController(DiscussionService discussionService,
PostService postService,
UserService userService,
VoteService voteService) {
this.discussionService = discussionService;
this.postService = postService;
this.userService = userService;
this.voteService = voteService;
}
@Operation(summary = "Vote on a post",
description = "Allows a user to upvote or downvote a post.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Vote recorded successfully"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "404", description = "Post not found")
})
@PostMapping("/votes/post/{id}")
public ResponseEntity<Object> votePost(
@Parameter(description = "ID of the post to vote on", required = true)
@PathVariable long id,
@Parameter(description = "JWT of the user", required = true)
@RequestParam String jwt,
@Parameter(description = "True for upvote, false for downvote", required = true)
@RequestParam boolean upvote) {
User user = userService.findByJwt(jwt);
if (user == null) {
return ResponseEntity.status(401).build(); // 401 Unauthorized
}
Post post = postService.findById(id);
if (post == null) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
Vote vote = voteService.votePost(post, user, upvote);
return ResponseEntity.ok(vote); // Return 200 OK with the vote details
}
@Operation(summary = "Vote on a discussion",
description = "Allows a user to upvote or downvote a discussion.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Vote recorded successfully"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "404", description = "Discussion not found")
})
@PostMapping("/votes/discussion/{id}")
public ResponseEntity<Object> voteDiscussion(
@Parameter(description = "ID of the discussion to vote on", required = true)
@PathVariable long id,
@Parameter(description = "JWT of the user", required = true)
@RequestParam String jwt,
@Parameter(description = "True for upvote, false for downvote", required = true)
@RequestParam boolean upvote) {
User user = userService.findByJwt(jwt);
if (user == null) {
return ResponseEntity.status(401).build(); // 401 Unauthorized
}
Discussion discussion = discussionService.findById(id);
if (discussion == null) {
return ResponseEntity.notFound().build(); // 404 Not Found
}
Vote vote = voteService.voteDiscussion(discussion, user, upvote);
return ResponseEntity.ok(vote); // Return 200 OK with the vote details
}
@Operation(summary = "Get user's vote on a post",
description = "Fetches the vote status for a specific post.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Vote details retrieved successfully"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "404", description = "Post not found")
})
@GetMapping("/votes/post/{id}")
public ResponseEntity<Object> getPostVote(
@Parameter(description = "ID of the post to check the vote", required = true)
@PathVariable long id,
@Parameter(description = "JWT of the user", required = true)
@RequestParam String jwt) {
User user = userService.findByJwt(jwt);
if (user == null) {
return ResponseEntity.status(401).build(); // 401 Unauthorized
}
Post post = postService.findById(id);
if (post == null) {
return ResponseEntity.badRequest().build(); // 404 Not Found
}
// Fetch the vote status for the post
Vote vote = voteService.findByPostAndUser(post, user);
return ResponseEntity.ok(vote); // Return 200 OK with the vote details or null if no vote
}
@Operation(summary = "Get user's vote on a discussion",
description = "Fetches the vote status for a specific discussion.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Vote details retrieved successfully"),
@ApiResponse(responseCode = "401", description = "Unauthorized access"),
@ApiResponse(responseCode = "404", description = "Discussion not found")
})
@GetMapping("/votes/discussion/{id}")
public ResponseEntity<Object> getDiscussionVote(
@Parameter(description = "ID of the discussion to check the vote", required = true)
@PathVariable long id,
@Parameter(description = "JWT of the user", required = true)
@RequestParam String jwt) {
User user = userService.findByJwt(jwt);
if (user == null) {
return ResponseEntity.status(401).build(); // 401 Unauthorized
}
Discussion discussion = discussionService.findById(id);
if (discussion == null) {
return ResponseEntity.badRequest().build(); // 401 Bad request
}
// Fetch the vote status for the discussion
Vote vote = voteService.findByDiscussionAndUser(discussion, user);
return ResponseEntity.ok(vote); // Return 200 OK with the vote details or null if no vote
}
}
@@ -0,0 +1,93 @@
package com.ermnvldmr.w.domain;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Entity
@Table(indexes = @Index(columnList = "creationTime"))
@SuppressWarnings("unused")
public class Discussion {
@Id
@GeneratedValue
private long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@OneToOne
@JoinColumn(name = "parent_discussion_id")
private Discussion parentDiscussion;
@NotNull
@NotBlank
@Size(min = 1, max = 65000)
@Lob
private String text;
@OneToMany(mappedBy = "discussion", cascade = CascadeType.ALL)
private final List<Vote> votes = new ArrayList<>();
@CreationTimestamp
private Date creationTime;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public void setPost(final Post post) {
this.post = post;
}
public Long getParentDiscussionId() {
return parentDiscussion != null ? parentDiscussion.getId() : null;
}
public void setParentDiscussion(Discussion parentDiscussion) {
this.parentDiscussion = parentDiscussion;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public long getVoteCount() {
return votes.stream().mapToLong((vote) -> vote.isUpvote() ? 1 : -1).sum();
}
public Date getCreationTime() {
return creationTime;
}
public void setCreationTime(Date creationTime) {
this.creationTime = creationTime;
}
}
@@ -0,0 +1,114 @@
package com.ermnvldmr.w.domain;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Entity
@SuppressWarnings("unused")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@NotEmpty
@Size(min = 1, max = 100)
private String title;
@NotEmpty
@Size(min = 1, max = 10000)
@Lob
private String text;
private List<String> media;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
@OrderBy("creationTime desc")
private List<Discussion> discussions = new ArrayList<>();
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private final List<Vote> votes = new ArrayList<>();
private long viewCount = 0;
@CreationTimestamp
private Date creationTime;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public List<String> getMedia() {
return media;
}
public void setMedia(List<String> media) {
this.media = media;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public void setDiscussions(final List<Discussion> discussions) {
this.discussions = discussions;
}
public long getDiscussionCount() {
return discussions.size();
}
public long getVoteCount() {
return votes.stream().mapToLong((vote) -> vote.isUpvote() ? 1 : -1).sum();
}
public long getViewCount() {
return viewCount;
}
public void incrementViewCount() {
this.viewCount++;
}
public Date getCreationTime() {
return creationTime;
}
public void setCreationTime(Date creationTime) {
this.creationTime = creationTime;
}
}
@@ -0,0 +1,83 @@
package com.ermnvldmr.w.domain;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "login"))
@SuppressWarnings("unused")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@NotEmpty
@Size(min = 1, max = 100)
private String name;
@NotEmpty
@Size(min = 2, max = 24)
@Pattern(regexp = "[a-zA-Z]{2,24}")
private String login;
private boolean admin;
@CreationTimestamp
private Date creationTime;
@OneToMany(mappedBy = "user")
private final List<Post> posts = new ArrayList<>();
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public boolean isAdmin() {
return admin;
}
public void setAdmin(boolean admin) {
this.admin = admin;
}
public Date getCreationTime() {
return creationTime;
}
public void setCreationTime(Date creationTime) {
this.creationTime = creationTime;
}
public void addPost(Post post) {
posts.add(post);
post.setUser(this);
}
}
@@ -0,0 +1,70 @@
package com.ermnvldmr.w.domain;
import jakarta.persistence.*;
@Entity
@SuppressWarnings("unused")
public class Vote {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
private boolean upvote; // true for upvote, false for downvote
@ManyToOne
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne
@JoinColumn(name = "discussion_id")
private Discussion discussion;
public boolean isValid() {
return (post == null) != (discussion == null); // XOR to ensure only one is set
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public boolean isUpvote() {
return upvote;
}
public void setUpvote(boolean upvote) {
this.upvote = upvote;
}
public Post getPost() {
return post;
}
public void setPost(Post post) {
this.post = post;
}
public Discussion getDiscussion() {
return discussion;
}
public void setDiscussion(Discussion discussion) {
this.discussion = discussion;
}
}
@@ -0,0 +1,18 @@
package com.ermnvldmr.w.form;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class UserCredentialsEditForm {
@NotBlank(message = "Name cannot not be blank")
@Size(min = 1, max = 64, message = "Name must be of size between 1 and 64 characters")
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,33 @@
package com.ermnvldmr.w.form;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public class UserCredentialsEnter {
@NotBlank(message = "Login cannot not be blank")
@Size(min = 2, max = 24, message = "Login must be of size between 2 and 24 characters")
@Pattern(regexp = "[a-zA-Z]{2,24}", message = "Login must only contain latin letters")
private String login;
@NotEmpty(message = "Password cannot be empty")
@Size(min = 8, max = 60, message = "Password must be of size between 8 and 60 characters")
private String password;
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@@ -0,0 +1,46 @@
package com.ermnvldmr.w.form;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public class UserCredentialsRegister {
@NotBlank(message = "Name cannot not be blank")
@Size(min = 1, max = 64, message = "Name must be of size between 1 and 64 characters")
private String name;
@NotBlank(message = "Login cannot not be blank")
@Size(min = 2, max = 24, message = "Login must be of size between 2 and 24 characters")
@Pattern(regexp = "[a-zA-Z]{2,24}", message = "Login must only contain latin letters")
private String login;
@NotEmpty(message = "Password cannot be empty")
@Size(min = 8, max = 60, message = "Password must be of size between 8 and 60 characters")
private String password;
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@@ -0,0 +1,44 @@
package com.ermnvldmr.w.form;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public class WritePostForm {
@NotBlank
@Size(min = 1, max = 100)
private String title;
@NotBlank
@Size(min = 1, max = 10000)
@Lob
private String text;
private List<MultipartFile> media;
public String getTitle() {
return title;
}
public void setTitle(final String title) {
this.title = title;
}
public String getText() {
return text;
}
public void setText(final String text) {
this.text = text;
}
public List<MultipartFile> getMedia() {
return media;
}
public void setImage(List<MultipartFile> media) {
this.media = media;
}
}
@@ -0,0 +1,29 @@
package com.ermnvldmr.w.form.validator;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.ermnvldmr.w.form.UserCredentialsEnter;
import com.ermnvldmr.w.service.UserService;
@Component
public class UserCredentialsEnterValidator implements Validator {
private final UserService userService;
public UserCredentialsEnterValidator(UserService userService) {
this.userService = userService;
}
public boolean supports(Class<?> clazz) {
return UserCredentialsEnter.class.equals(clazz);
}
public void validate(Object target, Errors errors) {
if (!errors.hasErrors()) {
UserCredentialsEnter enterForm = (UserCredentialsEnter) target;
if (userService.findByLoginAndPassword(enterForm.getLogin(), enterForm.getPassword()) == null) {
errors.reject("invalid-login-or-password", "Invalid login or password");
}
}
}
}
@@ -0,0 +1,29 @@
package com.ermnvldmr.w.form.validator;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.ermnvldmr.w.form.UserCredentialsRegister;
import com.ermnvldmr.w.service.UserService;
@Component
public class UserCredentialsRegisterValidator implements Validator {
private final UserService userService;
public UserCredentialsRegisterValidator(UserService userService) {
this.userService = userService;
}
public boolean supports(Class<?> clazz) {
return UserCredentialsRegister.class.equals(clazz);
}
public void validate(Object target, Errors errors) {
if (!errors.hasErrors()) {
UserCredentialsRegister registerForm = (UserCredentialsRegister) target;
if (userService.findByLogin(registerForm.getLogin()) != null) {
errors.reject("user-exists", "User with such login already exists");
}
}
}
}
@@ -0,0 +1,16 @@
package com.ermnvldmr.w.repository;
import com.ermnvldmr.w.domain.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.ermnvldmr.w.domain.Discussion;
import java.util.List;
public interface DiscussionRepository extends JpaRepository<Discussion, Long> {
@Query(value = "SELECT * FROM discussion WHERE post_id=?1", nativeQuery = true)
List<Discussion> findAllByPostId(long postId);
@Query(value = "SELECT * FROM discussion WHERE user_id=?1", nativeQuery = true)
List<Discussion> findAllByUserId(Long userId);
}
@@ -0,0 +1,14 @@
package com.ermnvldmr.w.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.ermnvldmr.w.domain.Post;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findAllByOrderByCreationTimeDesc();
@Query(value = "SELECT * FROM post WHERE user_id=?1", nativeQuery = true)
List<Post> findAllByUserId(Long userId);
}
@@ -0,0 +1,24 @@
package com.ermnvldmr.w.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import com.ermnvldmr.w.domain.User;
import java.util.List;
public interface UserRepository extends JpaRepository<User, Long> {
@Transactional
@Modifying
@Query(value = "UPDATE user SET password_sha=SHA1(CONCAT(?4, ?2, ?3)) WHERE id=?1", nativeQuery = true)
void updatePasswordSha(long id, String login, String password, String salt);
@Query(value = "SELECT * FROM user WHERE login=?1", nativeQuery = true)
User findByLogin(String login);
@Query(value = "SELECT * FROM user WHERE login=?1 AND password_sha=SHA1(CONCAT(?3, ?1, ?2))", nativeQuery = true)
User findByLoginAndPassword(String login, String password, String salt);
List<User> findAllByOrderByIdDesc();
}
@@ -0,0 +1,14 @@
package com.ermnvldmr.w.repository;
import com.ermnvldmr.w.domain.Discussion;
import com.ermnvldmr.w.domain.Post;
import com.ermnvldmr.w.domain.User;
import com.ermnvldmr.w.domain.Vote;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface VoteRepository extends JpaRepository<Vote, Long> {
Optional<Vote> findByUserAndPost(User user, Post post);
Optional<Vote> findByUserAndDiscussion(User user, Discussion discussion);
}
@@ -0,0 +1,35 @@
package com.ermnvldmr.w.service;
import com.ermnvldmr.w.domain.Post;
import org.springframework.stereotype.Service;
import com.ermnvldmr.w.domain.Discussion;
import com.ermnvldmr.w.repository.DiscussionRepository;
import java.util.List;
@Service
public class DiscussionService {
private final DiscussionRepository discussionRepository;
public DiscussionService(DiscussionRepository discussionRepository) {
this.discussionRepository = discussionRepository;
}
public Discussion findById(long id) {
return discussionRepository.findById(id).orElse(null);
}
public List<Discussion> findAllByPostId(long postId) { return discussionRepository.findAllByPostId(postId); }
public Discussion writeDiscussion(Discussion discussion) {
return discussionRepository.save(discussion);
}
public void deleteDiscussionById(long id) {
discussionRepository.deleteById(id);
}
public List<Discussion> findAllByUserId(Long userId) {
return discussionRepository.findAllByUserId(userId);
}
}
@@ -0,0 +1,133 @@
package com.ermnvldmr.w.service;
import com.ermnvldmr.w.config.PropertiesConfig;
import org.springframework.stereotype.Service;
import com.ermnvldmr.w.domain.Post;
import com.ermnvldmr.w.domain.User;
import com.ermnvldmr.w.form.WritePostForm;
import com.ermnvldmr.w.repository.PostRepository;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Service
public class PostService {
private final PropertiesConfig propertiesConfig;
private final PostRepository postRepository;
private final Map<Long, Map<String, Long>> viewTimestamps = new ConcurrentHashMap<>();
public PostService(PropertiesConfig propertiesConfig,
PostRepository postRepository) {
this.propertiesConfig = propertiesConfig;
this.postRepository = postRepository;
}
public Post findById(long id) {
return postRepository.findById(id).orElse(null);
}
public List<Post> findAll() {
return postRepository.findAllByOrderByCreationTimeDesc();
}
public Post writePost(WritePostForm form, User author) throws IOException {
Post post = new Post();
post.setTitle(form.getTitle());
post.setText(form.getText());
post.setMedia(saveMediaAndGetKeys(form.getMedia(), post.getId() + "_"));
post.setUser(author);
this.postRepository.save(post);
return post;
}
public Post getAndIncrementViewCount(Long postId, String viewerIdentifier) {
Post post = postRepository.findById(postId).orElse(null);
if (post != null && shouldIncrementView(postId, viewerIdentifier)) {
post.incrementViewCount();
postRepository.save(post);
}
return post;
}
private boolean shouldIncrementView(long postId, String viewerIdentifier) {
long currentTime = System.currentTimeMillis();
long viewLimit = TimeUnit.MINUTES.toMillis(10);
viewTimestamps.putIfAbsent(postId, new ConcurrentHashMap<>());
Map<String, Long> postViews = viewTimestamps.get(postId);
Long lastViewTime = postViews.get(viewerIdentifier);
if (lastViewTime == null || currentTime - lastViewTime > viewLimit) {
postViews.put(viewerIdentifier, currentTime);
return true;
}
return false;
}
private List<String> saveMediaAndGetKeys(List<MultipartFile> media, String keyPrefix) throws IOException {
List<String> keys = new ArrayList<>();
if (media == null || media.isEmpty()) {
return keys;
}
// Get the directory path from config properties
Path uploadDir = Paths.get(propertiesConfig.getMediaDir());
// Ensure the directory exists
if (!Files.exists(uploadDir)) {
Files.createDirectories(uploadDir);
}
for (int i = 0; i < media.size(); ++i) {
var file = media.get(i);
if (!file.isEmpty()) {
// Get the original filename to preserve the extension
String originalFilename = file.getOriginalFilename();
String extension = ""; // Default to an empty string
if (originalFilename != null) {
// Extract the file extension
int lastDotIndex = originalFilename.lastIndexOf('.');
if (lastDotIndex > 0 && lastDotIndex < originalFilename.length() - 1) {
extension = originalFilename.substring(lastDotIndex); // Include the dot
}
}
// Construct the full file name, preserving the file extension
String key = keyPrefix + i + extension;
Path path = uploadDir.resolve(key); // Combine directory and key
// Write the file
Files.write(path, file.getBytes());
keys.add(key);
}
}
return keys;
}
public void deletePostById(long id) {
postRepository.deleteById(id);
}
public List<Post> findAllByUserId(Long userId) {
return postRepository.findAllByUserId(userId);
}
}
@@ -0,0 +1,94 @@
package com.ermnvldmr.w.service;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.ermnvldmr.w.config.PropertiesConfig;
import org.springframework.stereotype.Service;
import com.ermnvldmr.w.domain.User;
import com.ermnvldmr.w.form.UserCredentialsRegister;
import com.ermnvldmr.w.repository.UserRepository;
import java.util.List;
@Service
public class UserService {
private final PropertiesConfig propertiesConfig;
private final UserRepository userRepository;
private final Algorithm algorithm;
private final JWTVerifier verifier;
public UserService(
PropertiesConfig propertiesConfig,
UserRepository userRepository) {
this.propertiesConfig = propertiesConfig;
this.userRepository = userRepository;
this.algorithm = Algorithm.HMAC256(this.propertiesConfig.getJwtSecret());
this.verifier = JWT.require(algorithm).build();
}
public User findByLogin(String login) {
return login == null ? null : userRepository.findByLogin(login);
}
public User findByLoginAndPassword(String login, String password) {
return login != null && password != null ?
userRepository.findByLoginAndPassword(login,
password,
this.propertiesConfig.getShaSalt()) : null;
}
public User findById(Long id) {
return id == null ? null : userRepository.findById(id).orElse(null);
}
public User findByJwt(String jwt) {
try {
DecodedJWT decodedJwt = verifier.verify(jwt);
return this.findById(decodedJwt.getClaim("userId").asLong());
} catch (JWTVerificationException exception){
return null;
}
}
public List<User> findAll() {
return userRepository.findAllByOrderByIdDesc();
}
public String createUserJwt(User user) {
try {
return JWT.create()
.withClaim("userId", user.getId())
.sign(algorithm);
} catch (JWTCreationException exception){
throw new RuntimeException("Can't create JWT.");
}
}
public User addUserByCredentials(UserCredentialsRegister userCredentials) {
User user = new User();
user.setName(userCredentials.getName());
user.setLogin(userCredentials.getLogin());
user.setAdmin(false);
userRepository.save(user);
userRepository.updatePasswordSha(user.getId(),
userCredentials.getLogin(),
userCredentials.getPassword(),
this.propertiesConfig.getShaSalt());
return user;
}
public void deleteUserById(long id) {
userRepository.deleteById(id);
}
public void saveUser(User user) {
userRepository.save(user);
}
}
@@ -0,0 +1,67 @@
package com.ermnvldmr.w.service;
import com.ermnvldmr.w.domain.Discussion;
import com.ermnvldmr.w.domain.Post;
import com.ermnvldmr.w.domain.User;
import com.ermnvldmr.w.domain.Vote;
import com.ermnvldmr.w.repository.VoteRepository;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class VoteService {
private final VoteRepository voteRepository;
public VoteService(VoteRepository voteRepository) {
this.voteRepository = voteRepository;
}
public Vote findByPostAndUser(Post post, User user) {
return voteRepository.findByUserAndPost(user, post).orElse(null);
}
public Vote findByDiscussionAndUser(Discussion discussion, User user) {
return voteRepository.findByUserAndDiscussion(user, discussion).orElse(null);
}
public Vote votePost(Post post, User user, boolean upvote) {
Optional<Vote> existingVote = voteRepository.findByUserAndPost(user, post);
if (existingVote.isPresent()) {
Vote vote = existingVote.get();
if (vote.isUpvote() == upvote) {
voteRepository.delete(vote); // Toggle off the vote if it’s the same type.
return null;
} else {
vote.setUpvote(upvote); // Change vote type if it’s different.
return voteRepository.save(vote);
}
} else {
Vote newVote = new Vote();
newVote.setUser(user);
newVote.setUpvote(upvote);
newVote.setPost(post);
return voteRepository.save(newVote);
}
}
public Vote voteDiscussion(Discussion discussion, User user, boolean upvote) {
Optional<Vote> existingVote = voteRepository.findByUserAndDiscussion(user, discussion);
if (existingVote.isPresent()) {
Vote vote = existingVote.get();
if (vote.isUpvote() == upvote) {
voteRepository.delete(vote); // Toggle off the vote if it’s the same type.
return null;
} else {
vote.setUpvote(upvote); // Change vote type if it’s different.
return voteRepository.save(vote);
}
} else {
Vote newVote = new Vote();
newVote.setUser(user);
newVote.setUpvote(upvote);
newVote.setDiscussion(discussion);
return voteRepository.save(newVote);
}
}
}
@@ -0,0 +1,14 @@
package com.ermnvldmr.w.utils;
import org.springframework.validation.BindingResult;
import java.util.HashMap;
import java.util.Map;
public class Utils {
public static Map<String, Object> getValidationErrors(BindingResult bindingResult) {
Map<String, Object> errors = new HashMap<>();
bindingResult.getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
return Map.of("validationErrors", errors); // Structured JSON format
}
}
+22
View File
@@ -0,0 +1,22 @@
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=${MYSQL_URL}
spring.datasource.username=${MYSQL_USER}
spring.datasource.password=${MYSQL_PASSWORD}
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.format_sql=true
server.port=8090
server.error.include-stacktrace=never
server.error.whitelabel.enabled=false
server.error.include-exception=false
server.servlet.contextPath=${PATH_PREFIX}
server.forward-headers-strategy=framework
config.shaSalt=${SHA_SALT}
config.jwtSecret=${JWT_SECRET}
config.mediaDir=${MEDIA_DIR}