summaryrefslogtreecommitdiffhomepage
path: root/backend/src/main/java/com
diff options
context:
space:
mode:
authorrealtradam <[email protected]>2024-07-23 20:47:31 -0400
committerrealtradam <[email protected]>2024-07-23 20:47:31 -0400
commit1e18e0ad7a47536be92384bbf815e0923a06698d (patch)
treeb07405ecdef4f05a96b6c4348930cbee976554cb /backend/src/main/java/com
parent56c59e3b98fe554c4e1484e208e4be5c30f09a04 (diff)
downloadspring-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')
-rw-r--r--backend/src/main/java/com/blog/web/WebApplication.java11
-rw-r--r--backend/src/main/java/com/blog/web/controllers/ArticleController.java125
-rw-r--r--backend/src/main/java/com/blog/web/controllers/AuthController.java56
-rw-r--r--backend/src/main/java/com/blog/web/dto/ArticleDto.java114
-rw-r--r--backend/src/main/java/com/blog/web/dto/ArticlePublicDto.java96
-rw-r--r--backend/src/main/java/com/blog/web/dto/RegistrationDto.java55
-rw-r--r--backend/src/main/java/com/blog/web/mappers/ArticleMapper.java22
-rw-r--r--backend/src/main/java/com/blog/web/models/Article.java104
-rw-r--r--backend/src/main/java/com/blog/web/models/Role.java36
-rw-r--r--backend/src/main/java/com/blog/web/models/UserEntity.java72
-rw-r--r--backend/src/main/java/com/blog/web/repository/ArticleRepository.java12
-rw-r--r--backend/src/main/java/com/blog/web/repository/RoleRepository.java10
-rw-r--r--backend/src/main/java/com/blog/web/repository/UserRepository.java14
-rw-r--r--backend/src/main/java/com/blog/web/security/CustomUserDetailsService.java29
-rw-r--r--backend/src/main/java/com/blog/web/security/SecurityConfig.java38
-rw-r--r--backend/src/main/java/com/blog/web/security/SecurityUtil.java12
-rw-r--r--backend/src/main/java/com/blog/web/services/ArticleService.java22
-rw-r--r--backend/src/main/java/com/blog/web/services/UserService.java16
-rw-r--r--backend/src/main/java/com/blog/web/services/impl/ArticleServiceImpl.java94
-rw-r--r--backend/src/main/java/com/blog/web/services/impl/UserServiceImpl.java60
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;
+ }
+}