diff --git a/db/sql-scripts/init.sql b/db/sql-scripts/init.sql index f50b206..8f06852 100644 --- a/db/sql-scripts/init.sql +++ b/db/sql-scripts/init.sql @@ -16,4 +16,103 @@ CREATE TABLE user_logins login_time TIMESTAMP NOT NULL, login_ip VARCHAR(255) NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) -); \ No newline at end of file +); + +CREATE TABLE bible_reading_plans +( + id SERIAL PRIMARY KEY, + plan_name VARCHAR(255) NOT NULL, + start_date TIMESTAMP NOT NULL, + chapter_per_day SMALLINT NOT NULL CHECK (chapter_per_day > 0), + creator_user_id BIGINT NOT NULL, + FOREIGN KEY (creator_user_id) REFERENCES users (id) +); + +CREATE TABLE bible_books +( + id SERIAL PRIMARY KEY, + book_name_en VARCHAR(255) NOT NULL, + book_name_de VARCHAR(255) NOT NULL, + chapter_count INT NOT NULL CHECK (chapter_count > 0), + is_new_testament BOOLEAN NOT NULL +); + +CREATE TABLE user_statistics +( + id SERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + current_book_id BIGINT, + current_chapter_id BIGINT, + bible_reading_plan_id BIGINT, + FOREIGN KEY (bible_reading_plan_id) REFERENCES bible_reading_plans (id), + FOREIGN KEY (current_chapter_id) REFERENCES bible_books (id), + FOREIGN KEY (user_id) REFERENCES users (id) +); + +INSERT INTO bible_books (book_name_en, book_name_de, chapter_count, is_new_testament) +VALUES ('Genesis', '1. Mose (Genesis)', 50, FALSE), + ('Exodus', '2. Mose (Exodus)', 40, FALSE), + ('Leviticus', '3. Mose (Levitikus)', 27, FALSE), + ('Numbers', '4. Mose (Numeri)', 36, FALSE), + ('Deuteronomy', '5. Mose (Deuteronomium)', 34, FALSE), + ('Joshua', 'Josua', 24, FALSE), + ('Judges', 'Richter', 21, FALSE), + ('Ruth', 'Ruth', 4, FALSE), + ('1. Samuel', '1. Samuel', 31, FALSE), + ('2. Samuel', '2. Samuel', 24, FALSE), + ('1. Kings', '1. Könige', 22, FALSE), + ('2. Kings', '2. Könige', 25, FALSE), + ('1. Chronicles', '1. Chronik', 29, FALSE), + ('2. Chronicles', '2. Chronik', 36, FALSE), + ('Ezra', 'Esra', 10, FALSE), + ('Nehemiah', 'Nehemia', 13, FALSE), + ('Esther', 'Ester', 10, FALSE), + ('Job', 'Hiob', 42, FALSE), + ('Psalms', 'Psalmen', 150, FALSE), + ('Proverbs', 'Sprüche', 31, FALSE), + ('Ecclesiastes', 'Prediger', 12, FALSE), + ('Song of Solomon', 'Hohelied', 8, FALSE), + ('Isaiah', 'Jesaja', 66, FALSE), + ('Jeremiah', 'Jeremia', 52, FALSE), + ('Lamentations', 'Klagelieder', 5, FALSE), + ('Ezekiel', 'Hesekiel', 48, FALSE), + ('Daniel', 'Daniel', 12, FALSE), + ('Hosea', 'Hosea', 14, FALSE), + ('Joel', 'Joel', 3, FALSE), + ('Amos', 'Amos', 9, FALSE), + ('Obadiah', 'Obadja', 1, FALSE), + ('Jonah', 'Jona', 4, FALSE), + ('Micah', 'Micha', 7, FALSE), + ('Nahum', 'Nahum', 3, FALSE), + ('Habakkuk', 'Habakuk', 3, FALSE), + ('Zephaniah', 'Zefanja', 3, FALSE), + ('Haggai', 'Haggai', 2, FALSE), + ('Zechariah', 'Sacharja', 14, FALSE), + ('Malachi', 'Maleachi', 4, FALSE), + ('Matthew', 'Matthäus', 28, TRUE), + ('Mark', 'Markus', 16, TRUE), + ('Luke', 'Lukas', 24, TRUE), + ('John', 'Johannes', 21, TRUE), + ('Acts', 'Apostelgeschichte', 28, TRUE), + ('Romans', 'Römer', 16, TRUE), + ('1. Corinthians', '1. Korinther', 16, TRUE), + ('2. Corinthians', '2. Korinther', 13, TRUE), + ('Galatians', 'Galater', 6, TRUE), + ('Ephesians', 'Epheser', 6, TRUE), + ('Philippians', 'Philipper', 4, TRUE), + ('Colossians', 'Kolosser', 4, TRUE), + ('1. Thessalonians', '1. Thessalonicher', 5, TRUE), + ('2. Thessalonians', '2. Thessalonicher', 3, TRUE), + ('1. Timothy', '1. Timotheus', 6, TRUE), + ('2. Timothy', '2. Timotheus', 4, TRUE), + ('Titus', 'Titus', 3, TRUE), + ('Philemon', 'Philemon', 1, TRUE), + ('Hebrews', 'Hebräer', 13, TRUE), + ('James', 'Jakobus', 5, TRUE), + ('1. Peter', '1. Petrus', 5, TRUE), + ('2. Peter', '2. Petrus', 3, TRUE), + ('1. John', '1. Johannes', 5, TRUE), + ('2. John', '2. Johannes', 1, TRUE), + ('3. John', '3. Johannes', 1, TRUE), + ('Jude', 'Judas', 1, TRUE), + ('Revelation', 'Offenbarung', 22, TRUE); \ No newline at end of file diff --git a/src/main/java/de/w665/biblenotes/db/entity/BibleBook.java b/src/main/java/de/w665/biblenotes/db/entity/BibleBook.java new file mode 100644 index 0000000..b0f56f7 --- /dev/null +++ b/src/main/java/de/w665/biblenotes/db/entity/BibleBook.java @@ -0,0 +1,28 @@ +package de.w665.biblenotes.db.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "bible_books") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class BibleBook { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "book_name_en", nullable = false) + private String bookNameEn; + + @Column(name = "book_name_de", nullable = false) + private String bookNameDe; + + @Column(name = "chapter_count", nullable = false) + private int chapterCount; + + @Column(name = "is_new_testament", nullable = false) + private boolean isNewTestament; +} diff --git a/src/main/java/de/w665/biblenotes/db/entity/BibleReadingPlan.java b/src/main/java/de/w665/biblenotes/db/entity/BibleReadingPlan.java new file mode 100644 index 0000000..f938475 --- /dev/null +++ b/src/main/java/de/w665/biblenotes/db/entity/BibleReadingPlan.java @@ -0,0 +1,34 @@ +package de.w665.biblenotes.db.entity; + +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "bible_reading_plans") +@Getter +@Setter +@AllArgsConstructor +@ToString +@NoArgsConstructor +public class BibleReadingPlan { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "plan_name", nullable = false) + private String planName; + + @Column(name = "start_date", nullable = false) + private LocalDateTime startDate; + + @Column(name = "chapter_per_day", nullable = false) + private short chapterPerDay; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @ManyToOne + @JoinColumn(name = "creator_user_id", nullable = false) + private User creatorUser; +} diff --git a/src/main/java/de/w665/biblenotes/db/entity/UserLogin.java b/src/main/java/de/w665/biblenotes/db/entity/UserLogin.java index a738952..c2e4ade 100644 --- a/src/main/java/de/w665/biblenotes/db/entity/UserLogin.java +++ b/src/main/java/de/w665/biblenotes/db/entity/UserLogin.java @@ -4,7 +4,6 @@ import jakarta.persistence.*; import lombok.*; import java.time.LocalDateTime; -import java.util.Date; @Entity @Table(name = "user_logins") diff --git a/src/main/java/de/w665/biblenotes/db/entity/UserStatistics.java b/src/main/java/de/w665/biblenotes/db/entity/UserStatistics.java new file mode 100644 index 0000000..85346c5 --- /dev/null +++ b/src/main/java/de/w665/biblenotes/db/entity/UserStatistics.java @@ -0,0 +1,31 @@ +package de.w665.biblenotes.db.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "user_statistics") +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class UserStatistics { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne + @JoinColumn(name = "current_book_id") + private BibleBook currentBook; + + @Column(name = "current_chapter_id") + private Long currentChapterId; + + @ManyToOne + @JoinColumn(name = "bible_reading_plan_id") + private BibleReadingPlan bibleReadingPlan; +} diff --git a/src/main/java/de/w665/biblenotes/db/repo/BibleBookRepository.java b/src/main/java/de/w665/biblenotes/db/repo/BibleBookRepository.java new file mode 100644 index 0000000..e61f85d --- /dev/null +++ b/src/main/java/de/w665/biblenotes/db/repo/BibleBookRepository.java @@ -0,0 +1,7 @@ +package de.w665.biblenotes.db.repo; + +import de.w665.biblenotes.db.entity.BibleBook; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BibleBookRepository extends JpaRepository { +} diff --git a/src/main/java/de/w665/biblenotes/db/repo/BibleReadingPlanRepository.java b/src/main/java/de/w665/biblenotes/db/repo/BibleReadingPlanRepository.java new file mode 100644 index 0000000..fa63241 --- /dev/null +++ b/src/main/java/de/w665/biblenotes/db/repo/BibleReadingPlanRepository.java @@ -0,0 +1,10 @@ +package de.w665.biblenotes.db.repo; + +import de.w665.biblenotes.db.entity.BibleReadingPlan; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface BibleReadingPlanRepository extends JpaRepository { + List getBibleReadingPlanById(Long id); +} \ No newline at end of file diff --git a/src/main/java/de/w665/biblenotes/db/repo/UserStatisticsRepository.java b/src/main/java/de/w665/biblenotes/db/repo/UserStatisticsRepository.java new file mode 100644 index 0000000..ff51251 --- /dev/null +++ b/src/main/java/de/w665/biblenotes/db/repo/UserStatisticsRepository.java @@ -0,0 +1,7 @@ +package de.w665.biblenotes.db.repo; + +import de.w665.biblenotes.db.entity.UserStatistics; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserStatisticsRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/de/w665/biblenotes/rest/AuthenticationController.java b/src/main/java/de/w665/biblenotes/rest/AuthenticationController.java index a2e9b31..c8801cf 100644 --- a/src/main/java/de/w665/biblenotes/rest/AuthenticationController.java +++ b/src/main/java/de/w665/biblenotes/rest/AuthenticationController.java @@ -1,6 +1,6 @@ package de.w665.biblenotes.rest; -import de.w665.biblenotes.rest.ro.AuthenticationRequest; +import de.w665.biblenotes.rest.dto.AuthenticationRequest; import de.w665.biblenotes.service.AuthenticationService; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/de/w665/biblenotes/rest/ro/AuthenticationRequest.java b/src/main/java/de/w665/biblenotes/rest/dto/AuthenticationRequest.java similarity index 86% rename from src/main/java/de/w665/biblenotes/rest/ro/AuthenticationRequest.java rename to src/main/java/de/w665/biblenotes/rest/dto/AuthenticationRequest.java index 312d9f5..79dee1e 100644 --- a/src/main/java/de/w665/biblenotes/rest/ro/AuthenticationRequest.java +++ b/src/main/java/de/w665/biblenotes/rest/dto/AuthenticationRequest.java @@ -1,4 +1,4 @@ -package de.w665.biblenotes.rest.ro; +package de.w665.biblenotes.rest.dto; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/de/w665/biblenotes/rest/dto/BibleReadingPlanDTO.java b/src/main/java/de/w665/biblenotes/rest/dto/BibleReadingPlanDTO.java new file mode 100644 index 0000000..2c8c530 --- /dev/null +++ b/src/main/java/de/w665/biblenotes/rest/dto/BibleReadingPlanDTO.java @@ -0,0 +1,18 @@ +package de.w665.biblenotes.rest.dto; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Getter +@Setter +@ToString +public class BibleReadingPlanDTO { + private Long id; + private String planName; + private LocalDateTime startDate; + private short chapterPerDay; + private String creatorUserName; +} diff --git a/src/main/java/de/w665/biblenotes/rest/mappings/BibleReadingPlanMapping.java b/src/main/java/de/w665/biblenotes/rest/mappings/BibleReadingPlanMapping.java new file mode 100644 index 0000000..8a2632f --- /dev/null +++ b/src/main/java/de/w665/biblenotes/rest/mappings/BibleReadingPlanMapping.java @@ -0,0 +1,72 @@ +package de.w665.biblenotes.rest.mappings; + +import de.w665.biblenotes.db.entity.BibleReadingPlan; +import de.w665.biblenotes.db.entity.User; +import de.w665.biblenotes.db.repo.BibleReadingPlanRepository; +import de.w665.biblenotes.db.repo.UserRepository; +import de.w665.biblenotes.rest.SecureApiRestController; +import de.w665.biblenotes.rest.dto.BibleReadingPlanDTO; +import de.w665.biblenotes.rest.security.UserAuthenticationContext; +import jakarta.persistence.EntityManager; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import java.util.Optional; + +@RestController +public class BibleReadingPlanMapping extends SecureApiRestController { + + private final EntityManager entityManager; + private final BibleReadingPlanRepository bibleReadingPlanRepository; + + public BibleReadingPlanMapping(UserRepository userRepository, EntityManager entityManager, BibleReadingPlanRepository bibleReadingPlanRepository) { + this.entityManager = entityManager; + this.bibleReadingPlanRepository = bibleReadingPlanRepository; + } + + @GetMapping("/bible-reading-plan") + public ResponseEntity getBibleReadingPlans(@RequestParam(required = true) Long id) { + Optional brp = bibleReadingPlanRepository.findById(id); + if(brp.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + BibleReadingPlanDTO bibleReadingPlanDTO = new BibleReadingPlanDTO(); + bibleReadingPlanDTO.setId(brp.get().getId()); + bibleReadingPlanDTO.setPlanName(brp.get().getPlanName()); + bibleReadingPlanDTO.setChapterPerDay(brp.get().getChapterPerDay()); + bibleReadingPlanDTO.setStartDate(brp.get().getStartDate()); + bibleReadingPlanDTO.setCreatorUserName(brp.get().getCreatorUser().getUsername()); + + return new ResponseEntity<>(bibleReadingPlanDTO, HttpStatus.OK); + } + + @PostMapping("/bible-reading-plan") + public ResponseEntity createBibleReadingPlan(@RequestBody BibleReadingPlanDTO bibleReadingPlanDTO) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + // Construct entity + BibleReadingPlan plan = new BibleReadingPlan(); + plan.setPlanName(bibleReadingPlanDTO.getPlanName()); + plan.setChapterPerDay(bibleReadingPlanDTO.getChapterPerDay()); + plan.setStartDate(bibleReadingPlanDTO.getStartDate()); + + // Set user as creator in db entity + User userProxy = entityManager.getReference(User.class, ((UserAuthenticationContext) auth.getPrincipal()).getUserId()); + plan.setCreatorUser(userProxy); + BibleReadingPlan createdPlan = bibleReadingPlanRepository.save(plan); + + // Construct response (we do this because otherwise the user object would be serialized (which would fail because it's a proxy + i addition we don't want to send the users credentials)) + BibleReadingPlanDTO responseDTO = new BibleReadingPlanDTO(); + responseDTO.setPlanName(createdPlan.getPlanName()); + responseDTO.setChapterPerDay(createdPlan.getChapterPerDay()); + responseDTO.setStartDate(createdPlan.getStartDate()); + responseDTO.setCreatorUserName(((UserAuthenticationContext) auth.getPrincipal()).getUsername()); + responseDTO.setId(createdPlan.getId()); + + return new ResponseEntity<>(responseDTO, HttpStatus.OK); + } +} diff --git a/src/main/java/de/w665/biblenotes/rest/security/JwtAuthenticationFilter.java b/src/main/java/de/w665/biblenotes/rest/security/JwtAuthenticationFilter.java index ae0cce8..2d581d6 100644 --- a/src/main/java/de/w665/biblenotes/rest/security/JwtAuthenticationFilter.java +++ b/src/main/java/de/w665/biblenotes/rest/security/JwtAuthenticationFilter.java @@ -1,5 +1,6 @@ package de.w665.biblenotes.rest.security; +import de.w665.biblenotes.db.entity.Role; import de.w665.biblenotes.service.AuthenticationService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -60,12 +61,15 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { return; } - String username = authenticationService.extractSubject(jwt); - // Extract the role from the JWT and set it to Spring AuthenticationContext for access control + // Extract the role and username from the JWT and set it to Spring AuthenticationContext for access control + String username = authenticationService.getClaimValue(jwt, "username", String.class); String role = authenticationService.getClaimValue(jwt, "role", String.class); + int userId = authenticationService.getClaimValue(jwt, "userId", Integer.class); + UserAuthenticationContext uac = new UserAuthenticationContext(username, userId, Role.valueOf(role)); + List authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role)); - JwtAuthenticationToken auth = new JwtAuthenticationToken(username, jwt, authorities); + JwtAuthenticationToken auth = new JwtAuthenticationToken(uac, jwt, authorities); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(auth); diff --git a/src/main/java/de/w665/biblenotes/rest/security/JwtAuthenticationToken.java b/src/main/java/de/w665/biblenotes/rest/security/JwtAuthenticationToken.java index 96dd2fa..169d835 100644 --- a/src/main/java/de/w665/biblenotes/rest/security/JwtAuthenticationToken.java +++ b/src/main/java/de/w665/biblenotes/rest/security/JwtAuthenticationToken.java @@ -1,5 +1,6 @@ package de.w665.biblenotes.rest.security; +import lombok.Getter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -7,24 +8,24 @@ import java.util.Collection; public class JwtAuthenticationToken extends AbstractAuthenticationToken { - private final String principal; + private final UserAuthenticationContext principal; + @Getter private final String token; - public JwtAuthenticationToken(String principal, String token, Collection authorities) { + public JwtAuthenticationToken(UserAuthenticationContext principal, String token, Collection authorities) { super(authorities); this.principal = principal; this.token = token; - super.setAuthenticated(true); // Set this to true only if authentication is verified + super.setAuthenticated(true); } - @Override public Object getCredentials() { - return null; + return "Credentials are not stored."; } @Override - public Object getPrincipal() { - return null; + public UserAuthenticationContext getPrincipal() { + return this.principal; } } diff --git a/src/main/java/de/w665/biblenotes/rest/security/UserAuthenticationContext.java b/src/main/java/de/w665/biblenotes/rest/security/UserAuthenticationContext.java new file mode 100644 index 0000000..2e0715d --- /dev/null +++ b/src/main/java/de/w665/biblenotes/rest/security/UserAuthenticationContext.java @@ -0,0 +1,13 @@ +package de.w665.biblenotes.rest.security; + +import de.w665.biblenotes.db.entity.Role; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserAuthenticationContext { + private String username; + private int userId; + private Role role; +} diff --git a/src/main/java/de/w665/biblenotes/service/AuthenticationService.java b/src/main/java/de/w665/biblenotes/service/AuthenticationService.java index a2ac11f..8c687bf 100644 --- a/src/main/java/de/w665/biblenotes/service/AuthenticationService.java +++ b/src/main/java/de/w665/biblenotes/service/AuthenticationService.java @@ -73,10 +73,11 @@ public class AuthenticationService { Date expiryDate = new Date(nowMillis + expirationTime); return Jwts.builder() - .subject("SharePulse Authentication Token") + .subject("BibleNotes Authentication Token") .issuedAt(now) .claim("role", user.getRole()) .claim("username", user.getUsername()) + .claim("userId", user.getId()) .expiration(expiryDate) .signWith(secretKey) .compact();