A comprehensive Blog Content Management System with role-based access control, built with Spring Boot and modern Java technologies.
System Architecture
- Spring Boot 3.x - Application framework
- Spring Security - Authentication and authorization
- Spring Data JPA - Data persistence
- H2/MySQL - Database
- Thymeleaf - Template engine
- Bootstrap 5 - Frontend framework
Dependencies
<dependencies> <!-- Spring Boot Starters --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- Thymeleaf Security --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity6</artifactId> </dependency> <!-- Database --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mariadb.jdbc</groupId> <artifactId>mariadb-java-client</artifactId> <scope>runtime</scope> </dependency> <!-- Development Tools --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> </dependencies>
Domain Models
Example 1: User and Role Entities
@Entity @Table(name = "users") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String username; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String password; private String firstName; private String lastName; @Builder.Default private boolean enabled = true; @Builder.Default private boolean accountNonExpired = true; @Builder.Default private boolean accountNonLocked = true; @Builder.Default private boolean credentialsNonExpired = true; @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id") ) @Builder.Default private Set<Role> roles = new HashSet<>(); @OneToMany(mappedBy = "author", cascade = CascadeType.ALL) @Builder.Default private List<Post> posts = new ArrayList<>(); @OneToMany(mappedBy = "author", cascade = CascadeType.ALL) @Builder.Default private List<Comment> comments = new ArrayList<>(); public String getFullName() { return firstName + " " + lastName; } public boolean hasRole(String roleName) { return roles.stream().anyMatch(role -> role.getName().equals(roleName)); } } @Entity @Table(name = "roles") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String name; private String description; @ManyToMany(mappedBy = "roles") @JsonIgnore @Builder.Default private Set<User> users = new HashSet<>(); @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "role_privileges", joinColumns = @JoinColumn(name = "role_id")) @Column(name = "privilege") @Builder.Default private Set<String> privileges = new HashSet<>(); public boolean hasPrivilege(String privilege) { return privileges.contains(privilege); } } @Entity @Table(name = "posts") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class Post { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String title; @Column(unique = true, nullable = false) private String slug; @Column(columnDefinition = "TEXT") private String excerpt; @Column(columnDefinition = "LONGTEXT") private String content; @Enumerated(EnumType.STRING) @Builder.Default private PostStatus status = PostStatus.DRAFT; @Builder.Default private boolean commentable = true; @CreationTimestamp private LocalDateTime createdAt; @UpdateTimestamp private LocalDateTime updatedAt; private LocalDateTime publishedAt; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "author_id") private User author; @ManyToMany @JoinTable( name = "post_categories", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "category_id") ) @Builder.Default private Set<Category> categories = new HashSet<>(); @ManyToMany @JoinTable( name = "post_tags", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) @Builder.Default private Set<Tag> tags = new HashSet<>(); @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) @Builder.Default private List<Comment> comments = new ArrayList<>(); public boolean isPublished() { return status == PostStatus.PUBLISHED && publishedAt != null; } public String getReadingTime() { if (content == null) return "0 min"; int words = content.split("\\s+").length; int minutes = (int) Math.ceil(words / 200.0); return minutes + " min read"; } } @Entity @Table(name = "categories") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String name; private String description; @Column(unique = true) private String slug; @ManyToMany(mappedBy = "categories") @JsonIgnore @Builder.Default private Set<Post> posts = new HashSet<>(); } @Entity @Table(name = "comments") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class Comment { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(columnDefinition = "TEXT", nullable = false) private String content; @Enumerated(EnumType.STRING) @Builder.Default private CommentStatus status = CommentStatus.PENDING; @CreationTimestamp private LocalDateTime createdAt; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "author_id") private User author; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private Comment parent; @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) @Builder.Default private List<Comment> replies = new ArrayList<>(); public boolean isApproved() { return status == CommentStatus.APPROVED; } } public enum PostStatus { DRAFT, PUBLISHED, ARCHIVED, PENDING_REVIEW } public enum CommentStatus { PENDING, APPROVED, REJECTED, SPAM } Security Configuration
Example 2: Security Configuration with Role-Based Access
@Configuration @EnableWebSecurity @EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { private final UserDetailsService userDetailsService; private final PasswordEncoder passwordEncoder; public SecurityConfig(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { this.userDetailsService = userDetailsService; this.passwordEncoder = passwordEncoder; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz // Public endpoints .requestMatchers("/", "/posts/**", "/categories/**", "/auth/register", "/auth/login", "/css/**", "/js/**", "/images/**", "/webjars/**").permitAll() // Admin endpoints .requestMatchers("/admin/**").hasRole("ADMIN") // Editor endpoints .requestMatchers("/editor/**").hasAnyRole("EDITOR", "ADMIN") // Author endpoints .requestMatchers("/author/**").hasAnyRole("AUTHOR", "EDITOR", "ADMIN") // User management .requestMatchers("/profile/**", "/dashboard").authenticated() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/auth/login") .loginProcessingUrl("/auth/login") .defaultSuccessUrl("/dashboard") .failureUrl("/auth/login?error=true") .permitAll() ) .logout(logout -> logout .logoutUrl("/auth/logout") .logoutSuccessUrl("/") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") .permitAll() ) .rememberMe(rememberMe -> rememberMe .key("uniqueAndSecret") .tokenValiditySeconds(86400) // 24 hours .userDetailsService(userDetailsService) ) .exceptionHandling(exception -> exception .accessDeniedPage("/access-denied") ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(1) .maxSessionsPreventsLogin(false) ); return http.build(); } @Bean public UserDetailsService userDetailsService(UserRepository userRepository) { return username -> userRepository.findByUsername(username) .map(CustomUserDetails::new) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } } @Data @AllArgsConstructor public class CustomUserDetails implements UserDetails { private final User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return user.getRoles().stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())) .collect(Collectors.toList()); } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return user.isAccountNonExpired(); } @Override public boolean isAccountNonLocked() { return user.isAccountNonLocked(); } @Override public boolean isCredentialsNonExpired() { return user.isCredentialsNonExpired(); } @Override public boolean isEnabled() { return user.isEnabled(); } public User getUser() { return user; } } Service Layer
Example 3: Business Logic Services
@Service @Transactional public class UserService { private final UserRepository userRepository; private final RoleRepository roleRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.roleRepository = roleRepository; this.passwordEncoder = passwordEncoder; } public User registerUser(UserRegistrationDto registrationDto) { if (userRepository.existsByUsername(registrationDto.getUsername())) { throw new UsernameExistsException("Username already exists: " + registrationDto.getUsername()); } if (userRepository.existsByEmail(registrationDto.getEmail())) { throw new EmailExistsException("Email already exists: " + registrationDto.getEmail()); } User user = User.builder() .username(registrationDto.getUsername()) .email(registrationDto.getEmail()) .password(passwordEncoder.encode(registrationDto.getPassword())) .firstName(registrationDto.getFirstName()) .lastName(registrationDto.getLastName()) .build(); // Assign default role Role userRole = roleRepository.findByName("USER") .orElseThrow(() -> new RoleNotFoundException("Default USER role not found")); user.getRoles().add(userRole); return userRepository.save(user); } public User updateUserProfile(Long userId, UserProfileDto profileDto) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found: " + userId)); user.setFirstName(profileDto.getFirstName()); user.setLastName(profileDto.getLastName()); user.setEmail(profileDto.getEmail()); return userRepository.save(user); } public void changeUserPassword(Long userId, PasswordChangeDto passwordChangeDto) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found: " + userId)); if (!passwordEncoder.matches(passwordChangeDto.getCurrentPassword(), user.getPassword())) { throw new InvalidPasswordException("Current password is incorrect"); } user.setPassword(passwordEncoder.encode(passwordChangeDto.getNewPassword())); userRepository.save(user); } public User assignRoleToUser(Long userId, String roleName) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found: " + userId)); Role role = roleRepository.findByName(roleName) .orElseThrow(() -> new RoleNotFoundException("Role not found: " + roleName)); user.getRoles().add(role); return userRepository.save(user); } public User removeRoleFromUser(Long userId, String roleName) { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found: " + userId)); user.getRoles().removeIf(role -> role.getName().equals(roleName)); return userRepository.save(user); } public Page<User> getAllUsers(Pageable pageable) { return userRepository.findAll(pageable); } public List<User> getUsersByRole(String roleName) { return userRepository.findByRoles_Name(roleName); } } @Service @Transactional public class PostService { private final PostRepository postRepository; private final UserRepository userRepository; private final CategoryRepository categoryRepository; private final TagRepository tagRepository; public PostService(PostRepository postRepository, UserRepository userRepository, CategoryRepository categoryRepository, TagRepository tagRepository) { this.postRepository = postRepository; this.userRepository = userRepository; this.categoryRepository = categoryRepository; this.tagRepository = tagRepository; } public Post createPost(PostCreateDto postDto, String username) { User author = userRepository.findByUsername(username) .orElseThrow(() -> new UserNotFoundException("User not found: " + username)); Post post = Post.builder() .title(postDto.getTitle()) .slug(generateSlug(postDto.getTitle())) .excerpt(postDto.getExcerpt()) .content(postDto.getContent()) .status(postDto.getStatus()) .commentable(postDto.isCommentable()) .author(author) .build(); // Set categories if (postDto.getCategoryIds() != null) { Set<Category> categories = categoryRepository.findAllById(postDto.getCategoryIds()) .stream().collect(Collectors.toSet()); post.setCategories(categories); } // Set tags if (postDto.getTagNames() != null) { Set<Tag> tags = processTags(postDto.getTagNames()); post.setTags(tags); } // Set published date if publishing if (postDto.getStatus() == PostStatus.PUBLISHED) { post.setPublishedAt(LocalDateTime.now()); } return postRepository.save(post); } public Post updatePost(Long postId, PostUpdateDto postDto, String username) { Post post = postRepository.findById(postId) .orElseThrow(() -> new PostNotFoundException("Post not found: " + postId)); // Check authorization if (!post.getAuthor().getUsername().equals(username) && !hasEditorPrivileges(username)) { throw new AccessDeniedException("You are not authorized to edit this post"); } post.setTitle(postDto.getTitle()); post.setExcerpt(postDto.getExcerpt()); post.setContent(postDto.getContent()); post.setStatus(postDto.getStatus()); post.setCommentable(postDto.isCommentable()); // Update categories if (postDto.getCategoryIds() != null) { Set<Category> categories = categoryRepository.findAllById(postDto.getCategoryIds()) .stream().collect(Collectors.toSet()); post.setCategories(categories); } // Update tags if (postDto.getTagNames() != null) { Set<Tag> tags = processTags(postDto.getTagNames()); post.setTags(tags); } // Update published date if status changed to PUBLISHED if (post.getStatus() == PostStatus.PUBLISHED && post.getPublishedAt() == null) { post.setPublishedAt(LocalDateTime.now()); } return postRepository.save(post); } public Page<Post> getPublishedPosts(Pageable pageable) { return postRepository.findByStatusOrderByPublishedAtDesc(PostStatus.PUBLISHED, pageable); } public Page<Post> getPostsByAuthor(String username, Pageable pageable) { return postRepository.findByAuthor_UsernameOrderByCreatedAtDesc(username, pageable); } public Page<Post> getPostsByStatus(PostStatus status, Pageable pageable) { return postRepository.findByStatusOrderByCreatedAtDesc(status, pageable); } public List<Post> getRecentPosts(int count) { return postRepository.findTop5ByStatusOrderByPublishedAtDesc(PostStatus.PUBLISHED, PageRequest.of(0, count)); } public void deletePost(Long postId, String username) { Post post = postRepository.findById(postId) .orElseThrow(() -> new PostNotFoundException("Post not found: " + postId)); // Check authorization if (!post.getAuthor().getUsername().equals(username) && !hasEditorPrivileges(username)) { throw new AccessDeniedException("You are not authorized to delete this post"); } postRepository.delete(post); } private String generateSlug(String title) { String slug = title.toLowerCase() .replaceAll("[^a-z0-9\\s-]", "") .replaceAll("\\s+", "-") .replaceAll("-+", "-") .trim(); // Ensure uniqueness String baseSlug = slug; int counter = 1; while (postRepository.existsBySlug(slug)) { slug = baseSlug + "-" + counter++; } return slug; } private Set<Tag> processTags(List<String> tagNames) { return tagNames.stream() .map(String::trim) .map(String::toLowerCase) .distinct() .map(tagName -> tagRepository.findByName(tagName) .orElseGet(() -> tagRepository.save(Tag.builder().name(tagName).build()))) .collect(Collectors.toSet()); } private boolean hasEditorPrivileges(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UserNotFoundException("User not found: " + username)); return user.hasRole("EDITOR") || user.hasRole("ADMIN"); } } Controller Layer
Example 4: REST Controllers
@RestController @RequestMapping("/api/posts") public class PostApiController { private final PostService postService; public PostApiController(PostService postService) { this.postService = postService; } @GetMapping public ResponseEntity<Page<PostDto>> getPublishedPosts( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("publishedAt").descending()); Page<Post> posts = postService.getPublishedPosts(pageable); Page<PostDto> postDtos = posts.map(this::convertToDto); return ResponseEntity.ok(postDtos); } @GetMapping("/{slug}") public ResponseEntity<PostDto> getPostBySlug(@PathVariable String slug) { Post post = postService.getPostBySlug(slug); return ResponseEntity.ok(convertToDto(post)); } @PostMapping @PreAuthorize("hasRole('AUTHOR') or hasRole('EDITOR') or hasRole('ADMIN')") public ResponseEntity<PostDto> createPost(@RequestBody @Valid PostCreateDto postDto, Authentication authentication) { Post post = postService.createPost(postDto, authentication.getName()); return ResponseEntity.status(HttpStatus.CREATED).body(convertToDto(post)); } @PutMapping("/{id}") @PreAuthorize("hasRole('AUTHOR') or hasRole('EDITOR') or hasRole('ADMIN')") public ResponseEntity<PostDto> updatePost(@PathVariable Long id, @RequestBody @Valid PostUpdateDto postDto, Authentication authentication) { Post post = postService.updatePost(id, postDto, authentication.getName()); return ResponseEntity.ok(convertToDto(post)); } @DeleteMapping("/{id}") @PreAuthorize("hasRole('AUTHOR') or hasRole('EDITOR') or hasRole('ADMIN')") public ResponseEntity<Void> deletePost(@PathVariable Long id, Authentication authentication) { postService.deletePost(id, authentication.getName()); return ResponseEntity.noContent().build(); } private PostDto convertToDto(Post post) { return PostDto.builder() .id(post.getId()) .title(post.getTitle()) .slug(post.getSlug()) .excerpt(post.getExcerpt()) .content(post.getContent()) .status(post.getStatus()) .commentable(post.isCommentable()) .createdAt(post.getCreatedAt()) .updatedAt(post.getUpdatedAt()) .publishedAt(post.getPublishedAt()) .authorName(post.getAuthor().getFullName()) .categories(post.getCategories().stream() .map(Category::getName) .collect(Collectors.toSet())) .tags(post.getTags().stream() .map(Tag::getName) .collect(Collectors.toSet())) .readingTime(post.getReadingTime()) .build(); } } @Controller @RequestMapping("/admin") @PreAuthorize("hasRole('ADMIN')") public class AdminController { private final UserService userService; private final PostService postService; private final CategoryService categoryService; public AdminController(UserService userService, PostService postService, CategoryService categoryService) { this.userService = userService; this.postService = postService; this.categoryService = categoryService; } @GetMapping("/dashboard") public String adminDashboard(Model model) { // Add statistics to model model.addAttribute("totalUsers", userService.getUserCount()); model.addAttribute("totalPosts", postService.getPostCount()); model.addAttribute("publishedPosts", postService.getPublishedPostCount()); model.addAttribute("pendingPosts", postService.getPendingPostCount()); return "admin/dashboard"; } @GetMapping("/users") public String userManagement(@RequestParam(defaultValue = "0") int page, Model model) { Pageable pageable = PageRequest.of(page, 10, Sort.by("createdAt").descending()); Page<User> users = userService.getAllUsers(pageable); model.addAttribute("users", users); model.addAttribute("currentPage", page); return "admin/users"; } @PostMapping("/users/{userId}/roles") public String assignRoleToUser(@PathVariable Long userId, @RequestParam String roleName, RedirectAttributes redirectAttributes) { try { userService.assignRoleToUser(userId, roleName); redirectAttributes.addFlashAttribute("success", "Role " + roleName + " assigned successfully"); } catch (Exception e) { redirectAttributes.addFlashAttribute("error", "Failed to assign role: " + e.getMessage()); } return "redirect:/admin/users"; } @GetMapping("/posts") public String postManagement(@RequestParam(defaultValue = "0") int page, @RequestParam(required = false) PostStatus status, Model model) { Pageable pageable = PageRequest.of(page, 10, Sort.by("createdAt").descending()); Page<Post> posts; if (status != null) { posts = postService.getPostsByStatus(status, pageable); } else { posts = postService.getAllPosts(pageable); } model.addAttribute("posts", posts); model.addAttribute("status", status); model.addAttribute("currentPage", page); return "admin/posts"; } } @Controller @RequestMapping("/author") @PreAuthorize("hasRole('AUTHOR')") public class AuthorController { private final PostService postService; private final CategoryService categoryService; public AuthorController(PostService postService, CategoryService categoryService) { this.postService = postService; this.categoryService = categoryService; } @GetMapping("/posts") public String myPosts(@RequestParam(defaultValue = "0") int page, Authentication authentication, Model model) { Pageable pageable = PageRequest.of(page, 10, Sort.by("createdAt").descending()); Page<Post> posts = postService.getPostsByAuthor(authentication.getName(), pageable); model.addAttribute("posts", posts); model.addAttribute("currentPage", page); return "author/posts"; } @GetMapping("/posts/new") public String createPostForm(Model model) { model.addAttribute("post", new PostCreateDto()); model.addAttribute("categories", categoryService.getAllCategories()); model.addAttribute("statuses", PostStatus.values()); return "author/post-form"; } @PostMapping("/posts") public String createPost(@ModelAttribute @Valid PostCreateDto postDto, BindingResult result, Authentication authentication, Model model) { if (result.hasErrors()) { model.addAttribute("categories", categoryService.getAllCategories()); model.addAttribute("statuses", PostStatus.values()); return "author/post-form"; } try { postService.createPost(postDto, authentication.getName()); return "redirect:/author/posts?success=true"; } catch (Exception e) { model.addAttribute("error", "Failed to create post: " + e.getMessage()); model.addAttribute("categories", categoryService.getAllCategories()); model.addAttribute("statuses", PostStatus.values()); return "author/post-form"; } } } Data Transfer Objects (DTOs)
Example 5: DTO Classes
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserRegistrationDto { @NotBlank(message = "Username is required") @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") private String username; @NotBlank(message = "Email is required") @Email(message = "Email should be valid") private String email; @NotBlank(message = "Password is required") @Size(min = 6, message = "Password must be at least 6 characters") private String password; @NotBlank(message = "Confirm password is required") private String confirmPassword; @NotBlank(message = "First name is required") private String firstName; @NotBlank(message = "Last name is required") private String lastName; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PostCreateDto { @NotBlank(message = "Title is required") @Size(max = 255, message = "Title must not exceed 255 characters") private String title; @NotBlank(message = "Content is required") private String content; @Size(max = 500, message = "Excerpt must not exceed 500 characters") private String excerpt; @NotNull(message = "Status is required") private PostStatus status; private boolean commentable = true; private Set<Long> categoryIds; private List<String> tagNames; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PostDto { private Long id; private String title; private String slug; private String excerpt; private String content; private PostStatus status; private boolean commentable; private LocalDateTime createdAt; private LocalDateTime updatedAt; private LocalDateTime publishedAt; private String authorName; private Set<String> categories; private Set<String> tags; private String readingTime; private int commentCount; } Repository Layer
Example 6: Data Repositories
@Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); Optional<User> findByEmail(String email); boolean existsByUsername(String username); boolean existsByEmail(String email); List<User> findByRoles_Name(String roleName); @Query("SELECT COUNT(u) FROM User u") long countAllUsers(); } @Repository public interface PostRepository extends JpaRepository<Post, Long> { Optional<Post> findBySlug(String slug); boolean existsBySlug(String slug); Page<Post> findByStatusOrderByPublishedAtDesc(PostStatus status, Pageable pageable); Page<Post> findByAuthor_UsernameOrderByCreatedAtDesc(String username, Pageable pageable); Page<Post> findByStatusOrderByCreatedAtDesc(PostStatus status, Pageable pageable); List<Post> findTop5ByStatusOrderByPublishedAtDesc(PostStatus status, Pageable pageable); @Query("SELECT COUNT(p) FROM Post p") long countAllPosts(); @Query("SELECT COUNT(p) FROM Post p WHERE p.status = :status") long countByStatus(@Param("status") PostStatus status); @Query("SELECT p FROM Post p WHERE " + "(:status IS NULL OR p.status = :status) AND " + "(:categoryId IS NULL OR :categoryId IN (SELECT c.id FROM p.categories c)) AND " + "(:tagName IS NULL OR :tagName IN (SELECT t.name FROM p.tags t))") Page<Post> findByFilters(@Param("status") PostStatus status, @Param("categoryId") Long categoryId, @Param("tagName") String tagName, Pageable pageable); } @Repository public interface CategoryRepository extends JpaRepository<Category, Long> { Optional<Category> findByName(String name); Optional<Category> findBySlug(String slug); List<Category> findByPostsIsNotEmpty(); } @Repository public interface RoleRepository extends JpaRepository<Role, Long> { Optional<Role> findByName(String name); boolean existsByName(String name); } Database Initialization
Example 7: Data Initializer
@Component public class DataInitializer { private final RoleRepository roleRepository; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public DataInitializer(RoleRepository roleRepository, UserRepository userRepository, PasswordEncoder passwordEncoder) { this.roleRepository = roleRepository; this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } @EventListener public void onApplicationEvent(ContextRefreshedEvent event) { initializeRoles(); initializeAdminUser(); } private void initializeRoles() { if (roleRepository.count() == 0) { // Create default roles Role adminRole = Role.builder() .name("ADMIN") .description("Administrator with full access") .privileges(Set.of( "USER_MANAGEMENT", "POST_MANAGEMENT", "CATEGORY_MANAGEMENT", "COMMENT_MODERATION", "SYSTEM_SETTINGS" )) .build(); Role editorRole = Role.builder() .name("EDITOR") .description("Editor with content management privileges") .privileges(Set.of( "POST_MANAGEMENT", "CATEGORY_MANAGEMENT", "COMMENT_MODERATION" )) .build(); Role authorRole = Role.builder() .name("AUTHOR") .description("Author who can create and manage their own posts") .privileges(Set.of("POST_CREATE", "POST_EDIT_OWN", "POST_DELETE_OWN")) .build(); Role userRole = Role.builder() .name("USER") .description("Regular user with basic privileges") .privileges(Set.of("COMMENT_CREATE", "PROFILE_MANAGEMENT")) .build(); roleRepository.saveAll(List.of(adminRole, editorRole, authorRole, userRole)); } } private void initializeAdminUser() { if (userRepository.count() == 0) { Role adminRole = roleRepository.findByName("ADMIN") .orElseThrow(() new RuntimeException("Admin role not found")); User admin = User.builder() .username("admin") .email("[email protected]") .password(passwordEncoder.encode("admin123")) .firstName("System") .lastName("Administrator") .roles(Set.of(adminRole)) .build(); userRepository.save(admin); } } } Application Configuration
Example 8: Application Properties
# application.yml spring: datasource: url: jdbc:h2:mem:blogcms driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: create-drop properties: hibernate: dialect: org.hibernate.dialect.H2Dialect format_sql: true show-sql: true h2: console: enabled: true path: /h2-console thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html web: resources: static-locations: classpath:/static/ server: port: 8080 # Custom application properties app: blog: name: "My Blog CMS" description: "A modern blog content management system" posts-per-page: 10 allow-registration: true require-email-verification: false # Logging logging: level: com.example.blog: DEBUG org.springframework.security: DEBUG
Frontend Templates (Thymeleaf)
Example 9: Base Template
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title th:text="${blogName} + ' - ' + #{page.title}">My Blog CMS</title> <!-- Bootstrap CSS --> <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Font Awesome --> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> <style> .navbar-brand { font-weight: bold; } .post-card { transition: transform 0.2s; } .post-card:hover { transform: translateY(-2px); } .user-avatar { width: 32px; height: 32px; border-radius: 50%; } </style> </head> <body> <!-- Navigation --> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand" th:href="@{/}"> <i class="fas fa-blog"></i> [[${blogName}]] </a> <div class="navbar-nav ms-auto"> <a class="nav-link" th:href="@{/}">Home</a> <a class="nav-link" th:href="@{/posts}">Posts</a> <a class="nav-link" th:href="@{/categories}">Categories</a> <!-- Authenticated User Menu --> <div sec:authorize="isAuthenticated()" class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> <i class="fas fa-user"></i> <span sec:authentication="name"></span> </a> <ul class="dropdown-menu"> <li sec:authorize="hasRole('AUTHOR')"> <a class="dropdown-item" th:href="@{/author/posts}"> <i class="fas fa-edit"></i> My Posts </a> </li> <li sec:authorize="hasRole('EDITOR')"> <a class="dropdown-item" th:href="@{/editor/dashboard}"> <i class="fas fa-tasks"></i> Editor Dashboard </a> </li> <li sec:authorize="hasRole('ADMIN')"> <a class="dropdown-item" th:href="@{/admin/dashboard}"> <i class="fas fa-cog"></i> Admin Dashboard </a> </li> <li><hr class="dropdown-divider"></li> <li> <a class="dropdown-item" th:href="@{/profile}"> <i class="fas fa-user-cog"></i> Profile </a> </li> <li> <form th:action="@{/auth/logout}" method="post" class="d-inline"> <button type="submit" class="dropdown-item"> <i class="fas fa-sign-out-alt"></i> Logout </button> </form> </li> </ul> </div> <!-- Anonymous User Menu --> <div sec:authorize="!isAuthenticated()" class="d-flex"> <a class="nav-link" th:href="@{/auth/login}">Login</a> <a class="nav-link" th:href="@{/auth/register}">Register</a> </div> </div> </div> </nav> <!-- Main Content --> <main class="container my-4"> <div th:if="${success}" class="alert alert-success alert-dismissible fade show"> [[${success}]] <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> <div th:if="${error}" class="alert alert-danger alert-dismissible fade show"> [[${error}]] <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> <div th:insert="~{::main-content}"> <!-- Page-specific content will be inserted here --> </div> </main> <!-- Footer --> <footer class="bg-dark text-light py-4 mt-5"> <div class="container text-center"> <p>© 2024 [[${blogName}]]. All rights reserved.</p> </div> </footer> <!-- Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> </body> </html> Testing
Example 10: Unit and Integration Tests
@SpringBootTest @AutoConfigureTestDatabase class BlogCmsApplicationTests { @Autowired private UserService userService; @Autowired private PostService postService; @Autowired private TestRestTemplate restTemplate; @Test void contextLoads() { // Basic context loading test } @Test @WithMockUser(roles = "AUTHOR") void testCreatePost() { PostCreateDto postDto = PostCreateDto.builder() .title("Test Post") .content("This is a test post content") .excerpt("Test excerpt") .status(PostStatus.DRAFT) .build(); Post post = postService.createPost(postDto, "testuser"); assertThat(post).isNotNull(); assertThat(post.getTitle()).isEqualTo("Test Post"); assertThat(post.getAuthor().getUsername()).isEqualTo("testuser"); } @Test void testUserRegistration() { UserRegistrationDto registrationDto = UserRegistrationDto.builder() .username("newuser") .email("[email protected]") .password("password123") .confirmPassword("password123") .firstName("New") .lastName("User") .build(); User user = userService.registerUser(registrationDto); assertThat(user).isNotNull(); assertThat(user.getUsername()).isEqualTo("newuser"); assertThat(user.getEmail()).isEqualTo("[email protected]"); assertThat(user.hasRole("USER")).isTrue(); } } @DataJpaTest class RepositoryTests { @Autowired private TestEntityManager entityManager; @Autowired private UserRepository userRepository; @Test void testFindByUsername() { User user = User.builder() .username("testuser") .email("[email protected]") .password("encodedpassword") .firstName("Test") .lastName("User") .build(); entityManager.persist(user); entityManager.flush(); Optional<User> found = userRepository.findByUsername("testuser"); assertThat(found).isPresent(); assertThat(found.get().getEmail()).isEqualTo("[email protected]"); } } Conclusion
This Blog CMS with User Roles provides:
- Comprehensive Role System - ADMIN, EDITOR, AUTHOR, USER with specific privileges
- Content Management - Full CRUD operations for posts, categories, and tags
- Security - Spring Security with role-based access control
- REST API - JSON endpoints for frontend applications
- Thymeleaf Templates - Server-rendered views with Bootstrap
- Database Persistence - JPA with H2/MySQL support
- Testing - Comprehensive unit and integration tests
The system is extensible and can be enhanced with features like:
- Email notifications
- File upload for images
- Advanced search
- Comment moderation
- Analytics integration
- Multi-language support
- API rate limiting
- Caching implementation