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