diff --git a/Docker-compose/docker-compose.yml b/Docker-compose/docker-compose.yml new file mode 100644 index 0000000..aa4d118 --- /dev/null +++ b/Docker-compose/docker-compose.yml @@ -0,0 +1,21 @@ +services: + app: + image: 'twitter-box:latest' + container_name: app + depends_on: + - db + ports: + - '4000:4000' + environment: + - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mypostgresuser + - SPRING_DATASOURCE_USERNAME=mypostgresuser + - SPRING_DATASOURCE_PASSWORD=mypostgrespassword + - SPRING_JPA_HIBERNATE_DDL_AUTO=update + + db: + image: 'postgres:latest' + container_name: db + environment: + - POSTGRES_USER=mypostgresuser + - POSTGRES_DATABASE=mypostgresuser + - POSTGRES_PASSWORD=mypostgrespassword \ No newline at end of file diff --git a/build.gradle b/build.gradle index fb070c4..1d3d0a8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,17 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.1.4' - id 'io.spring.dependency-management' version '1.1.3' + id 'org.springframework.boot' version '3.4.2' + id 'io.spring.dependency-management' version '1.1.7' } group = 'com.booleanuk' -version = '0.0.1' -sourceCompatibility = '17' +version = '0.0.2' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} configurations { compileOnly { @@ -20,14 +25,27 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' + compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' - implementation 'org.postgresql:postgresql:42.6.0' + runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' + implementation 'org.postgresql:postgresql:42.6.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + + implementation 'jakarta.validation:jakarta.validation-api:3.1.0' +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' } tasks.named('test') { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/com/booleanuk/api/Main.java b/src/main/java/com/booleanuk/api/Main.java new file mode 100644 index 0000000..5cc54e1 --- /dev/null +++ b/src/main/java/com/booleanuk/api/Main.java @@ -0,0 +1,32 @@ +package com.booleanuk.api; + +import com.booleanuk.api.model.ERole; +import com.booleanuk.api.model.Role; +import com.booleanuk.api.repository.RoleRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main implements CommandLineRunner { + @Autowired + private RoleRepository roleRepository; + + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } + + @Override + public void run(String... args) { + if (!this.roleRepository.existsByName(ERole.ROLE_USER)) { + this.roleRepository.save(new Role(ERole.ROLE_USER)); + } + if (!this.roleRepository.existsByName(ERole.ROLE_ADMIN)) { + this.roleRepository.save(new Role(ERole.ROLE_ADMIN)); + } + if (!this.roleRepository.existsByName(ERole.ROLE_MODERATOR)) { + this.roleRepository.save(new Role(ERole.ROLE_MODERATOR)); + } + } +} diff --git a/src/main/java/com/booleanuk/api/controller/AuthController.java b/src/main/java/com/booleanuk/api/controller/AuthController.java new file mode 100644 index 0000000..ddd5955 --- /dev/null +++ b/src/main/java/com/booleanuk/api/controller/AuthController.java @@ -0,0 +1,101 @@ +package com.booleanuk.api.controller; + +import com.booleanuk.api.model.ERole; +import com.booleanuk.api.model.Role; +import com.booleanuk.api.model.User; +import com.booleanuk.api.payload.request.LoginRequest; +import com.booleanuk.api.payload.request.SignupRequest; +import com.booleanuk.api.payload.response.JwtResponse; +import com.booleanuk.api.payload.response.MessageResponse; +import com.booleanuk.api.repository.RoleRepository; +import com.booleanuk.api.repository.UserRepository; +import com.booleanuk.api.security.jwt.JwtUtils; +import com.booleanuk.api.security.services.UserDetailsImpl; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@CrossOrigin(origins = "*", maxAge = 3600) +@RestController +@RequestMapping("auth") +public class AuthController { + @Autowired + AuthenticationManager authenticationManager; + + @Autowired + UserRepository userRepository; + + @Autowired + RoleRepository roleRepository; + + @Autowired + PasswordEncoder encoder; + + @Autowired + JwtUtils jwtUtils; + + @PostMapping("/signin") + public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { + // If using a salt for password use it here + Authentication authentication = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = jwtUtils.generateJwtToken(authentication); + + UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); + List roles = userDetails.getAuthorities().stream().map((item) -> item.getAuthority()) + .collect(Collectors.toList()); + return ResponseEntity + .ok(new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(), userDetails.getEmail(), roles)); + } + + @PostMapping("/signup") + public ResponseEntity registerUser(@Valid @RequestBody SignupRequest signupRequest) { + if (userRepository.existsByUsername(signupRequest.getUsername())) { + return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken")); + } + if (userRepository.existsByEmail(signupRequest.getEmail())) { + return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!")); + } + // Create a new user add salt here if using one + User user = new User(signupRequest.getUsername(), signupRequest.getEmail(), encoder.encode(signupRequest.getPassword())); + Set strRoles = signupRequest.getRole(); + Set roles = new HashSet<>(); + + if (strRoles == null) { + Role userRole = roleRepository.findByName(ERole.ROLE_USER).orElseThrow(() -> new RuntimeException("Error: Role is not found")); + roles.add(userRole); + } else { + strRoles.forEach((role) -> { + switch (role) { + case "admin": + Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN).orElseThrow(() -> new RuntimeException("Error: Role is not found")); + roles.add(adminRole); + break; + case "mod": + Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR).orElseThrow(() -> new RuntimeException("Error: Role is not found")); + roles.add(modRole); + break; + default: + Role userRole = roleRepository.findByName(ERole.ROLE_USER).orElseThrow(() -> new RuntimeException("Error: Role is not found")); + roles.add(userRole); + break; + } + }); + } + user.setRoles(roles); + userRepository.save(user); + return ResponseEntity.ok((new MessageResponse("User registered successfully"))); + } +} \ No newline at end of file diff --git a/src/main/java/com/booleanuk/api/controller/FollowerController.java b/src/main/java/com/booleanuk/api/controller/FollowerController.java new file mode 100644 index 0000000..7aa95e4 --- /dev/null +++ b/src/main/java/com/booleanuk/api/controller/FollowerController.java @@ -0,0 +1,128 @@ +package com.booleanuk.api.controller; + +import com.booleanuk.api.model.*; +import com.booleanuk.api.payload.response.*; +import com.booleanuk.api.repository.FollowerRepository; +import com.booleanuk.api.repository.UserRepository; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("followers") +public class FollowerController { + @Autowired + private FollowerRepository followerRepository; + + @Autowired + private UserRepository userRepository; + + @GetMapping + public ResponseEntity getAllFollows() { + FollowerListResponse followerListResponse = new FollowerListResponse(); + followerListResponse.set(this.followerRepository.findAll()); + return ResponseEntity.ok(followerListResponse); + } + + @PostMapping("/{u1}/follow/{u2}") + public ResponseEntity> follow(@RequestBody Follower follower, @PathVariable("u1") int u1, @PathVariable("u2") int u2) { + User user = this.userRepository.findById(u1).orElse(null); + User isfollowed = this.userRepository.findById(u2).orElse(null); + + try { + if(user == null || isfollowed == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + for(Follower aFollower : this.followerRepository.findAll()){ + if(aFollower.getIsFollowing().equals(user) && aFollower.getIsFollowed().equals(isfollowed)){ + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + } + + follower.setIsFollowing(user); + follower.setIsFollowed(isfollowed); + + FollowerResponse followerResponse = new FollowerResponse(); + followerResponse.set(this.followerRepository.save(follower)); + + return new ResponseEntity<>(followerResponse, HttpStatus.CREATED); + + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + } + + + + + + @DeleteMapping("/{u1}/unfollow/{u2}") + public ResponseEntity> unfollow(@PathVariable("u1") int u1, @PathVariable("u2") int u2) { + User user = this.userRepository.findById(u1).orElse(null); + User isfollowed = this.userRepository.findById(u2).orElse(null); + + try { + if(user == null || isfollowed == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + for(Follower aFollower : this.followerRepository.findAll()){ + if(aFollower.getIsFollowing().equals(user) && aFollower.getIsFollowed().equals(isfollowed)){ + this.followerRepository.delete(aFollower); + FollowerResponse followerResponse = new FollowerResponse(); + followerResponse.set(aFollower); + return ResponseEntity.ok(followerResponse); + } + } + + ErrorResponse error = new ErrorResponse(); + error.set("Bad request, user with ID" + u1 + "do not follow user with ID" + u2); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + } + + @GetMapping("{id}") + public ResponseEntity getAllFollowsForSpecificUser(@PathVariable int id) { + User user = this.userRepository.findById(id).orElse(null); + if(user == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + FollowerListResponse followerListResponse = new FollowerListResponse(); + followerListResponse.set(this.followerRepository.findAllIsFollowingByIsFollowed(user)); + return ResponseEntity.ok(followerListResponse); + } + + @GetMapping("follows/{id}") + public ResponseEntity getAllFollowingForSpecificUser(@PathVariable int id) { + User user = this.userRepository.findById(id).orElse(null); + if(user == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + FollowerListResponse followerListResponse = new FollowerListResponse(); + followerListResponse.set(this.followerRepository.findAllIsFollowedByIsFollowing(user)); + return ResponseEntity.ok(followerListResponse); + } +} diff --git a/src/main/java/com/booleanuk/api/controller/PostController.java b/src/main/java/com/booleanuk/api/controller/PostController.java new file mode 100644 index 0000000..cf2af27 --- /dev/null +++ b/src/main/java/com/booleanuk/api/controller/PostController.java @@ -0,0 +1,122 @@ +package com.booleanuk.api.controller; + +import com.booleanuk.api.model.Post; +import com.booleanuk.api.model.Upvote; +import com.booleanuk.api.model.User; +import com.booleanuk.api.payload.response.ErrorResponse; +import com.booleanuk.api.payload.response.PostListResponse; +import com.booleanuk.api.payload.response.PostResponse; +import com.booleanuk.api.payload.response.Response; +import com.booleanuk.api.repository.PostRepository; +import com.booleanuk.api.repository.UpvoteRepository; +import com.booleanuk.api.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("posts") +public class PostController { + @Autowired + private PostRepository postRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UpvoteRepository upvoteRepository; + + @GetMapping + public ResponseEntity getAllPosts() { + PostListResponse postListResponse = new PostListResponse(); + postListResponse.set(this.postRepository.findAll()); + return ResponseEntity.ok(postListResponse); + } + + @PostMapping("/{uId}/create") + public ResponseEntity> createPost(@RequestBody Post post, @PathVariable("uId") int uId) { + User user = this.userRepository.findById(uId).orElse(null); + + try { + if(user == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + post.setUser(user); + post.setContent(post.getContent()); + + PostResponse postResponse = new PostResponse(); + postResponse.set(this.postRepository.save(post)); + + user.getPosts().add(post); + user.setPosts(user.getPosts()); + this.userRepository.save(user); + return new ResponseEntity<>(postResponse, HttpStatus.CREATED); + + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + } + + @GetMapping("/{id}") + public ResponseEntity> getPostById(@PathVariable int id) { + Post post = this.postRepository.findById(id).orElse(null); + if (post == null) { + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + PostResponse postResponse = new PostResponse(); + postResponse.set(post); + return ResponseEntity.ok(postResponse); + } + + @PutMapping("/{id}") + public ResponseEntity> updatePost(@PathVariable int id, @RequestBody Post post) { + Post postToUpdate = this.postRepository.findById(id).orElse(null); + if (postToUpdate == null) { + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + try { + postToUpdate.setContent(post.getContent()); + this.postRepository.save(postToUpdate); + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + PostResponse postResponse = new PostResponse(); + postResponse.set(postToUpdate); + return new ResponseEntity<>(postResponse, HttpStatus.CREATED); + } + + @DeleteMapping("/{id}") + public ResponseEntity> deletePost(@PathVariable int id) { + Post postToDelete = this.postRepository.findById(id).orElse(null); + if (postToDelete == null) { + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + + for(Upvote upvote : this.upvoteRepository.findAll()){ + if(upvote.getPost().equals(postToDelete)){ + this.upvoteRepository.delete(upvote); + } + } + + this.postRepository.delete(postToDelete); + PostResponse postResponse = new PostResponse(); + postResponse.set(postToDelete); + return ResponseEntity.ok(postResponse); + } +} diff --git a/src/main/java/com/booleanuk/api/controller/UpvoteController.java b/src/main/java/com/booleanuk/api/controller/UpvoteController.java new file mode 100644 index 0000000..1293315 --- /dev/null +++ b/src/main/java/com/booleanuk/api/controller/UpvoteController.java @@ -0,0 +1,113 @@ +package com.booleanuk.api.controller; + +import com.booleanuk.api.model.Follower; +import com.booleanuk.api.model.Post; +import com.booleanuk.api.model.Upvote; +import com.booleanuk.api.model.User; +import com.booleanuk.api.payload.response.*; +import com.booleanuk.api.repository.PostRepository; +import com.booleanuk.api.repository.UpvoteRepository; +import com.booleanuk.api.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("upvotes/{pId}") +public class UpvoteController { + @Autowired + private UpvoteRepository upvoteRepository; + + @Autowired + private PostRepository postRepository; + + @Autowired + private UserRepository userRepository; + + @PostMapping("/{uId}") + public ResponseEntity> createUpvote(@RequestBody Upvote upvote, @PathVariable("pId") int postId, @PathVariable("uId") int userId) { + Post post = this.postRepository.findById(postId).orElse(null); + User user = this.userRepository.findById(userId).orElse(null); + + + try { + if(user == null || post == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + for(Upvote aUpvote : this.upvoteRepository.findAll()){ + if(aUpvote.getPost().equals(post) && aUpvote.getUser().equals(user)){ + ErrorResponse error = new ErrorResponse(); + error.set("Bad request, post is already upvoted"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + } + upvote.setPost(post); + upvote.setUser(user); + + UpvoteResponse upvoteResponse = new UpvoteResponse(); + upvoteResponse.set(this.upvoteRepository.save(upvote)); + return new ResponseEntity<>(upvoteResponse, HttpStatus.CREATED); + + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + } + + + + + + @DeleteMapping("/{uId}") + public ResponseEntity> deleteUpvote(@PathVariable("pId") int postId, @PathVariable("uId") int userId) { + Post post = this.postRepository.findById(postId).orElse(null); + User user = this.userRepository.findById(userId).orElse(null); + + try { + if(user == null || post == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + for(Upvote aUpvote : this.upvoteRepository.findAll()){ + if(aUpvote.getPost().equals(post) && aUpvote.getUser().equals(user)){ + this.upvoteRepository.delete(aUpvote); + UpvoteResponse upvoteResponse = new UpvoteResponse(); + upvoteResponse.set(aUpvote); + return ResponseEntity.ok(upvoteResponse); + } + } + + ErrorResponse error = new ErrorResponse(); + error.set("Bad request, user with ID" + userId + "have not upvoted post with ID" + postId); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + + } catch (Exception e) { + ErrorResponse error = new ErrorResponse(); + error.set("Bad request"); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + } + + @GetMapping + public ResponseEntity getAllUpvotesForSpecificPost(@PathVariable("pId") int id) { + Post post = this.postRepository.findById(id).orElse(null); + if(post == null){ + ErrorResponse error = new ErrorResponse(); + error.set("not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + UpvoteListResponse upvoteListResponse = new UpvoteListResponse(); + upvoteListResponse.set(this.upvoteRepository.findAllUserByPost(post)); + return ResponseEntity.ok(upvoteListResponse); + } + +} diff --git a/src/main/java/com/booleanuk/api/model/ERole.java b/src/main/java/com/booleanuk/api/model/ERole.java new file mode 100644 index 0000000..29ebf7e --- /dev/null +++ b/src/main/java/com/booleanuk/api/model/ERole.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.model; + +public enum ERole { + ROLE_USER, + ROLE_MODERATOR, + ROLE_ADMIN +} diff --git a/src/main/java/com/booleanuk/api/model/Follower.java b/src/main/java/com/booleanuk/api/model/Follower.java new file mode 100644 index 0000000..3b65b39 --- /dev/null +++ b/src/main/java/com/booleanuk/api/model/Follower.java @@ -0,0 +1,42 @@ +package com.booleanuk.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "followers") +@JsonIncludeProperties({"id", "isFollowing", "isFollowed"}) +public class Follower { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne + @JoinColumn(name = "follower_id") + @JsonIgnoreProperties({"password", "roles", "posts"}) + private User isFollowing; + + @ManyToOne + @JoinColumn(name = "followed_id") + @JsonIgnoreProperties({"password", "roles", "posts"}) + private User isFollowed; + + public Follower(User isFollowing, User isFollowed){ + this.isFollowing = isFollowing; + this.isFollowed = isFollowed; + } + + public Follower(int id){ + this.id = id; + } + +} diff --git a/src/main/java/com/booleanuk/api/model/Post.java b/src/main/java/com/booleanuk/api/model/Post.java new file mode 100644 index 0000000..3128921 --- /dev/null +++ b/src/main/java/com/booleanuk/api/model/Post.java @@ -0,0 +1,47 @@ +package com.booleanuk.api.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "posts") +@JsonIgnoreProperties({"reposts", "upvotes"}) +public class Post { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne + @JoinColumn(name = "user_id") + @JsonIgnoreProperties({"posts", "roles", "id", "email", "password"}) + private User user; + + @Column + private String content; + + @OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List upvotes; + + public Post(User user, String content){ + this.user = user; + this.content = content; + } + + public Post(String content){ + this.content = content; + } + + public Post(int id){ + this.id = id; + } +} diff --git a/src/main/java/com/booleanuk/api/model/Role.java b/src/main/java/com/booleanuk/api/model/Role.java new file mode 100644 index 0000000..d21e3be --- /dev/null +++ b/src/main/java/com/booleanuk/api/model/Role.java @@ -0,0 +1,23 @@ +package com.booleanuk.api.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@Data +@Entity +@Table(name = "roles") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @Enumerated(EnumType.STRING) + @Column(length = 20) + private ERole name; + + public Role(ERole name) { + this.name = name; + } +} diff --git a/src/main/java/com/booleanuk/api/model/Upvote.java b/src/main/java/com/booleanuk/api/model/Upvote.java new file mode 100644 index 0000000..0bb82b8 --- /dev/null +++ b/src/main/java/com/booleanuk/api/model/Upvote.java @@ -0,0 +1,39 @@ +package com.booleanuk.api.model; + +import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "upvotes") +public class Upvote { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne + @JoinColumn(name = "user_id") + @JsonIncludeProperties({"username"}) + private User user; + + @ManyToOne + @JoinColumn(name = "post_id") + + private Post post; + + public Upvote(User user, Post post){ + this.user = user; + this.post = post; + } + + public Upvote(int id){ + this.id = id; + } +} diff --git a/src/main/java/com/booleanuk/api/model/User.java b/src/main/java/com/booleanuk/api/model/User.java new file mode 100644 index 0000000..1ffe51e --- /dev/null +++ b/src/main/java/com/booleanuk/api/model/User.java @@ -0,0 +1,58 @@ +package com.booleanuk.api.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@Entity +@Table(name = "users", + uniqueConstraints = { + @UniqueConstraint(columnNames = "username"), + @UniqueConstraint(columnNames = "email") + }) +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + @NotBlank + @Size(max = 20) + private String username; + + @NotBlank + @Size(max = 50) + @Email + private String email; + + @NotBlank + @Size(max = 120) + private String password; + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "user_roles",joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id")) + private Set roles = new HashSet<>(); + + @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List posts; + + public User(String username, String email, String password) { + this.username = username; + this.email = email; + this.password = password; + } + + public User(int id) { + this.id =id; + } +} diff --git a/src/main/java/com/booleanuk/api/payload/request/LoginRequest.java b/src/main/java/com/booleanuk/api/payload/request/LoginRequest.java new file mode 100644 index 0000000..bb4c3a5 --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/request/LoginRequest.java @@ -0,0 +1,15 @@ +package com.booleanuk.api.payload.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequest { + @NotBlank + private String username; + + @NotBlank + private String password; +} diff --git a/src/main/java/com/booleanuk/api/payload/request/SignupRequest.java b/src/main/java/com/booleanuk/api/payload/request/SignupRequest.java new file mode 100644 index 0000000..83720cb --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/request/SignupRequest.java @@ -0,0 +1,29 @@ +package com.booleanuk.api.payload.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +import java.util.Set; + +@Getter +@Setter +public class SignupRequest { + @NotBlank + @Size(min = 3, max = 20) + private String username; + + @NotBlank + @Size(max = 50) + @Email + private String email; + + private Set role; + + @NotBlank + @Size(min = 6, max = 40) + private String password; + +} diff --git a/src/main/java/com/booleanuk/api/payload/response/ErrorResponse.java b/src/main/java/com/booleanuk/api/payload/response/ErrorResponse.java new file mode 100644 index 0000000..d58c282 --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.booleanuk.api.payload.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +@Getter +@NoArgsConstructor +public class ErrorResponse extends Response> { + public void set(String message) { + this.status = "error"; + + Map reply = new HashMap<>(); + reply.put("message", message); + this.data = reply; + } +} diff --git a/src/main/java/com/booleanuk/api/payload/response/FollowerListResponse.java b/src/main/java/com/booleanuk/api/payload/response/FollowerListResponse.java new file mode 100644 index 0000000..4855c76 --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/FollowerListResponse.java @@ -0,0 +1,9 @@ +package com.booleanuk.api.payload.response; + +import com.booleanuk.api.model.Follower; +import com.booleanuk.api.model.Post; + +import java.util.List; + +public class FollowerListResponse extends Response>{ +} diff --git a/src/main/java/com/booleanuk/api/payload/response/FollowerResponse.java b/src/main/java/com/booleanuk/api/payload/response/FollowerResponse.java new file mode 100644 index 0000000..942a7ac --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/FollowerResponse.java @@ -0,0 +1,9 @@ +package com.booleanuk.api.payload.response; + +import com.booleanuk.api.model.Follower; +import com.booleanuk.api.model.Post; + +import java.util.List; + +public class FollowerResponse extends Response{ +} diff --git a/src/main/java/com/booleanuk/api/payload/response/JwtResponse.java b/src/main/java/com/booleanuk/api/payload/response/JwtResponse.java new file mode 100644 index 0000000..b596fb1 --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/JwtResponse.java @@ -0,0 +1,25 @@ +package com.booleanuk.api.payload.response; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class JwtResponse { + private String token; + private String type = "Bearer"; + private int id; + private String username; + private String email; + private List roles; + + public JwtResponse(String token, int id, String username, String email, List roles) { + this.token = token; + this.id = id; + this.username = username; + this.email = email; + this.roles = roles; + } +} diff --git a/src/main/java/com/booleanuk/api/payload/response/MessageResponse.java b/src/main/java/com/booleanuk/api/payload/response/MessageResponse.java new file mode 100644 index 0000000..d001efc --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/MessageResponse.java @@ -0,0 +1,14 @@ +package com.booleanuk.api.payload.response; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class MessageResponse { + private String message; + + public MessageResponse(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/booleanuk/api/payload/response/PostListResponse.java b/src/main/java/com/booleanuk/api/payload/response/PostListResponse.java new file mode 100644 index 0000000..82047da --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/PostListResponse.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.payload.response; + +import com.booleanuk.api.model.Post; + +import java.util.List; + +public class PostListResponse extends Response> { +} diff --git a/src/main/java/com/booleanuk/api/payload/response/PostResponse.java b/src/main/java/com/booleanuk/api/payload/response/PostResponse.java new file mode 100644 index 0000000..9b44ca9 --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/PostResponse.java @@ -0,0 +1,8 @@ +package com.booleanuk.api.payload.response; + +import com.booleanuk.api.model.Post; + +import java.util.List; + +public class PostResponse extends Response { +} diff --git a/src/main/java/com/booleanuk/api/payload/response/Response.java b/src/main/java/com/booleanuk/api/payload/response/Response.java new file mode 100644 index 0000000..7acb67f --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/Response.java @@ -0,0 +1,14 @@ +package com.booleanuk.api.payload.response; + +import lombok.Getter; + +@Getter +public class Response { + protected String status; + protected T data; + + public void set(T data) { + this.status = "success"; + this.data = data; + } +} diff --git a/src/main/java/com/booleanuk/api/payload/response/UpvoteListResponse.java b/src/main/java/com/booleanuk/api/payload/response/UpvoteListResponse.java new file mode 100644 index 0000000..743bc56 --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/UpvoteListResponse.java @@ -0,0 +1,9 @@ +package com.booleanuk.api.payload.response; + +import com.booleanuk.api.model.Follower; +import com.booleanuk.api.model.Upvote; + +import java.util.List; + +public class UpvoteListResponse extends Response>{ +} diff --git a/src/main/java/com/booleanuk/api/payload/response/UpvoteResponse.java b/src/main/java/com/booleanuk/api/payload/response/UpvoteResponse.java new file mode 100644 index 0000000..49642a1 --- /dev/null +++ b/src/main/java/com/booleanuk/api/payload/response/UpvoteResponse.java @@ -0,0 +1,9 @@ +package com.booleanuk.api.payload.response; + +import com.booleanuk.api.model.Follower; +import com.booleanuk.api.model.Upvote; + +import java.util.List; + +public class UpvoteResponse extends Response{ +} diff --git a/src/main/java/com/booleanuk/api/repository/FollowerRepository.java b/src/main/java/com/booleanuk/api/repository/FollowerRepository.java new file mode 100644 index 0000000..2801b32 --- /dev/null +++ b/src/main/java/com/booleanuk/api/repository/FollowerRepository.java @@ -0,0 +1,13 @@ +package com.booleanuk.api.repository; + +import com.booleanuk.api.model.Follower; +import com.booleanuk.api.model.User; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FollowerRepository extends JpaRepository { + List findAllIsFollowingByIsFollowed(User user); + List findAllIsFollowedByIsFollowing(User user); +} diff --git a/src/main/java/com/booleanuk/api/repository/PostRepository.java b/src/main/java/com/booleanuk/api/repository/PostRepository.java new file mode 100644 index 0000000..a80e468 --- /dev/null +++ b/src/main/java/com/booleanuk/api/repository/PostRepository.java @@ -0,0 +1,7 @@ +package com.booleanuk.api.repository; + +import com.booleanuk.api.model.Post; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostRepository extends JpaRepository { +} diff --git a/src/main/java/com/booleanuk/api/repository/RoleRepository.java b/src/main/java/com/booleanuk/api/repository/RoleRepository.java new file mode 100644 index 0000000..cc3f2e3 --- /dev/null +++ b/src/main/java/com/booleanuk/api/repository/RoleRepository.java @@ -0,0 +1,13 @@ +package com.booleanuk.api.repository; + +import com.booleanuk.api.model.ERole; +import com.booleanuk.api.model.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RoleRepository extends JpaRepository { + Optional findByName(ERole name); + + boolean existsByName(ERole name); +} diff --git a/src/main/java/com/booleanuk/api/repository/UpvoteRepository.java b/src/main/java/com/booleanuk/api/repository/UpvoteRepository.java new file mode 100644 index 0000000..f2c4ed1 --- /dev/null +++ b/src/main/java/com/booleanuk/api/repository/UpvoteRepository.java @@ -0,0 +1,11 @@ +package com.booleanuk.api.repository; + +import com.booleanuk.api.model.Post; +import com.booleanuk.api.model.Upvote; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface UpvoteRepository extends JpaRepository { + List findAllUserByPost(Post post); +} diff --git a/src/main/java/com/booleanuk/api/repository/UserRepository.java b/src/main/java/com/booleanuk/api/repository/UserRepository.java new file mode 100644 index 0000000..9bd89f4 --- /dev/null +++ b/src/main/java/com/booleanuk/api/repository/UserRepository.java @@ -0,0 +1,15 @@ +package com.booleanuk.api.repository; + +import com.booleanuk.api.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + + Boolean existsByUsername(String username); + Boolean existsByEmail(String email); +} diff --git a/src/main/java/com/booleanuk/api/security/WebSecurityConfig.java b/src/main/java/com/booleanuk/api/security/WebSecurityConfig.java new file mode 100644 index 0000000..9d4dbb2 --- /dev/null +++ b/src/main/java/com/booleanuk/api/security/WebSecurityConfig.java @@ -0,0 +1,69 @@ +package com.booleanuk.api.security; + +import com.booleanuk.api.security.jwt.AuthEntryPointJwt; +import com.booleanuk.api.security.jwt.AuthTokenFilter; +import com.booleanuk.api.security.services.UserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig { + @Autowired + private UserDetailsServiceImpl userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + + return authProvider; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf((csrf) -> csrf.disable()) + .exceptionHandling((exception) -> exception.authenticationEntryPoint(unauthorizedHandler)) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/auth/**").permitAll() + .requestMatchers("/posts", "/followers", "/upvotes").authenticated() + .requestMatchers("/posts/**", "/followers/**", "/upvotes/**").hasRole("USER") + ); + http.authenticationProvider(authenticationProvider()); + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/booleanuk/api/security/jwt/AuthEntryPointJwt.java b/src/main/java/com/booleanuk/api/security/jwt/AuthEntryPointJwt.java new file mode 100644 index 0000000..e7b3fb4 --- /dev/null +++ b/src/main/java/com/booleanuk/api/security/jwt/AuthEntryPointJwt.java @@ -0,0 +1,24 @@ +package com.booleanuk.api.security.jwt; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class AuthEntryPointJwt implements AuthenticationEntryPoint { + private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + logger.error("Unauthorized error: {}", authException.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); + } + +} diff --git a/src/main/java/com/booleanuk/api/security/jwt/AuthTokenFilter.java b/src/main/java/com/booleanuk/api/security/jwt/AuthTokenFilter.java new file mode 100644 index 0000000..674173d --- /dev/null +++ b/src/main/java/com/booleanuk/api/security/jwt/AuthTokenFilter.java @@ -0,0 +1,54 @@ +package com.booleanuk.api.security.jwt; + +import com.booleanuk.api.security.services.UserDetailsServiceImpl; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class AuthTokenFilter extends OncePerRequestFilter { + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private UserDetailsServiceImpl userDetailsService; + + private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtils.validateJwtToken(jwt)) { + String username = jwtUtils.getUserNameFromJwtToken(jwt); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Cannot set user authentication: {}", e.getMessage()); + } + filterChain.doFilter(request, response); + } + + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/booleanuk/api/security/jwt/JwtUtils.java b/src/main/java/com/booleanuk/api/security/jwt/JwtUtils.java new file mode 100644 index 0000000..eb3ce39 --- /dev/null +++ b/src/main/java/com/booleanuk/api/security/jwt/JwtUtils.java @@ -0,0 +1,63 @@ +package com.booleanuk.api.security.jwt; + +import com.booleanuk.api.security.services.UserDetailsImpl; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtUtils { + private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class); + + @Value("${booleanuk.app.jwtSecret}") + private String jwtSecret; + + @Value("${booleanuk.app.jwtExpirationMs}") + private int jwtExpirationMs; + + public String generateJwtToken(Authentication authentication) { + UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); + + return Jwts.builder() + .subject((userPrincipal.getUsername())) + .issuedAt(new Date()) + .expiration(new Date((new Date()).getTime() + this.jwtExpirationMs)) + .signWith(this.key()) + .compact(); + } + + private SecretKey key() { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(this.jwtSecret)); + } + + public String getUserNameFromJwtToken(String token) { + return Jwts.parser().verifyWith(this.key()).build().parseSignedClaims(token).getPayload().getSubject(); + } + + public boolean validateJwtToken(String authToken) { + try { + Jwts.parser().verifyWith(this.key()).build().parse(authToken); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token has expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); + } + return false; + } +} diff --git a/src/main/java/com/booleanuk/api/security/services/UserDetailsImpl.java b/src/main/java/com/booleanuk/api/security/services/UserDetailsImpl.java new file mode 100644 index 0000000..5a327ea --- /dev/null +++ b/src/main/java/com/booleanuk/api/security/services/UserDetailsImpl.java @@ -0,0 +1,59 @@ +package com.booleanuk.api.security.services; + +import com.booleanuk.api.model.User; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +@Getter +public class UserDetailsImpl implements UserDetails { + private static final long serialVersionUID = 1L; + + private int id; + private String username; + private String email; + + @JsonIgnore + private String password; + + private Collection authorities; + + public UserDetailsImpl(int id, String username, String email, String password, Collection authorities) { + this.id = id; + this.username = username; + this.email = email; + this.password = password; + this.authorities = authorities; + } + + public static UserDetailsImpl build(User user) { + List authorities = user.getRoles().stream() + .map(role -> new SimpleGrantedAuthority(role.getName().name())) + .collect(Collectors.toList()); + return new UserDetailsImpl( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getPassword(), + authorities); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserDetailsImpl user = (UserDetailsImpl) o; + return Objects.equals(id, user.id); + } +} diff --git a/src/main/java/com/booleanuk/api/security/services/UserDetailsServiceImpl.java b/src/main/java/com/booleanuk/api/security/services/UserDetailsServiceImpl.java new file mode 100644 index 0000000..65ad110 --- /dev/null +++ b/src/main/java/com/booleanuk/api/security/services/UserDetailsServiceImpl.java @@ -0,0 +1,26 @@ +package com.booleanuk.api.security.services; + +import com.booleanuk.api.model.User; +import com.booleanuk.api.repository.UserRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + @Autowired + UserRepository userRepository; + + @Override + @Transactional + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username).orElseThrow( + () -> new UsernameNotFoundException("User not found with username: " + username) + ); + return UserDetailsImpl.build(user); + + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 02c2591..8f99d41 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,3 +5,8 @@ server: include-binding-errors: always include-stacktrace: never include-exception: false + +booleanuk: + app: + jwtSecret: ===============================BooleanUK=Spring============================================== + jwtExpirationMs: 86400000 \ No newline at end of file