diff options
| author | realtradam <[email protected]> | 2024-07-23 20:47:31 -0400 |
|---|---|---|
| committer | realtradam <[email protected]> | 2024-07-23 20:47:31 -0400 |
| commit | 1e18e0ad7a47536be92384bbf815e0923a06698d (patch) | |
| tree | b07405ecdef4f05a96b6c4348930cbee976554cb /backend/src/main/java/com | |
| parent | 56c59e3b98fe554c4e1484e208e4be5c30f09a04 (diff) | |
| download | spring-blog-1e18e0ad7a47536be92384bbf815e0923a06698d.tar.gz spring-blog-1e18e0ad7a47536be92384bbf815e0923a06698d.zip | |
split front and back end, add react to project
Diffstat (limited to 'backend/src/main/java/com')
20 files changed, 998 insertions, 0 deletions
diff --git a/backend/src/main/java/com/blog/web/WebApplication.java b/backend/src/main/java/com/blog/web/WebApplication.java new file mode 100644 index 0000000..f5dd2ef --- /dev/null +++ b/backend/src/main/java/com/blog/web/WebApplication.java @@ -0,0 +1,11 @@ +package com.blog.web; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WebApplication { + public static void main(String[] args) { + SpringApplication.run(WebApplication.class, args); + } +} diff --git a/backend/src/main/java/com/blog/web/controllers/ArticleController.java b/backend/src/main/java/com/blog/web/controllers/ArticleController.java new file mode 100644 index 0000000..6cd5d50 --- /dev/null +++ b/backend/src/main/java/com/blog/web/controllers/ArticleController.java @@ -0,0 +1,125 @@ +package com.blog.web.controllers; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.ArticlePublicDto; +import com.blog.web.models.Article; +import com.blog.web.models.UserEntity; +import com.blog.web.services.ArticleService; +import com.blog.web.services.UserService; +import jakarta.validation.Valid; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; + +@RestController +@Controller +public class ArticleController { + private ArticleService articleService; + private UserService userService; + + public ArticleController(ArticleService articleService, UserService userService) { + this.articleService = articleService; + this.userService = userService; + } + + @GetMapping("/get") + public Article getMethod() { + return new Article( + 5, + "blah", + "blah", + "blah", + new UserEntity(), + LocalDateTime.now(), + LocalDateTime.now() + ); + } + + @GetMapping("/articles") + public HashSet<ArticlePublicDto> listArticles(Model model) { + HashSet<ArticlePublicDto> articles = new HashSet<ArticlePublicDto>(articleService.findAllArticles()); + //UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); + //model.addAttribute("user", user); + //model.addAttribute("articles", articles); + //return "index"; + return articles; + } + + @GetMapping("/articles/{articleId}") + public String showArticle(@PathVariable("articleId") long articleId, Model model) { + ArticleDto articleDto = articleService.findArticleById(articleId); + model.addAttribute("article", articleDto); + UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); + model.addAttribute("user", user); + return "articles/show"; + } + + @GetMapping("/articles/new") + public String createArticleForm(Model model) { + model.addAttribute("user", userService.getLoggedInUser().orElse(new UserEntity())); + model.addAttribute("article", new Article()); + return "articles/new"; + } + + @PostMapping("/articles/new") + public String saveArticle(@Valid @ModelAttribute("article") ArticleDto articleDto, BindingResult result, Model model) { + // if non-authenticated in user tries to create an article + // redirect them to login page + UserEntity user = userService.getLoggedInUser().orElse(null); + if (user == null) { + return "redirect:/userlogin"; + } else if (result.hasErrors()) { + model.addAttribute("article", articleDto); + return "articles/new"; + } else { + articleService.saveArticle(articleDto); + return "redirect:/articles"; + } + } + + @GetMapping("/articles/delete/{articleId}") + public String deleteArticle(@PathVariable("articleId") Long articleId) { + articleService.delete(articleId); + return "redirect:/articles"; + } + + @GetMapping("/articles/edit/{articleId}") + public String editArticleForm(@PathVariable("articleId") long articleId, Model model) { + UserEntity user = userService.getLoggedInUser().orElse(null); + if (user != null) { + model.addAttribute("user", user); + ArticleDto articleDto = articleService.findArticleById(articleId); + model.addAttribute("article", articleDto); + } + return "articles/edit"; + } + + @PostMapping("/articles/edit/{articleId}") + public String updateArticle(@PathVariable("articleId") Long articleId, @Valid @ModelAttribute("article") ArticleDto article, BindingResult result) { + if (result.hasErrors()) { + return "articles/edit"; + } + article.setId(articleId); + articleService.updateArticle(article); + return "redirect:/articles"; + } + + @GetMapping("/articles/search") + public String searchArticle(@RequestParam(value = "search") String search, Model model) { + UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); + model.addAttribute("user", user); + List<ArticleDto> articles = articleService.searchArticles(search); + model.addAttribute("articles", articles); + return "index"; + } + + @GetMapping("/") + public String getArticles() { + return "redirect:/articles"; + } +} diff --git a/backend/src/main/java/com/blog/web/controllers/AuthController.java b/backend/src/main/java/com/blog/web/controllers/AuthController.java new file mode 100644 index 0000000..efb3672 --- /dev/null +++ b/backend/src/main/java/com/blog/web/controllers/AuthController.java @@ -0,0 +1,56 @@ +package com.blog.web.controllers; + +import com.blog.web.dto.RegistrationDto; +import com.blog.web.models.UserEntity; +import com.blog.web.services.UserService; +import jakarta.validation.Valid; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +@Controller +public class AuthController { + private final UserService userService; + + public AuthController(UserService userService) { + this.userService = userService; + } + + @GetMapping("/userlogin") + public String login(Model model) { + final UserEntity user = userService.getLoggedInUser().orElse(new UserEntity()); + model.addAttribute("user", user); + return "auth/login"; + } + + @GetMapping("/register") + public String getRegisterForm(Model model) { + final RegistrationDto user = new RegistrationDto(); + model.addAttribute("user", user); + return "auth/register"; + } + + @PostMapping("/register/save") + public String register(@Valid @ModelAttribute("user") RegistrationDto user, BindingResult result, Model model) { + UserEntity existingUserEmail = userService.findByEmail(user.getEmail()).orElse(null); + if (existingUserEmail != null && StringUtils.isBlank(existingUserEmail.getEmail())) { + result.rejectValue("email", "There is already a user with this email"); + } + + UserEntity existingUsername = userService.findByUsername(user.getUsername()).orElse(null); + if (existingUsername != null && StringUtils.isBlank(existingUsername.getUsername())) { + result.rejectValue("username", "There is already a user with this username"); + } + + if (result.hasErrors()) { + model.addAttribute("user", user); + return "register"; + } + userService.saveUser(user); + return "redirect:/articles?success"; + } +} diff --git a/backend/src/main/java/com/blog/web/dto/ArticleDto.java b/backend/src/main/java/com/blog/web/dto/ArticleDto.java new file mode 100644 index 0000000..d275f3b --- /dev/null +++ b/backend/src/main/java/com/blog/web/dto/ArticleDto.java @@ -0,0 +1,114 @@ +package com.blog.web.dto; + +import com.blog.web.models.Article; +import com.blog.web.models.UserEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.validator.constraints.URL; + +import java.time.LocalDateTime; + +public class ArticleDto { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @NotEmpty(message = "Article title should not be empty") + private String title; + @NotEmpty(message = "Article Photo URL should not be empty") + @URL(message = "Article Photo URL should be a URL") + private String photoUrl; + @NotEmpty(message = "Article Content should not be empty") + private String content; + @CreationTimestamp + private LocalDateTime createdOn; + @UpdateTimestamp + private LocalDateTime updatedOn; + @ManyToOne + @JoinColumn(name = "created_by", nullable = false) + private UserEntity createdBy; + + public ArticleDto(long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { + this.id = id; + this.title = title; + this.photoUrl = photoUrl; + this.content = content; + this.createdBy = createdBy; + this.createdOn = createdOn; + this.updatedOn = updatedOn; + } + + public ArticleDto() { + } + + ; + + public ArticleDto(Article article) { + this.id = article.getId(); + this.title = article.getTitle(); + this.photoUrl = article.getPhotoUrl(); + this.content = article.getContent(); + this.createdBy = article.getCreatedBy(); + this.createdOn = article.getCreatedOn(); + this.updatedOn = article.getUpdatedOn(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public @NotEmpty(message = "Article title should not be empty") String getTitle() { + return title; + } + + public void setTitle(@NotEmpty(message = "Article title should not be empty") String title) { + this.title = title; + } + + public @NotEmpty(message = "Article Photo URL should not be empty") @URL(message = "Article Photo URL should be a URL") String getPhotoUrl() { + return photoUrl; + } + + public void setPhotoUrl(@NotEmpty(message = "Article Photo URL should not be empty") @URL(message = "Article Photo URL should be a URL") String photoUrl) { + this.photoUrl = photoUrl; + } + + public @NotEmpty(message = "Article Content should not be empty") String getContent() { + return content; + } + + public void setContent(@NotEmpty(message = "Article Content should not be empty") String content) { + this.content = content; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public UserEntity getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(UserEntity createdBy) { + this.createdBy = createdBy; + } + + public String getUsername() { return createdBy.getUsername(); } +} diff --git a/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java b/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java new file mode 100644 index 0000000..3ced6d2 --- /dev/null +++ b/backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java @@ -0,0 +1,96 @@ +package com.blog.web.dto; + +import com.blog.web.models.Article; + +import java.time.LocalDateTime; + +public class ArticlePublicDto { + private Long id; + private String title; + private String photoUrl; + private String content; + private LocalDateTime createdOn; + private LocalDateTime updatedOn; + private String createdBy; + + public ArticlePublicDto(long id, String title, String photoUrl, String content, String createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { + this.id = id; + this.title = title; + this.photoUrl = photoUrl; + this.content = content; + this.createdBy = createdBy; + this.createdOn = createdOn; + this.updatedOn = updatedOn; + } + + public ArticlePublicDto() { + } + + ; + + public ArticlePublicDto(Article article) { + this.id = article.getId(); + this.title = article.getTitle(); + this.photoUrl = article.getPhotoUrl(); + this.content = article.getContent(); + this.createdBy = article.getCreatedBy().getUsername(); + this.createdOn = article.getCreatedOn(); + this.updatedOn = article.getUpdatedOn(); + } + + 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 getPhotoUrl() { + return photoUrl; + } + + public void setPhotoUrl(String photoUrl) { + this.photoUrl = photoUrl; + } + + public String getContent() { + return content; + } + + public void setContent( String content) { + this.content = content; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } +} diff --git a/backend/src/main/java/com/blog/web/dto/RegistrationDto.java b/backend/src/main/java/com/blog/web/dto/RegistrationDto.java new file mode 100644 index 0000000..5b86557 --- /dev/null +++ b/backend/src/main/java/com/blog/web/dto/RegistrationDto.java @@ -0,0 +1,55 @@ +package com.blog.web.dto; + +import jakarta.validation.constraints.NotEmpty; + +public class RegistrationDto { + private Long id; + @NotEmpty + private String username; + @NotEmpty + private String email; + + @NotEmpty + private String password; + + public RegistrationDto() { + } + + public RegistrationDto(String username, String email, String password) { + this.username = username; + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public @NotEmpty String getUsername() { + return username; + } + + public void setUsername(@NotEmpty String username) { + this.username = username; + } + + public @NotEmpty String getEmail() { + return email; + } + + public void setEmail(@NotEmpty String email) { + this.email = email; + } + + public void setPassword(@NotEmpty String password) { + this.password = password; + } + + public @NotEmpty String getPassword() { + return password; + } +} diff --git a/backend/src/main/java/com/blog/web/mappers/ArticleMapper.java b/backend/src/main/java/com/blog/web/mappers/ArticleMapper.java new file mode 100644 index 0000000..8fe729e --- /dev/null +++ b/backend/src/main/java/com/blog/web/mappers/ArticleMapper.java @@ -0,0 +1,22 @@ +package com.blog.web.mappers; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.ArticlePublicDto; +import com.blog.web.models.Article; + +public class ArticleMapper { + private ArticleMapper() { + } + + public static Article mapToArticle(ArticleDto articleDto) { + return new Article(articleDto); + } + + public static ArticleDto mapToArticleDto(Article article) { + return new ArticleDto(article); + } + + public static ArticlePublicDto mapToArticlePublicDto(Article article) { + return new ArticlePublicDto(article); + } +} diff --git a/backend/src/main/java/com/blog/web/models/Article.java b/backend/src/main/java/com/blog/web/models/Article.java new file mode 100644 index 0000000..b54907a --- /dev/null +++ b/backend/src/main/java/com/blog/web/models/Article.java @@ -0,0 +1,104 @@ +package com.blog.web.models; + +import com.blog.web.dto.ArticleDto; +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +public class Article { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String title; + private String photoUrl; + private String content; + @CreationTimestamp + private LocalDateTime createdOn; + @UpdateTimestamp + private LocalDateTime updatedOn; + @ManyToOne + @JoinColumn(name = "created_by", nullable = false) + private UserEntity createdBy; + + public Article(long id, String title, String photoUrl, String content, UserEntity createdBy, LocalDateTime createdOn, LocalDateTime updatedOn) { + this.id = id; + this.title = title; + this.photoUrl = photoUrl; + this.content = content; + this.createdBy = createdBy; + this.createdOn = createdOn; + this.updatedOn = updatedOn; + } + + public Article() { + } + + public Article(ArticleDto articleDto) { + this.id = articleDto.getId(); + this.title = articleDto.getTitle(); + this.photoUrl = articleDto.getPhotoUrl(); + this.content = articleDto.getContent(); + this.createdBy = articleDto.getCreatedBy(); + this.createdOn = articleDto.getCreatedOn(); + this.updatedOn = articleDto.getUpdatedOn(); + } + + 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 getPhotoUrl() { + return photoUrl; + } + + public void setPhotoUrl(String photoUrl) { + this.photoUrl = photoUrl; + } + + public LocalDateTime getCreatedOn() { + return createdOn; + } + + public void setCreatedOn(LocalDateTime createdOn) { + this.createdOn = createdOn; + } + + public LocalDateTime getUpdatedOn() { + return updatedOn; + } + + public void setUpdatedOn(LocalDateTime updatedOn) { + this.updatedOn = updatedOn; + } + + public UserEntity getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(UserEntity createdBy) { + this.createdBy = createdBy; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/backend/src/main/java/com/blog/web/models/Role.java b/backend/src/main/java/com/blog/web/models/Role.java new file mode 100644 index 0000000..2b7143f --- /dev/null +++ b/backend/src/main/java/com/blog/web/models/Role.java @@ -0,0 +1,36 @@ +package com.blog.web.models; + +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity(name = "roles") +public class Role { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String name; + @ManyToMany(mappedBy = "roles") + private List<UserEntity> users = 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 List<UserEntity> getUsers() { + return users; + } +} diff --git a/backend/src/main/java/com/blog/web/models/UserEntity.java b/backend/src/main/java/com/blog/web/models/UserEntity.java new file mode 100644 index 0000000..bf45b21 --- /dev/null +++ b/backend/src/main/java/com/blog/web/models/UserEntity.java @@ -0,0 +1,72 @@ +package com.blog.web.models; + +import jakarta.persistence.*; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Entity(name = "users") +// Named UserEntity to prevent conflicts with Java User object +public class UserEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + private String username; + private String email; + private String password; + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinTable(name = "user_roles", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")}) + private final List<Role> roles = new ArrayList<>(); + + public boolean equals(UserEntity user) { + return this.id == user.getId(); + } + + public User toSecurityUser() { + return new User(this.getEmail(), this.getPassword(), this.getRoles().stream().map((role) -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList())); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public List<Role> getRoles() { + return roles; + } + + public void setRoles(List<Role> roles) { + this.roles.clear(); + this.roles.addAll(roles); + } +} diff --git a/backend/src/main/java/com/blog/web/repository/ArticleRepository.java b/backend/src/main/java/com/blog/web/repository/ArticleRepository.java new file mode 100644 index 0000000..594cb15 --- /dev/null +++ b/backend/src/main/java/com/blog/web/repository/ArticleRepository.java @@ -0,0 +1,12 @@ +package com.blog.web.repository; + +import com.blog.web.models.Article; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface ArticleRepository extends JpaRepository<Article, Long> { + @Query("SELECT a from Article a WHERE a.title LIKE CONCAT('%', :search, '%')") + List<Article> searchArticles(String search); +} diff --git a/backend/src/main/java/com/blog/web/repository/RoleRepository.java b/backend/src/main/java/com/blog/web/repository/RoleRepository.java new file mode 100644 index 0000000..f271bf1 --- /dev/null +++ b/backend/src/main/java/com/blog/web/repository/RoleRepository.java @@ -0,0 +1,10 @@ +package com.blog.web.repository; + +import com.blog.web.models.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RoleRepository extends JpaRepository<Role, Long> { + Optional<Role> findByName(String name); +} diff --git a/backend/src/main/java/com/blog/web/repository/UserRepository.java b/backend/src/main/java/com/blog/web/repository/UserRepository.java new file mode 100644 index 0000000..30eefc5 --- /dev/null +++ b/backend/src/main/java/com/blog/web/repository/UserRepository.java @@ -0,0 +1,14 @@ +package com.blog.web.repository; + +import com.blog.web.models.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository<UserEntity, Long> { + Optional<UserEntity> findByEmail(String email); + + Optional<UserEntity> findByUsername(String username); + + Optional<UserEntity> findFirstByUsername(String username); +} diff --git a/backend/src/main/java/com/blog/web/security/CustomUserDetailsService.java b/backend/src/main/java/com/blog/web/security/CustomUserDetailsService.java new file mode 100644 index 0000000..ee3e950 --- /dev/null +++ b/backend/src/main/java/com/blog/web/security/CustomUserDetailsService.java @@ -0,0 +1,29 @@ +package com.blog.web.security; + +import com.blog.web.models.UserEntity; +import com.blog.web.repository.UserRepository; +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; + +import java.util.stream.Collectors; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserEntity userEntity = userRepository.findFirstByUsername(username).orElse(new UserEntity()); + if (userEntity.getUsername() != null) { + return userEntity.toSecurityUser(); + } else { + throw new UsernameNotFoundException("Invalid username"); + } + } +} diff --git a/backend/src/main/java/com/blog/web/security/SecurityConfig.java b/backend/src/main/java/com/blog/web/security/SecurityConfig.java new file mode 100644 index 0000000..17e09c7 --- /dev/null +++ b/backend/src/main/java/com/blog/web/security/SecurityConfig.java @@ -0,0 +1,38 @@ +package com.blog.web.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfig { + private CustomUserDetailsService userDetailsService; + + public SecurityConfig(CustomUserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Bean + public static PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // disabling csrf leaves us vulnerable, in a real production app do not do this + http.csrf(c -> c.disable()).cors(c -> c.disable()).authorizeHttpRequests(auths -> auths.anyRequest().permitAll()).formLogin(form -> form.loginPage("/userlogin").usernameParameter("username").passwordParameter("password").defaultSuccessUrl("/articles").loginProcessingUrl("/userlogin").failureUrl("/userlogin?error=true").permitAll()).logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/articles")); + return http.build(); + } + + public void configure(AuthenticationManagerBuilder builder) throws Exception { + builder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } +} diff --git a/backend/src/main/java/com/blog/web/security/SecurityUtil.java b/backend/src/main/java/com/blog/web/security/SecurityUtil.java new file mode 100644 index 0000000..ef0b3d9 --- /dev/null +++ b/backend/src/main/java/com/blog/web/security/SecurityUtil.java @@ -0,0 +1,12 @@ +package com.blog.web.security; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtil { + public static String getSessionUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return !(authentication instanceof AnonymousAuthenticationToken) ? authentication.getName() : null; + } +} diff --git a/backend/src/main/java/com/blog/web/services/ArticleService.java b/backend/src/main/java/com/blog/web/services/ArticleService.java new file mode 100644 index 0000000..1bfe38f --- /dev/null +++ b/backend/src/main/java/com/blog/web/services/ArticleService.java @@ -0,0 +1,22 @@ +package com.blog.web.services; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.ArticlePublicDto; +import com.blog.web.models.Article; + +import java.util.List; +import java.util.Optional; + +public interface ArticleService { + List<ArticlePublicDto> findAllArticles(); + + Optional<Article> saveArticle(ArticleDto article); + + ArticleDto findArticleById(long articleId); + + void updateArticle(ArticleDto articleDto); + + boolean delete(Long articleId); + + List<ArticleDto> searchArticles(String search); +} diff --git a/backend/src/main/java/com/blog/web/services/UserService.java b/backend/src/main/java/com/blog/web/services/UserService.java new file mode 100644 index 0000000..b9c231b --- /dev/null +++ b/backend/src/main/java/com/blog/web/services/UserService.java @@ -0,0 +1,16 @@ +package com.blog.web.services; + +import com.blog.web.dto.RegistrationDto; +import com.blog.web.models.UserEntity; + +import java.util.Optional; + +public interface UserService { + void saveUser(RegistrationDto registrationDto); + + Optional<UserEntity> findByEmail(String email); + + Optional<UserEntity> findByUsername(String username); + + public Optional<UserEntity> getLoggedInUser(); +} diff --git a/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java b/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java new file mode 100644 index 0000000..7073073 --- /dev/null +++ b/backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java @@ -0,0 +1,94 @@ +package com.blog.web.services.impl; + +import com.blog.web.dto.ArticleDto; +import com.blog.web.dto.ArticlePublicDto; +import com.blog.web.models.Article; +import com.blog.web.models.UserEntity; +import com.blog.web.repository.ArticleRepository; +import com.blog.web.repository.UserRepository; +import com.blog.web.security.SecurityUtil; +import com.blog.web.services.ArticleService; +import com.blog.web.services.UserService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.blog.web.mappers.ArticleMapper; + +import static com.blog.web.mappers.ArticleMapper.mapToArticle; +import static com.blog.web.mappers.ArticleMapper.mapToArticleDto; + +@Service +public class ArticleServiceImpl implements ArticleService { + final private ArticleRepository articleRepository; + final private UserRepository userRepository; + final private UserService userService; + + public ArticleServiceImpl(ArticleRepository articleRepository, UserRepository userRepository, UserService userService) { + this.userRepository = userRepository; + this.articleRepository = articleRepository; + this.userService = userService; + } + + + @Override + public List<ArticlePublicDto> findAllArticles() { + List<Article> articles = articleRepository.findAll(); + return articles.stream().map(ArticleMapper::mapToArticlePublicDto).collect(Collectors.toList()); + } + + @Override + public Optional<Article> saveArticle(ArticleDto articleDto) { + String username = SecurityUtil.getSessionUser(); + UserEntity user = userRepository.findByUsername(username).orElse(null); + if (user == null) { + return null; + } + Article article = mapToArticle(articleDto); + article.setCreatedBy(user); + return Optional.of(articleRepository.save(article)); + } + + @Override + public ArticleDto findArticleById(long articleId) { + Article article = articleRepository.findById(articleId).get(); + return mapToArticleDto(article); + } + + @Override + public void updateArticle(ArticleDto articleDto) { + String username = SecurityUtil.getSessionUser(); + UserEntity user = userRepository.findByUsername(username).orElse(null); + if (user == null) { + return; + } + Article article = mapToArticle(articleDto); + article.setCreatedBy(user); + articleRepository.save(article); + } + + @Override + public boolean delete(Long articleId) { + final UserEntity user = userService.getLoggedInUser().orElse(null); + if (user == null) { + return false; + } + String userId = user.getUsername(); + ArticleDto article = this.findArticleById(articleId); + String ownerId = article.getUsername(); + if (ownerId.equals(userId)) { + articleRepository.deleteById(articleId); + return true; + } else { + return false; + } + } + + @Override + public List<ArticleDto> searchArticles(String search) { + List<Article> articles = articleRepository.searchArticles(search); + return articles.stream().map(article -> mapToArticleDto(article)).collect(Collectors.toList()); + } +} diff --git a/backend/src/main/java/com/blog/web/services/impl/UserServiceImpl.java b/backend/src/main/java/com/blog/web/services/impl/UserServiceImpl.java new file mode 100644 index 0000000..859e72c --- /dev/null +++ b/backend/src/main/java/com/blog/web/services/impl/UserServiceImpl.java @@ -0,0 +1,60 @@ +package com.blog.web.services.impl; + +import com.blog.web.dto.RegistrationDto; +import com.blog.web.models.Role; +import com.blog.web.models.UserEntity; +import com.blog.web.repository.RoleRepository; +import com.blog.web.repository.UserRepository; +import com.blog.web.security.SecurityUtil; +import com.blog.web.services.UserService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Optional; + +@Service +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + + public UserServiceImpl(UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public void saveUser(RegistrationDto registrationDto) { + UserEntity user = new UserEntity(); + user.setUsername(registrationDto.getUsername()); + user.setEmail(registrationDto.getEmail()); + user.setPassword(passwordEncoder.encode(registrationDto.getPassword())); + + final Role role = roleRepository.findByName("User").orElse(new Role()); + user.setRoles(Arrays.asList(role)); + userRepository.save(user); + } + + @Override + public Optional<UserEntity> findByEmail(String email) { + return userRepository.findByEmail(email); + } + + @Override + public Optional<UserEntity> findByUsername(String username) { + return userRepository.findByUsername(username); + } + + public Optional<UserEntity> getLoggedInUser() { + final Optional<UserEntity> user; + String username = SecurityUtil.getSessionUser(); + if (username != null) { + user = this.findByUsername(username); + } else { + user = Optional.of(new UserEntity()); + } + return user; + } +} |
