@Override
54 | * public int hashCode() {
55 | * return java.util.Objects.hash(timestamp, message, details);
56 | * }
57 | *
58 | */
59 | public record ErrorDetails(Date timestamp, String message, String details) {
60 | }
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/exception/ResourceNotFoundException.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.exception;
2 |
3 | import java.io.Serial;
4 |
5 | /**
6 | * Custom exception class to handle scenarios where a requested resource
7 | * is not found in the Library Management System.
8 | * This exception is thrown when an operation fails to locate a resource such as
9 | * a book, member, or borrowing record.
10 | */
11 | public class ResourceNotFoundException extends RuntimeException {
12 |
13 | /**
14 | * The {@code serialVersionUID} is a unique identifier for each version of a serializable class.
15 | * It is used during the deserialization process to verify that the sender and receiver of a
16 | * serialized object have loaded classes for that object that are compatible with each other.
17 | *
18 | * The {@code serialVersionUID} field is important for ensuring that a serialized class
19 | * (especially when transmitted over a network or saved to disk) can be successfully deserialized,
20 | * even if the class definition changes in later versions. If the {@code serialVersionUID} does not
21 | * match during deserialization, an {@code InvalidClassException} is thrown.
22 | *
23 | * This field is optional, but it is good practice to explicitly declare it to prevent
24 | * automatic generation, which could lead to compatibility issues when the class structure changes.
25 | *
26 | * The {@code @Serial} annotation is used here to indicate that this field is related to
27 | * serialization. This annotation is available starting from Java 14 and helps improve clarity
28 | * regarding the purpose of this field.
29 | */
30 | @Serial
31 | private static final long serialVersionUID = 1L;
32 |
33 | /**
34 | * Constructs a new {@code ResourceNotFoundException} with the specified detail message.
35 | *
36 | * @param message the detail message explaining the reason for the exception
37 | */
38 | public ResourceNotFoundException(String message) {
39 | super(message);
40 | }
41 |
42 | /**
43 | * Constructs a new {@code ResourceNotFoundException} with the specified detail message and cause.
44 | *
45 | * @param message the detail message explaining the reason for the exception
46 | * @param cause the cause of the exception (which is saved for later retrieval by the {@link #getCause()} method)
47 | */
48 | public ResourceNotFoundException(String message, Throwable cause) {
49 | super(message, cause);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/exception/InvalidSortFieldException.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.exception;
2 |
3 | import java.io.Serial;
4 |
5 | /**
6 | * Custom exception class to handle scenarios where an invalid sort field
7 | * is provided for API requests in the Library Management System.
8 | * This exception is thrown when a sorting operation is attempted
9 | * using a field that does not exist or is not supported.
10 | */
11 | public class InvalidSortFieldException extends RuntimeException {
12 |
13 | /**
14 | * The {@code serialVersionUID} is a unique identifier for each version of a serializable class.
15 | * It is used during the deserialization process to verify that the sender and receiver of a
16 | * serialized object have loaded classes for that object that are compatible with each other.
17 | *
18 | * The {@code serialVersionUID} field is important for ensuring that a serialized class
19 | * (especially when transmitted over a network or saved to disk) can be successfully deserialized,
20 | * even if the class definition changes in later versions. If the {@code serialVersionUID} does not
21 | * match during deserialization, an {@code InvalidClassException} is thrown.
22 | *
23 | * This field is optional, but it is good practice to explicitly declare it to prevent
24 | * automatic generation, which could lead to compatibility issues when the class structure changes.
25 | *
26 | * The {@code @Serial} annotation is used here to indicate that this field is related to
27 | * serialization. This annotation is available starting from Java 14 and helps improve clarity
28 | * regarding the purpose of this field.
29 | */
30 | @Serial
31 | private static final long serialVersionUID = 1L;
32 |
33 | /**
34 | * Constructs a new {@code InvalidSortFieldException} with the specified detail message.
35 | *
36 | * @param message the detail message explaining the reason for the exception
37 | */
38 | public InvalidSortFieldException(String message) {
39 | super(message);
40 | }
41 |
42 | /**
43 | * Constructs a new {@code InvalidSortFieldException} with the specified detail message and cause.
44 | *
45 | * @param message the detail message explaining the reason for the exception
46 | * @param cause the cause of the exception (which is saved for later retrieval by the {@link #getCause()} method)
47 | */
48 | public InvalidSortFieldException(String message, Throwable cause) {
49 | super(message, cause);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: Report a Bug 🐞
2 | description: Help us improve by reporting any issues you encounter.
3 | title: "[BUG] Brief description"
4 | labels: ["status: awaiting triage"]
5 | #assignees: ' '
6 | body:
7 | - type: checkboxes
8 | id: duplicates
9 | attributes:
10 | label: Is this a new issue?
11 | description: Please confirm you've checked that this issue hasn't been reported before.
12 | options:
13 | - label: I have searched "open" and "closed" issues, and this is not a duplicate.
14 | required: true
15 |
16 | - type: textarea
17 | id: description
18 | attributes:
19 | label: Bug Description
20 | description: Please provide a clear description of the issue. You can explain it in your own way if preferred.
21 | placeholder: |
22 | Please describe the bug in detail. For example:
23 | - What happened?
24 | - Where did it happen?
25 | validations:
26 | required: true
27 |
28 | - type: textarea
29 | id: steps-to-reproduce
30 | attributes:
31 | label: Steps to Reproduce
32 | description: Outline the steps to reproduce the issue. If unsure, just provide any relevant details.
33 | placeholder: |
34 | If you know the steps, you can follow this format:
35 |
36 | 1. Go to [page or feature]
37 | 2. [Describe action taken]
38 | 3. [Describe what went wrong]
39 |
40 | If unsure, you can explain what you were doing or what you expected to happen.
41 | validations:
42 | required: false
43 |
44 | - type: textarea
45 | id: screenshots
46 | attributes:
47 | label: Screenshots
48 | description: Attach any screenshots that might help clarify the issue (if applicable).
49 | placeholder: Upload or drag and drop images here.
50 | validations:
51 | required: false
52 |
53 | - type: dropdown
54 | id: assignee
55 | attributes:
56 | label: Would you like to work on this issue?
57 | options:
58 | - "Yes"
59 | - "No"
60 | validations:
61 | required: true
62 |
63 | - type: textarea
64 | id: implementation-plan
65 | attributes:
66 | label: Implementation Plan
67 | description: If you selected "Yes" above, please describe how you would approach fixing this issue (Optional).
68 | placeholder: Provide a brief plan or any initial thoughts on fixing the bug.
69 | validations:
70 | required: false
71 |
72 | - type: markdown
73 | attributes:
74 | value: |
75 | Thank you for reporting this bug! Please ensure you've filled out all the required sections to help us address the issue as efficiently as possible.
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/notification/Notifications.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.notification;
2 |
3 | import com.libraryman_api.member.Members;
4 | import jakarta.persistence.*;
5 |
6 | import java.sql.Timestamp;
7 |
8 | @Entity
9 | public class Notifications {
10 |
11 | @Id
12 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "notification_id_generator")
13 | @SequenceGenerator(name = "notification_id_generator", sequenceName = "notification_id_sequence", allocationSize = 1)
14 | @Column(name = "notification_id")
15 | private int notificationId;
16 |
17 | @ManyToOne
18 | @JoinColumn(name = "member_id", nullable = false)
19 | private Members member;
20 |
21 | @Column(nullable = false, length = 500)
22 | private String message;
23 |
24 | @Enumerated(EnumType.STRING)
25 | @Column(name = "notification_type", nullable = false)
26 | private NotificationType notificationType;
27 |
28 | @Column(name = "sent_date", nullable = false)
29 | private Timestamp sentDate;
30 |
31 | @Enumerated(EnumType.STRING)
32 | private NotificationStatus notificationStatus;
33 |
34 |
35 | public Notifications() {
36 | }
37 |
38 | public Notifications(Members member, String message, NotificationType notificationType, Timestamp sentDate, NotificationStatus notificationStatus) {
39 | this.member = member;
40 | this.message = message;
41 | this.notificationType = notificationType;
42 | this.sentDate = sentDate;
43 | this.notificationStatus = notificationStatus;
44 | }
45 |
46 | public int getNotificationId() {
47 | return notificationId;
48 | }
49 |
50 | public Members getMember() {
51 | return member;
52 | }
53 |
54 | public void setMember(Members member) {
55 | this.member = member;
56 | }
57 |
58 | public String getMessage() {
59 | return message;
60 | }
61 |
62 | public void setMessage(String message) {
63 | this.message = message;
64 | }
65 |
66 | public NotificationType getNotificationType() {
67 | return notificationType;
68 | }
69 |
70 | public void setNotificationType(NotificationType notificationType) {
71 | this.notificationType = notificationType;
72 | }
73 |
74 | public Timestamp getSentDate() {
75 | return sentDate;
76 | }
77 |
78 | public void setSentDate(Timestamp sentDate) {
79 | this.sentDate = sentDate;
80 | }
81 |
82 | public NotificationStatus getNotificationStatus() {
83 | return notificationStatus;
84 | }
85 |
86 | public void setNotificationStatus(NotificationStatus notificationStatus) {
87 | this.notificationStatus = notificationStatus;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/security/jwt/JwtAuthenticationFilter.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.security.jwt;
2 |
3 |
4 | import jakarta.servlet.FilterChain;
5 | import jakarta.servlet.ServletException;
6 | import jakarta.servlet.http.HttpServletRequest;
7 | import jakarta.servlet.http.HttpServletResponse;
8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
9 | import org.springframework.security.core.context.SecurityContextHolder;
10 | import org.springframework.security.core.userdetails.UserDetails;
11 | import org.springframework.security.core.userdetails.UserDetailsService;
12 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
13 | import org.springframework.stereotype.Component;
14 | import org.springframework.web.filter.OncePerRequestFilter;
15 |
16 | import java.io.IOException;
17 |
18 | @Component
19 | public class JwtAuthenticationFilter extends OncePerRequestFilter {
20 |
21 | private final JwtAuthenticationHelper jwtHelper;
22 |
23 | private final UserDetailsService userDetailsService;
24 |
25 | public JwtAuthenticationFilter(JwtAuthenticationHelper jwtHelper, UserDetailsService userDetailsService) {
26 | this.jwtHelper = jwtHelper;
27 | this.userDetailsService = userDetailsService;
28 | }
29 |
30 | @Override
31 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
32 | throws ServletException, IOException {
33 |
34 | String requestHeader = request.getHeader("Authorization");
35 | String username = null;
36 | String token = null;
37 | if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
38 | token = requestHeader.substring(7);
39 | username = jwtHelper.getUsernameFromToken(token);
40 | if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
41 | UserDetails userDetails = userDetailsService.loadUserByUsername(username);
42 | if (!(jwtHelper.isTokenExpired(token))) {
43 | UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
44 | usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
45 | SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
46 |
47 | } else {
48 | System.out.println("Token is expired or user details not found.");
49 | }
50 | }
51 |
52 | }
53 | filterChain.doFilter(request, response);
54 | }
55 |
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/test/java/com/libraryman_api/security/services/LoginServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.security.services;
2 |
3 | import com.libraryman_api.TestUtil;
4 | import com.libraryman_api.member.MemberRepository;
5 | import com.libraryman_api.member.Members;
6 | import com.libraryman_api.security.jwt.JwtAuthenticationHelper;
7 | import com.libraryman_api.security.model.LoginRequest;
8 | import com.libraryman_api.security.model.LoginResponse;
9 | import org.junit.jupiter.api.Nested;
10 | import org.junit.jupiter.api.Test;
11 | import org.junit.jupiter.api.extension.ExtendWith;
12 | import org.mockito.InjectMocks;
13 | import org.mockito.Mock;
14 | import org.mockito.junit.jupiter.MockitoExtension;
15 | import org.springframework.security.authentication.AuthenticationManager;
16 | import org.springframework.security.authentication.BadCredentialsException;
17 | import org.springframework.security.core.userdetails.UserDetailsService;
18 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
19 |
20 | import static org.junit.jupiter.api.Assertions.assertEquals;
21 | import static org.junit.jupiter.api.Assertions.assertThrows;
22 | import static org.mockito.Mockito.any;
23 | import static org.mockito.Mockito.when;
24 |
25 | /**
26 | * Tests the {@link LoginService}.
27 | */
28 | @ExtendWith(MockitoExtension.class)
29 | class LoginServiceTest {
30 | @Mock
31 | private AuthenticationManager authenticationManager;
32 | @Mock
33 | private UserDetailsService userDetailsService;
34 | @Mock
35 | private JwtAuthenticationHelper jwtHelper;
36 | @Mock
37 | private MemberRepository memberRepository;
38 | @InjectMocks
39 | private LoginService loginService;
40 |
41 | @Nested
42 | class Login {
43 | @Test
44 | void success() {
45 | LoginRequest loginRequest = TestUtil.getLoginRequest();
46 | String token = "jwtToken";
47 | Members members = TestUtil.getMembers();
48 |
49 | when(userDetailsService.loadUserByUsername(any())).thenReturn(members);
50 | when(jwtHelper.generateToken(members)).thenReturn(token);
51 |
52 | LoginResponse response = loginService.login(loginRequest);
53 |
54 | assertEquals(token, response.getToken());
55 | }
56 |
57 | @Test
58 | void badCredentials() {
59 | when(authenticationManager.authenticate(any())).thenThrow(BadCredentialsException.class);
60 |
61 | assertThrows(BadCredentialsException.class, () -> loginService.login(TestUtil.getLoginRequest()));
62 | }
63 |
64 | @Test
65 | void userNotFound() {
66 | when(userDetailsService.loadUserByUsername(any())).thenThrow(UsernameNotFoundException.class);
67 |
68 | assertThrows(UsernameNotFoundException.class, () -> loginService.login(TestUtil.getLoginRequest()));
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/test/java/com/libraryman_api/analytics/AnalyticsServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.analytics;
2 |
3 | import com.libraryman_api.book.BookRepository;
4 | import com.libraryman_api.borrowing.BorrowingRepository;
5 | import com.libraryman_api.member.MemberRepository;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.extension.ExtendWith;
8 | import org.mockito.InjectMocks;
9 | import org.mockito.Mock;
10 | import org.mockito.junit.jupiter.MockitoExtension;
11 |
12 | import java.time.LocalDate;
13 | import java.util.List;
14 | import java.util.Map;
15 |
16 | import static org.junit.jupiter.api.Assertions.assertEquals;
17 | import static org.mockito.ArgumentMatchers.any;
18 | import static org.mockito.Mockito.when;
19 |
20 | /**
21 | * Tests the {@link AnalyticsService} class.
22 | */
23 | @ExtendWith(MockitoExtension.class)
24 | class AnalyticsServiceTest {
25 | @Mock
26 | private BookRepository bookRepository;
27 | @Mock
28 | private BorrowingRepository borrowingRepository;
29 | @Mock
30 | private MemberRepository memberRepository;
31 | @InjectMocks
32 | private AnalyticsService analyticsService;
33 |
34 | @Test
35 | void getLibraryOverview() {
36 | when(bookRepository.count()).thenReturn(100L);
37 | when(borrowingRepository.count()).thenReturn(50L);
38 | when(memberRepository.count()).thenReturn(25L);
39 |
40 | Map overview = analyticsService.getLibraryOverview();
41 |
42 | assertEquals(100L, overview.get("totalBooks"));
43 | assertEquals(25L, overview.get("totalMembers"));
44 | assertEquals(50L, overview.get("totalBorrowings"));
45 | }
46 |
47 | @Test
48 | void getPopularBooks() {
49 | List> expectedList = List.of(Map.of("title", "Book A", "borrowCount", 10), Map.of("title", "Book B", "borrowCount", 8));
50 | when(borrowingRepository.findMostBorrowedBooks(2)).thenReturn(expectedList);
51 |
52 | List> result = analyticsService.getPopularBooks(2);
53 |
54 | assertEquals(expectedList, result);
55 | }
56 |
57 | @Test
58 | void getBorrowingTrends() {
59 | Map trends = Map.of("2025-06-01", 5L, "2025-06-02", 3L);
60 | when(borrowingRepository.getBorrowingTrendsBetweenDates(any(), any())).thenReturn(trends);
61 |
62 | Map result = analyticsService.getBorrowingTrends(LocalDate.now().minusDays(1), LocalDate.now());
63 |
64 | assertEquals(trends, result);
65 | }
66 |
67 | @Test
68 | void getMemberActivityReport() {
69 | List> report = List.of(Map.of("memberId", 1, "borrowCount", 10), Map.of("memberId", 2, "borrowCount", 5));
70 | when(memberRepository.getMemberActivityReport()).thenReturn(report);
71 |
72 | List> result = analyticsService.getMemberActivityReport();
73 |
74 | assertEquals(report, result);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request 💡
2 | description: Have a new idea or feature? Let us know...
3 | title: "[FEATURE] "
4 | labels: ["status: awaiting triage"]
5 |
6 | body:
7 | - type: checkboxes
8 | id: duplicates
9 | attributes:
10 | label: Is this feature already requested?
11 | description: Ensure this feature hasn't been suggested before.
12 | options:
13 | - label: I have checked "open" and "closed" issues, and this is not a duplicate.
14 | required: true
15 |
16 | - type: textarea
17 | id: problem
18 | attributes:
19 | label: Problem or Missing Functionality
20 | description: Briefly describe the problem or the missing functionality that this feature would address.
21 | placeholder: "For ex: I often encounter [specific problem] when using [current feature or lack thereof]. Implementing this feature would improve [specific aspect or workflow] by [explain how it will help]."
22 | validations:
23 | required: false
24 |
25 | - type: textarea
26 | id: solution
27 | attributes:
28 | label: Feature Description
29 | description: Describe the feature you're suggesting and how it would solve the problem. Include any relevant details or references.
30 | placeholder: "For ex: I suggest adding [describe the feature] that will [explain its impact]. This feature could be similar to [mention any known implementations or inspirations], which helps by [describe how it benefits the users]."
31 | validations:
32 | required: true
33 |
34 | - type: textarea
35 | id: screenshots
36 | attributes:
37 | label: Screenshots
38 | description: Attach any screenshots that might help illustrate the feature or its need (Optional).
39 | placeholder: "If applicable, drag and drop images here or click to upload. Screenshots can help clarify your suggestion by showing the current issue or how the feature might look."
40 | validations:
41 | required: false
42 |
43 | - type: dropdown
44 | id: work_on_issue
45 | attributes:
46 | label: Would you like to work on this feature?
47 | options:
48 | - "Yes"
49 | - "No"
50 | validations:
51 | required: true
52 |
53 | - type: textarea
54 | id: implementation_plan
55 | attributes:
56 | label: Implementation Plan
57 | description: If you selected "Yes" above, briefly describe how you plan to implement this feature (Optional).
58 | placeholder: "For example: I plan to start by [outline your steps], using [mention any tools, libraries, or frameworks]. This will help ensure the feature is developed efficiently and meets the intended purpose."
59 | validations:
60 | required: false
61 |
62 | - type: markdown
63 | attributes:
64 | value: |
65 | Thank you for suggesting a new feature! Please ensure you've filled out all the required sections to help us evaluate your suggestion effectively.
66 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/fine/Fine.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.fine;
2 |
3 | import jakarta.persistence.*;
4 |
5 | import java.math.BigDecimal;
6 |
7 | /**
8 | * Represents a fine in the Library Management System.
9 | * Each fine has an amount, a payment status, and a unique identifier.
10 | */
11 | @Entity
12 | public class Fine {
13 |
14 | @Id
15 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "fine_id_generator")
16 | @SequenceGenerator(name = "fine_id_generator", sequenceName = "fine_id_sequence", allocationSize = 1)
17 | @Column(updatable = false, nullable = false)
18 | private int fineId;
19 |
20 | /**
21 | * The amount of the fine with a precision of 10 and a scale of 2.
22 | * Precision = 10 means the total number of digits (including decimal places) is 10.
23 | * Scale = 2 means the number of decimal places is 2.
24 | */
25 | @Column(nullable = false, precision = 10, scale = 2)
26 | private BigDecimal amount;
27 |
28 | /**
29 | * Indicates whether the fine has been paid.
30 | */
31 | @Column(nullable = false)
32 | private boolean paid = false;
33 |
34 | // Default constructor for JPA
35 | public Fine() {
36 | }
37 |
38 | // Constructor with fields
39 | public Fine(BigDecimal amount, boolean paid) {
40 | this.amount = amount;
41 | this.paid = paid;
42 | }
43 |
44 | /**
45 | * Gets the unique ID of the fine.
46 | *
47 | * @return the unique fine ID
48 | */
49 | public int getFineId() {
50 | return fineId;
51 | }
52 |
53 | /**
54 | * Gets the amount of the fine.
55 | *
56 | * @return the amount of the fine
57 | */
58 | public BigDecimal getAmount() {
59 | return amount;
60 | }
61 |
62 | /**
63 | * Sets the amount of the fine.
64 | *
65 | * @param amount the amount to set
66 | */
67 | public void setAmount(BigDecimal amount) {
68 | this.amount = amount;
69 | }
70 |
71 | /**
72 | * Checks if the fine has been paid.
73 | *
74 | * @return true if the fine is paid, false otherwise
75 | */
76 | public boolean isPaid() {
77 | return paid;
78 | }
79 |
80 | /**
81 | * Sets the payment status of the fine.
82 | *
83 | * @param paid true if the fine is paid, false otherwise
84 | */
85 | public void setPaid(boolean paid) {
86 | this.paid = paid;
87 | }
88 |
89 | /**
90 | * Provides a string representation of the Fine object.
91 | *
92 | * @return a string containing the fine details
93 | */
94 | @Override
95 | public String toString() {
96 | return "Fine{" +
97 | "fineId=" + fineId +
98 | ", amount=" + amount +
99 | ", paid=" + paid +
100 | '}';
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/borrowing/Borrowings.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.borrowing;
2 |
3 | import com.libraryman_api.book.Book;
4 | import com.libraryman_api.fine.Fine;
5 | import com.libraryman_api.member.Members;
6 | import jakarta.persistence.*;
7 |
8 | import java.util.Date;
9 |
10 | @Entity
11 | public class Borrowings {
12 | @Id
13 | @GeneratedValue(strategy = GenerationType.SEQUENCE,
14 | generator = "borrowing_id_generator")
15 | @SequenceGenerator(name = "borrowing_id_generator",
16 | sequenceName = "borrowing_id_sequence",
17 | allocationSize = 1)
18 | @Column(name = "borrowing_id")
19 | private int borrowingId;
20 |
21 | @ManyToOne
22 | @JoinColumn(name = "book_id", nullable = false)
23 | private Book book;
24 |
25 | @OneToOne
26 | @JoinColumn(name = "fine_id")
27 | private Fine fine;
28 |
29 | @ManyToOne
30 | @JoinColumn(name = "member_id", nullable = false)
31 | private Members member;
32 |
33 | @Column(name = "borrow_date", nullable = false)
34 | private Date borrowDate;
35 |
36 | @Column(name = "due_date", nullable = false)
37 | private Date dueDate;
38 |
39 | @Column(name = "return_date")
40 | private Date returnDate;
41 |
42 |
43 | public Borrowings() {
44 | }
45 |
46 | public Borrowings(Book book, Members member, Date borrowDate, Date dueDate, Date returnDate) {
47 | this.book = book;
48 | this.member = member;
49 | this.borrowDate = borrowDate;
50 | this.dueDate = dueDate;
51 | this.returnDate = returnDate;
52 | }
53 |
54 | public Fine getFine() {
55 | return fine;
56 | }
57 |
58 | public void setFine(Fine fine) {
59 | this.fine = fine;
60 | }
61 |
62 | public int getBorrowingId() {
63 | return borrowingId;
64 | }
65 |
66 | public void setBorrowingId(int borrowingId) {
67 | this.borrowingId = borrowingId;
68 | }
69 |
70 | public Book getBook() {
71 | return book;
72 | }
73 |
74 | public void setBook(Book book) {
75 | this.book = book;
76 | }
77 |
78 | public Members getMember() {
79 | return member;
80 | }
81 |
82 | public void setMember(Members member) {
83 | this.member = member;
84 | }
85 |
86 | public Date getBorrowDate() {
87 | return borrowDate;
88 | }
89 |
90 | public void setBorrowDate(Date borrowDate) {
91 | this.borrowDate = borrowDate;
92 | }
93 |
94 | public Date getDueDate() {
95 | return dueDate;
96 | }
97 |
98 | public void setDueDate(Date dueDate) {
99 | this.dueDate = dueDate;
100 | }
101 |
102 | public Date getReturnDate() {
103 | return returnDate;
104 | }
105 |
106 | public void setReturnDate(Date returnDate) {
107 | this.returnDate = returnDate;
108 | }
109 | }
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/borrowing/BorrowingsDto.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.borrowing;
2 |
3 | import com.libraryman_api.book.BookDto;
4 | import com.libraryman_api.fine.Fine;
5 | import com.libraryman_api.member.dto.MembersDto;
6 | import jakarta.validation.constraints.NotNull;
7 |
8 | import java.util.Date;
9 |
10 | public class BorrowingsDto {
11 |
12 | private int borrowingId;
13 | @NotNull(message = "Book is required")
14 | private BookDto book;
15 | private Fine fine;
16 | @NotNull(message = "Member is required")
17 | private MembersDto member;
18 |
19 | private Date borrowDate;
20 | private Date dueDate;
21 | private Date returnDate;
22 |
23 |
24 | public BorrowingsDto(int borrowingId, BookDto book, Fine fine, MembersDto member, Date borrowDate, Date dueDate, Date returnDate) {
25 | this.borrowingId = borrowingId;
26 | this.book = book;
27 | this.fine = fine;
28 | this.member = member;
29 | this.borrowDate = borrowDate;
30 | this.dueDate = dueDate;
31 | this.returnDate = returnDate;
32 | }
33 |
34 | public BorrowingsDto() {
35 | }
36 |
37 | public int getBorrowingId() {
38 | return borrowingId;
39 | }
40 |
41 | public void setBorrowingId(int borrowingId) {
42 | this.borrowingId = borrowingId;
43 | }
44 |
45 | public BookDto getBook() {
46 | return book;
47 | }
48 |
49 | public void setBook(BookDto book) {
50 | this.book = book;
51 | }
52 |
53 | public Fine getFine() {
54 | return fine;
55 | }
56 |
57 | public void setFine(Fine fine) {
58 | this.fine = fine;
59 | }
60 |
61 | public MembersDto getMember() {
62 | return member;
63 | }
64 |
65 | public void setMember(MembersDto member) {
66 | this.member = member;
67 | }
68 |
69 | public Date getBorrowDate() {
70 | return borrowDate;
71 | }
72 |
73 | public void setBorrowDate(Date borrowDate) {
74 | this.borrowDate = borrowDate;
75 | }
76 |
77 | public Date getDueDate() {
78 | return dueDate;
79 | }
80 |
81 | public void setDueDate(Date dueDate) {
82 | this.dueDate = dueDate;
83 | }
84 |
85 | public Date getReturnDate() {
86 | return returnDate;
87 | }
88 |
89 | public void setReturnDate(Date returnDate) {
90 | this.returnDate = returnDate;
91 | }
92 |
93 | @Override
94 | public String toString() {
95 | return "BorrowingsDto{" +
96 | "borrowingId=" + borrowingId +
97 | ", book=" + book +
98 | ", fine=" + fine +
99 | ", member=" + member +
100 | ", borrowDate=" + borrowDate +
101 | ", dueDate=" + dueDate +
102 | ", returnDate=" + returnDate +
103 | '}';
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/newsletter/NewsletterController.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.newsletter;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.http.ResponseEntity;
5 | import org.springframework.web.bind.annotation.*;
6 |
7 | @RestController
8 | @RequestMapping("/api/newsletter")
9 | public class NewsletterController {
10 |
11 | private final NewsletterService newsletterService;
12 |
13 | public NewsletterController(NewsletterService newsletterService) {
14 | this.newsletterService = newsletterService;
15 | }
16 |
17 | // Subscribe Endpoint
18 | @PostMapping("/subscribe")
19 | public ResponseEntity subscribe(@RequestParam String email) {
20 | try {
21 | String result = newsletterService.subscribe(email);
22 |
23 | return switch (result) {
24 | case "Invalid email format." ->
25 | ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result); // 400 Bad Request
26 |
27 | case "Email is already subscribed." ->
28 | ResponseEntity.status(HttpStatus.CONFLICT).body(result); // 409 Conflict
29 |
30 | case "You have successfully subscribed!" ->
31 | ResponseEntity.status(HttpStatus.CREATED).body(result); // 201 Created
32 |
33 | case "You have successfully re-subscribed!" ->
34 | ResponseEntity.status(HttpStatus.OK).body(result); // 200 OK
35 |
36 | default -> ResponseEntity.status(HttpStatus.OK).body(result); // Default 200 OK
37 | };
38 | } catch (Exception e) {
39 | // Handle unexpected errors
40 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
41 | .body("An error occurred while processing your subscription.");
42 | }
43 | }
44 |
45 | // Unsubscribe Endpoint
46 | @GetMapping("/unsubscribe")
47 | public ResponseEntity unsubscribe(@RequestParam String token) {
48 | try {
49 | String result = newsletterService.unsubscribe(token);
50 |
51 | return switch (result) {
52 | case "Invalid or expired token." ->
53 | ResponseEntity.status(HttpStatus.NOT_FOUND).body(result); // 404 Not Found
54 |
55 | case "You are already unsubscribed." ->
56 | ResponseEntity.status(HttpStatus.CONFLICT).body(result); // 409 Conflict
57 |
58 | case "You have successfully unsubscribed!" ->
59 | ResponseEntity.status(HttpStatus.OK).body(result); // 200 OK
60 |
61 | default -> ResponseEntity.status(HttpStatus.OK).body(result); // Default 200 OK
62 | };
63 | } catch (Exception e) {
64 | // Handle unexpected errors
65 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
66 | .body("An error occurred while processing your unsubscription.");
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/member/Members.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.member;
2 |
3 | import jakarta.persistence.*;
4 | import org.springframework.security.core.GrantedAuthority;
5 | import org.springframework.security.core.authority.SimpleGrantedAuthority;
6 | import org.springframework.security.core.userdetails.UserDetails;
7 |
8 | import java.util.Collection;
9 | import java.util.Collections;
10 | import java.util.Date;
11 |
12 |
13 | @Entity
14 | public class Members implements UserDetails {
15 |
16 | @Id
17 | @GeneratedValue(strategy = GenerationType.SEQUENCE,
18 | generator = "member_id_generator")
19 | @SequenceGenerator(name = "member_id_generator",
20 | sequenceName = "member_id_sequence",
21 | allocationSize = 1)
22 | @Column(name = "member_id")
23 | private int memberId;
24 |
25 | @Column(nullable = false)
26 | private String name;
27 |
28 | @Column(name = "username")
29 | private String username;
30 |
31 | @Column(unique = true, nullable = false)
32 | private String email;
33 |
34 | @Column(nullable = false)
35 | private String password;
36 |
37 | @Enumerated(EnumType.STRING)
38 | @Column(nullable = false)
39 | private Role role;
40 |
41 | @Column(name = "membership_date")
42 | private Date membershipDate;
43 |
44 |
45 | public Members() {
46 | }
47 |
48 | public Members(String name, String email, String password, Role role, Date membershipDate) {
49 | this.name = name;
50 | this.email = email;
51 | this.password = password;
52 | this.role = role;
53 | this.membershipDate = membershipDate;
54 | }
55 |
56 | public int getMemberId() {
57 | return memberId;
58 | }
59 |
60 | public void setMemberId(int memberId) {
61 | this.memberId = memberId;
62 | }
63 |
64 | public String getName() {
65 | return name;
66 | }
67 |
68 | public void setName(String name) {
69 | this.name = name;
70 | }
71 |
72 | public String getEmail() {
73 | return email;
74 | }
75 |
76 | public void setEmail(String email) {
77 | this.email = email;
78 | }
79 |
80 | public String getPassword() {
81 | return password;
82 | }
83 |
84 | public void setPassword(String password) {
85 | this.password = password;
86 | }
87 |
88 | public Role getRole() {
89 | return role;
90 | }
91 |
92 | public void setRole(Role role) {
93 | this.role = role;
94 | }
95 |
96 | public Date getMembershipDate() {
97 | return membershipDate;
98 | }
99 |
100 | public void setMembershipDate(Date membershipDate) {
101 | this.membershipDate = membershipDate;
102 | }
103 |
104 | public String getUsername() {
105 | return username;
106 | }
107 |
108 | public void setUsername(String username) {
109 | this.username = username;
110 | }
111 |
112 | @Override
113 | public Collection extends GrantedAuthority> getAuthorities() {
114 | // TODO Auto-generated method stub
115 | return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role.name()));
116 | }
117 |
118 | }
119 |
--------------------------------------------------------------------------------
/src/test/java/com/libraryman_api/security/jwt/JwtAuthenticationHelperTest.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.security.jwt;
2 |
3 | import com.libraryman_api.TestUtil;
4 | import io.jsonwebtoken.*;
5 | import io.jsonwebtoken.security.Keys;
6 | import io.jsonwebtoken.security.SignatureException;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.Nested;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.extension.ExtendWith;
11 | import org.mockito.junit.jupiter.MockitoExtension;
12 | import org.springframework.security.core.userdetails.UserDetails;
13 |
14 | import java.util.Date;
15 |
16 | import static org.junit.jupiter.api.Assertions.*;
17 |
18 | /**
19 | * Tests the {@link JwtAuthenticationHelper}.
20 | */
21 | @ExtendWith(MockitoExtension.class)
22 | class JwtAuthenticationHelperTest {
23 | private JwtAuthenticationHelper jwtAuthenticationHelper;
24 | private String secret;
25 |
26 | @BeforeEach
27 | void setup() {
28 | secret = "aVeryLongSecretStringThatIsAtLeast64BytesLongAndSecureEnoughForHS512";
29 | jwtAuthenticationHelper = new JwtAuthenticationHelper(secret);
30 | }
31 |
32 | @Nested
33 | class GetUsernameFromToken {
34 | @Test
35 | void success() {
36 | String expectedUsername = "User";
37 | String token = Jwts.builder().signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256).setSubject(expectedUsername).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + 100000)).compact();
38 |
39 | String actualUsername = jwtAuthenticationHelper.getUsernameFromToken(token);
40 |
41 | assertEquals(expectedUsername, actualUsername);
42 | }
43 |
44 | @Test
45 | void expired() {
46 | Date date = new Date();
47 | String token = Jwts.builder().signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256).setSubject("User").setIssuedAt(date).setExpiration(date).compact();
48 |
49 | assertThrows(ExpiredJwtException.class, () -> jwtAuthenticationHelper.getUsernameFromToken(token));
50 | }
51 |
52 | @Test
53 | void malformed() {
54 | assertThrows(MalformedJwtException.class, () -> jwtAuthenticationHelper.getUsernameFromToken("malformed.token"));
55 | }
56 |
57 | @Test
58 | void signatureException() {
59 | String differentSecret = "notTheSameSecretAsTheSecretValueInTheHelper";
60 | String token = Jwts.builder().signWith(Keys.hmacShaKeyFor(differentSecret.getBytes()), SignatureAlgorithm.HS256).setSubject("User").setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + 100000)).compact();
61 |
62 | assertThrows(SignatureException.class, () -> jwtAuthenticationHelper.getUsernameFromToken(token));
63 | }
64 | }
65 |
66 | @Test
67 | void generateToken() {
68 | UserDetails userDetails = TestUtil.getMembers();
69 |
70 | String token = jwtAuthenticationHelper.generateToken(userDetails);
71 |
72 | Claims claims = jwtAuthenticationHelper.getClaimsFromToken(token);
73 | assertEquals(userDetails.getUsername(), claims.getSubject());
74 | assertNotNull(claims.getIssuedAt());
75 | assertNotNull(claims.getExpiration());
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/newsletter/NewsletterService.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.newsletter;
2 |
3 | import com.libraryman_api.email.EmailService;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.stereotype.Service;
6 |
7 | import java.util.Optional;
8 | import java.util.regex.Pattern;
9 |
10 | @Service
11 | public class NewsletterService {
12 |
13 | private final NewsletterSubscriberRepository subscriberRepository;
14 | private final EmailService emailService;
15 |
16 | @Autowired
17 | public NewsletterService(NewsletterSubscriberRepository subscriberRepository, EmailService emailService) {
18 | this.subscriberRepository = subscriberRepository;
19 | this.emailService = emailService;
20 | }
21 |
22 | public String subscribe(String email) {
23 | if (!isValidEmail(email)) return "Invalid email format.";
24 |
25 | Optional optionalSubscriber = subscriberRepository.findByEmail(email);
26 | if (optionalSubscriber.isPresent()) {
27 | NewsletterSubscriber subscriber = optionalSubscriber.get();
28 | if (!subscriber.isActive()) {
29 | subscriber.setActive(true);
30 | subscriber.regenerateToken();
31 | subscriberRepository.save(subscriber);
32 | sendSubscriptionEmail(email, subscriber.getUnsubscribeToken());
33 | return "You have successfully re-subscribed!";
34 | }
35 | return "Email is already subscribed.";
36 | }
37 |
38 | NewsletterSubscriber newSubscriber = new NewsletterSubscriber(email);
39 | subscriberRepository.save(newSubscriber);
40 | sendSubscriptionEmail(email, newSubscriber.getUnsubscribeToken());
41 | return "You have successfully subscribed!";
42 | }
43 |
44 | public String unsubscribe(String token) {
45 | Optional optionalSubscriber = subscriberRepository.findByUnsubscribeToken(token);
46 | if (optionalSubscriber.isEmpty()) return "Invalid or expired token.";
47 |
48 | NewsletterSubscriber subscriber = optionalSubscriber.get();
49 | if (!subscriber.isActive()) return "You are already unsubscribed.";
50 |
51 | subscriber.setActive(false);
52 | subscriberRepository.save(subscriber);
53 | sendUnsubscribeEmail(subscriber.getEmail());
54 | return "You have successfully unsubscribed!";
55 | }
56 |
57 | private boolean isValidEmail(String email) {
58 | String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
59 | return Pattern.compile(emailRegex).matcher(email).matches();
60 | }
61 |
62 | private void sendSubscriptionEmail(String email, String token) {
63 | String subject = "Welcome to Our Newsletter!";
64 | String body = "Thank you for subscribing! " +
65 | "To unsubscribe, click the link:\n" +
66 | "http://localhost:8080/api/newsletter/unsubscribe?token=" + token;
67 |
68 | emailService.sendEmail(email, body, subject); // No need to change this line
69 | }
70 |
71 | private void sendUnsubscribeEmail(String email) {
72 | String subject = "You have been unsubscribed";
73 | String body = "You have successfully unsubscribed. If this was a mistake, you can re-subscribe.";
74 |
75 | emailService.sendEmail(email, body, subject); // No need to change this line
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/book/Book.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.book;
2 |
3 | import jakarta.persistence.*;
4 |
5 |
6 | @Entity
7 | public class Book {
8 | @Id
9 |
10 | @GeneratedValue(strategy = GenerationType.SEQUENCE,
11 | generator = "book_id_generator")
12 | @SequenceGenerator(name = "book_id_generator",
13 | sequenceName = "book_id_sequence",
14 | allocationSize = 1)
15 | @Column(name = "book_id")
16 | private int bookId;
17 |
18 | @Column(nullable = false)
19 | private String title;
20 |
21 | private String author;
22 |
23 | @Column(unique = true, nullable = false)
24 | private String isbn;
25 |
26 | private String publisher;
27 |
28 | @Column(name = "published_year")
29 | private int publishedYear;
30 | private String genre;
31 |
32 |
33 | @Column(name = "copies_available", nullable = false)
34 | private int copiesAvailable;
35 |
36 | public Book() {
37 | }
38 |
39 | public Book(String title, String author, String isbn, String publisher, int publishedYear, String genre, int copiesAvailable) {
40 | this.title = title;
41 | this.author = author;
42 | this.isbn = isbn;
43 | this.publisher = publisher;
44 | this.publishedYear = publishedYear;
45 | this.genre = genre;
46 | this.copiesAvailable = copiesAvailable;
47 | }
48 |
49 | public int getBookId() {
50 | return bookId;
51 | }
52 |
53 | public void setBookId(int bookId) {
54 | this.bookId = bookId;
55 | }
56 |
57 | public String getTitle() {
58 | return title;
59 | }
60 |
61 | public void setTitle(String title) {
62 | this.title = title;
63 | }
64 |
65 | public String getAuthor() {
66 | return author;
67 | }
68 |
69 | public void setAuthor(String author) {
70 | this.author = author;
71 | }
72 |
73 | public String getIsbn() {
74 | return isbn;
75 | }
76 |
77 | public void setIsbn(String isbn) {
78 | this.isbn = isbn;
79 | }
80 |
81 | public String getPublisher() {
82 | return publisher;
83 | }
84 |
85 | public void setPublisher(String publisher) {
86 | this.publisher = publisher;
87 | }
88 |
89 | public int getPublishedYear() {
90 | return publishedYear;
91 | }
92 |
93 | public void setPublishedYear(int publishedYear) {
94 | this.publishedYear = publishedYear;
95 | }
96 |
97 | public String getGenre() {
98 | return genre;
99 | }
100 |
101 | public void setGenre(String genre) {
102 | this.genre = genre;
103 | }
104 |
105 | public int getCopiesAvailable() {
106 | return copiesAvailable;
107 | }
108 |
109 | public void setCopiesAvailable(int copiesAvailable) {
110 | this.copiesAvailable = copiesAvailable;
111 | }
112 |
113 | @Override
114 | public String toString() {
115 | return "Books{" +
116 | "bookId=" + bookId +
117 | ", title='" + title + '\'' +
118 | ", author='" + author + '\'' +
119 | ", isbn='" + isbn + '\'' +
120 | ", publisher='" + publisher + '\'' +
121 | ", publishedYear=" + publishedYear +
122 | ", genre='" + genre + '\'' +
123 | ", copiesAvailable=" + copiesAvailable +
124 | '}';
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/member/dto/MembersDto.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.member.dto;
2 |
3 | import com.libraryman_api.member.Role;
4 | import jakarta.validation.constraints.NotBlank;
5 | import jakarta.validation.constraints.Pattern;
6 | import jakarta.validation.constraints.Size;
7 |
8 | import java.util.Date;
9 |
10 | public class MembersDto {
11 |
12 | private int memberId;
13 | @NotBlank(message = "Name is required")
14 | @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
15 | private String name;
16 | @NotBlank(message = "Username is required")
17 | @Size(min = 4, max = 50, message = "Username must be between 4 and 50 characters")
18 | private String username;
19 |
20 | @Pattern(regexp = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$", message = "Please enter a valid email address (e.g., user@example.com)")
21 | @NotBlank(message = "Email field cannot be empty. Please provide a valid email address.")
22 | private String email;
23 |
24 | @NotBlank(message = "Password is required")
25 | @Size(min = 8, message = "Password must be at least 8 characters long")
26 | @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@#$%^&+=]).*$",
27 | message = "Password must contain at least one letter, one number, and one special character")
28 | private String password;
29 |
30 |
31 | private Role role;
32 |
33 | private Date membershipDate;
34 |
35 | public MembersDto(int memberId, String name, String username, String email, String password, Role role, Date membershipDate) {
36 | this.memberId = memberId;
37 | this.name = name;
38 | this.username = username;
39 | this.email = email;
40 | this.password = password;
41 | this.role = role;
42 | this.membershipDate = membershipDate;
43 | }
44 |
45 | public MembersDto() {
46 | }
47 |
48 | public int getMemberId() {
49 | return memberId;
50 | }
51 |
52 | public void setMemberId(int memberId) {
53 | this.memberId = memberId;
54 | }
55 |
56 | public String getName() {
57 | return name;
58 | }
59 |
60 | public void setName(String name) {
61 | this.name = name;
62 | }
63 |
64 | public String getUsername() {
65 | return username;
66 | }
67 |
68 | public void setUsername(String username) {
69 | this.username = username;
70 | }
71 |
72 | public String getEmail() {
73 | return email;
74 | }
75 |
76 | public void setEmail(String email) {
77 | this.email = email;
78 | }
79 |
80 | public String getPassword() {
81 | return password;
82 | }
83 |
84 | public void setPassword(String password) {
85 | this.password = password;
86 | }
87 |
88 | public Role getRole() {
89 | return role;
90 | }
91 |
92 | public void setRole(Role role) {
93 | this.role = role;
94 | }
95 |
96 | public Date getMembershipDate() {
97 | return membershipDate;
98 | }
99 |
100 | public void setMembershipDate(Date membershipDate) {
101 | this.membershipDate = membershipDate;
102 | }
103 |
104 | @Override
105 | public String toString() {
106 | return "MembersDto{" +
107 | "memberId=" + memberId +
108 | ", name='" + name + '\'' +
109 | ", username='" + username + '\'' +
110 | ", email='" + email + '\'' +
111 | ", password='" + password + '\'' +
112 | ", role=" + role +
113 | ", membershipDate=" + membershipDate +
114 | '}';
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/notification-docs/Account Details Updated 🔄.eml:
--------------------------------------------------------------------------------
1 | Date: Thu, 29 Aug 2024 22:51:04 +0530 (IST)
2 | From: hello@libraryman.com
3 | To: ajay@ajaynegi.co
4 | Message-ID: <1312357003.15.1724952064894@localhost>
5 | Subject: =?utf-8?Q?Account_Details_Updated_=F0=9F=94=84?=
6 | MIME-Version: 1.0
7 | Content-Type: text/html;charset=utf-8
8 | Content-Transfer-Encoding: quoted-printable
9 |
10 |
12 |
13 |
14 |
15 |
18 |
19 |
20 | =20
21 |
24 |
25 |
26 |
28 |
29 |
30 | =20
31 |
32 |
34 | Account Details Updated =F0=9F=94=84
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | =20
45 |
46 |
47 |
48 |
51 |
52 |
53 |
54 | =20
55 |
57 |
58 |
=
59 | td>
60 |
61 |
62 | =20
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 |
78 |
80 | =20
81 |
Hi Ajay Negi,
Your account details have been successfull=
84 | y updated as per your request. If you did not authorize this change or if y=
85 | ou notice any discrepancies, please contact us immediately.
Thank yo=
86 | u for keeping your account information up to date.
Thank you for your payment. We=E2=80=99ve =
84 | received your payment of =E2=82=B990.00 towards the fine for the overdue re=
85 | turn of 'The Great Gatsby' book. =E2=9C=85
Your account has been upd=
86 | ated accordingly. If you have any questions or need further assistance, ple=
87 | ase feel free to reach out.
88 |
We=E2=80=99re excited to welcome you to Li=
84 | braryMan! Your account has been successfully created, and you=E2=80=99re no=
85 | w part of our community of book lovers. =F0=9F=93=9A
Feel free to ex=
86 | plore our vast collection of books and other resources. If you have any que=
87 | stions or need assistance, our team is here to help.
34 | Reminder: Due date approaching =E2=8F=B0
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | =20
45 |
46 |
47 |
48 |
51 |
52 |
53 |
54 | =20
55 |
57 |
58 |
=
59 | td>
60 |
61 |
62 | =20
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
73 |
74 |
75 |
76 |
77 |
78 |
80 | =20
81 |
Hi Ajay Negi,
This is a friendly reminder that the due d=
84 | ate to return 'Head First Java' book is approaching. Please ensure that you=
85 | return the book by Thu Aug 29 22:54:20 IST 2024 to avoid any late fees. =
86 | =F0=9F=93=85
If you need more time, consider renewing your book thro=
87 | ugh our online portal or by contacting us.
Thank you, and happy read=
88 | ing! =F0=9F=98=8A
Best regards,
LibraryMan
=20
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/email/EmailService.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.email;
2 |
3 | import com.libraryman_api.notification.NotificationRepository;
4 | import com.libraryman_api.notification.NotificationStatus;
5 | import com.libraryman_api.notification.Notifications;
6 | import jakarta.mail.MessagingException;
7 | import jakarta.mail.internet.MimeMessage;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import org.springframework.beans.factory.annotation.Value;
11 | import org.springframework.mail.javamail.JavaMailSender;
12 | import org.springframework.mail.javamail.MimeMessageHelper;
13 | import org.springframework.scheduling.annotation.Async;
14 | import org.springframework.stereotype.Service;
15 |
16 | /**
17 | * Unified service class for sending emails asynchronously.
18 | * Handles both general email sending and notifications.
19 | */
20 | @Service
21 | public class EmailService implements EmailSender {
22 |
23 | private static final Logger LOGGER = LoggerFactory.getLogger(EmailService.class);
24 |
25 | private final NotificationRepository notificationRepository;
26 | private final JavaMailSender mailSender;
27 |
28 | @Value("${spring.mail.properties.domain_name}") // Domain name from application properties
29 | private String domainName;
30 |
31 | public EmailService(NotificationRepository notificationRepository, JavaMailSender mailSender) {
32 | this.notificationRepository = notificationRepository;
33 | this.mailSender = mailSender;
34 | }
35 |
36 | /**
37 | * Sends a general email asynchronously.
38 | *
39 | * @param to recipient's email
40 | * @param body email content (HTML supported)
41 | * @param subject subject of the email
42 | */
43 | @Async
44 | public void sendEmail(String to, String body, String subject) {
45 | sendEmail(to, body, subject, null); // Default 'from' to null
46 | }
47 |
48 | /**
49 | * Sends a general email asynchronously.
50 | *
51 | * @param to recipient's email
52 | * @param body email content (HTML supported)
53 | * @param subject subject of the email
54 | * @param from sender's email address (overrides default if provided)
55 | */
56 | @Async
57 | public void sendEmail(String to, String body, String subject, String from) {
58 | try {
59 | MimeMessage mimeMessage = mailSender.createMimeMessage();
60 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "utf-8");
61 |
62 | helper.setText(body, true); // true = enable HTML content
63 | helper.setTo(to);
64 | helper.setSubject(subject);
65 | helper.setFrom(from != null ? from : domainName); // Use provided sender or default domain
66 |
67 | mailSender.send(mimeMessage);
68 | } catch (MessagingException e) {
69 | LOGGER.error("Failed to send email", e);
70 | throw new IllegalStateException("Failed to send email", e);
71 | }
72 | }
73 |
74 | /**
75 | * Sends a notification email and updates notification status.
76 | *
77 | * @param to recipient's email
78 | * @param email email content
79 | * @param subject subject of the email
80 | * @param notification notification entity to update status
81 | */
82 | @Override
83 | @Async
84 | public void send(String to, String email, String subject, Notifications notification) {
85 | try {
86 | sendEmail(to, email, subject); // Reuse sendEmail method for notifications
87 |
88 | // Update notification status to SENT
89 | notification.setNotificationStatus(NotificationStatus.SENT);
90 | notificationRepository.save(notification);
91 | } catch (Exception e) {
92 | LOGGER.error("Failed to send notification email", e);
93 |
94 | // Update notification status to FAILED
95 | notification.setNotificationStatus(NotificationStatus.FAILED);
96 | notificationRepository.save(notification);
97 |
98 | throw new IllegalStateException("Failed to send notification email", e);
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/notification-docs/Account Deletion Confirmation 🗑️.eml:
--------------------------------------------------------------------------------
1 | Date: Thu, 29 Aug 2024 22:52:24 +0530 (IST)
2 | From: hello@libraryman.com
3 | To: ajay@ajaynegi.co
4 | Message-ID: <1092009655.16.1724952144870@localhost>
5 | Subject: =?utf-8?Q?Account_Deletion_Confirmation_=F0=9F=97=91=EF=B8=8F?=
6 | MIME-Version: 1.0
7 | Content-Type: text/html;charset=utf-8
8 | Content-Transfer-Encoding: quoted-printable
9 |
10 |
We=E2=80=99re sorry to see you go! Your ac=
85 | count with LibraryMan has been successfully deleted as per your request.
If you change your mind in the future, you=E2=80=99re always welcome t=
87 | o create a new account with us. Should you have any questions or concerns, =
88 | please don=E2=80=99t hesitate to reach out.
Thank you for being a pa=
89 | rt of our community.
Congratulations! =F0=9F=8E=89 You have suc=
84 | cessfully borrowed 'Head First Java' book on Thu Aug 29 22:54:20 IST 2024.<=
85 | br> You now have 15 days to enjoy reading it. We kindly request that you=
86 | return it to us on or before Fri Sep 13 22:54:20 IST 2024 to avoid any lat=
87 | e fees =F0=9F=93=86, which are =E2=82=B910 per day for late returns.
If you need to renew the book or have any questions, please don't hesitate=
89 | to reach out to us.
We hope you enjoyed reading 'The Great Gat=
84 | sby' book. Unfortunately, our records show that the book was returned after=
85 | the due date of 2024-08-20 22:54:20.537 . As a result, a fine of =E2=82=B9=
86 | 10 per day has been imposed for the late return.
The total fine amou=
87 | nt for this overdue return is =E2=82=B990.
If you have any questions=
88 | or would like to discuss this matter further, please don't hesitate to con=
89 | tact us.
Thank you for your understanding and for being a valued mem=
90 | ber of our library.
Best regards,
LibraryMan
=20
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/main/java/com/libraryman_api/security/config/WebConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.libraryman_api.security.config;
2 |
3 | import com.libraryman_api.security.jwt.JwtAuthenticationFilter;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.security.authentication.AuthenticationManager;
7 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
8 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
11 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
12 | import org.springframework.security.web.SecurityFilterChain;
13 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
14 | import org.springframework.web.cors.CorsConfiguration;
15 | import org.springframework.web.cors.CorsConfigurationSource;
16 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
17 | import org.springframework.web.filter.CorsFilter;
18 |
19 | import static org.springframework.security.config.Customizer.withDefaults;
20 | import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
21 |
22 | @Configuration
23 | @EnableWebSecurity(debug = true)
24 | // Do not use (debug=true) in a production system! as this contain sensitive information.
25 | @EnableMethodSecurity(prePostEnabled = true)
26 | public class WebConfiguration {
27 |
28 | private final JwtAuthenticationFilter jwtFilter;
29 |
30 | public WebConfiguration(JwtAuthenticationFilter jwtFilter) {
31 | this.jwtFilter = jwtFilter;
32 | }
33 |
34 | @Bean
35 | public SecurityFilterChain web(HttpSecurity http) throws Exception {
36 | http
37 | .csrf(AbstractHttpConfigurer::disable)
38 | .authorizeHttpRequests((request) -> request
39 | // make sure it is in order to access the proper Url
40 | .requestMatchers("/api/signup").permitAll()
41 | .requestMatchers("/api/login").permitAll()
42 | .requestMatchers("/api/logout").permitAll()
43 | .requestMatchers("/api/get-all-books/**").permitAll()
44 | .requestMatchers("/api/book/search**").permitAll()
45 | .requestMatchers("/api/analytics/**").hasAnyRole("ADMIN", "LIBRARIAN") // New line for analytics
46 | .anyRequest().authenticated()
47 |
48 | )
49 | .logout(logout -> logout
50 | .deleteCookies("LibraryManCookie"))
51 | .sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
52 | .formLogin(withDefaults());
53 |
54 | http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
55 | .httpBasic(httpBasic -> {
56 | });
57 |
58 | http.oauth2Login(withDefaults());
59 | return http.build();
60 | }
61 |
62 | @Bean
63 | public AuthenticationManager authenticationManager(AuthenticationConfiguration builder) throws Exception {
64 | return builder.getAuthenticationManager();
65 | }
66 |
67 | @Bean
68 | public CorsConfigurationSource corsConfigurationSource() {
69 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
70 | CorsConfiguration corsConfiguration = new CorsConfiguration();
71 | corsConfiguration.setAllowCredentials(true);
72 | corsConfiguration.addAllowedOriginPattern("*");
73 | corsConfiguration.addAllowedHeader("Authorization");
74 | corsConfiguration.addAllowedHeader("Content-Type");
75 | corsConfiguration.addAllowedHeader("Accept");
76 | corsConfiguration.addAllowedMethod("POST");
77 | corsConfiguration.addAllowedMethod("PUT");
78 | corsConfiguration.addAllowedMethod("GET");
79 | corsConfiguration.addAllowedMethod("DELETE");
80 | corsConfiguration.addAllowedMethod("OPTIONS");
81 | corsConfiguration.setMaxAge(3600L);
82 |
83 | source.registerCorsConfiguration("/**", corsConfiguration);
84 | return source;
85 | }
86 |
87 | @Bean
88 | public CorsFilter corsFilter() {
89 | return new CorsFilter(corsConfigurationSource());
90 | }
91 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LibraryMan: Library Management Simplified 📚
2 |
3 | LibraryMan is a user-friendly software solution for schools, companies, and libraries to efficiently manage book collections, track borrowing, and monitor due dates. It streamlines operations, reduces errors, and enhances the lending experience, making it easy to maintain a well-organized library.
4 |
5 | #### Checkout [Frontend](https://github.com/ajaynegi45/LibraryMan) Repository
6 |
7 | ## Project Structure 📂
8 | Checkout [Project Structure](https://github.com/ajaynegi45/LibraryMan-API/tree/main/project-structure) Diagram
9 |
10 | ## API Endpoints 🔗
11 |
12 | #### Learn More
13 | Want to know more about our API endpoints? Check out our [API Documentation](https://github.com/ajaynegi45/LibraryMan-API/tree/main/api-docs) for detailed information.
14 |
15 | #### Test Endpoints
16 | Ready to try out our API endpoints? Use [Postman Documentation](https://documenter.getpostman.com/view/28691426/2sAXjJ6D7L) to test and explore our APIs.
17 |
18 | ## How to Run the Project 💨
19 |
20 | 1. Ensure you have Java and MySQL installed on your system.
21 | 2. Clone or download the project from the repository.
22 | 3. Import the project into your preferred IDE (e.g., Eclipse, IntelliJ).
23 | 4. Set up the MySQL database and update the database configurations in the `application-development.properties` file.
24 | 5. Build and run the project using the IDE or by running `mvn spring-boot:run` command from the project root directory.
25 |
26 | ## ‼️ Important Note ‼️
27 |
28 | - You need to set up the database and make sure the application properties are correctly configured to run the project successfully.
29 |
30 | ## Upcoming Update
31 | Adding more features, error handling, authentication, and security measures.
32 |
33 | ## Contributing 🤗
34 |
35 | Feel free to explore and use these project. If you encounter any issues or have suggestions for improvements, please feel free to contribute or reach out for assistance.
36 |
37 | Contributions are always welcome! ✨
38 |
39 | See [`contributing.md`](https://github.com/ajaynegi45/Library-API/blob/main/Contributing.md) for ways to get started.
40 |
41 | Please adhere to this project's [`code_of_conduct.md`](https://github.com/ajaynegi45/Library-API/blob/main/code_of_conduct.md).
42 |
43 | ## Contact Information 📧
44 |
45 | If you have any questions or would like to connect, please don't hesitate to reach out. I'd be more than happy to chat and learn from your experiences too.
46 |
47 | **LinkedIn:** [Connect with me](https://www.linkedin.com/in/ajaynegi45/)
48 |
80 | If you like LibraryMan Project, please ★ star this repository to show your support! 🤩
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | ## Thankyou ❤️
91 | Thank you for taking the time to explore my project. I hope you find them informative and useful in your journey to learn Java and enhance your programming skills. Your support and contributions are highly appreciated.
92 | Happy coding! ✨
93 |