Compare commits

...

3 Commits

Author SHA1 Message Date
07141f3a1c Mini refactor 2025-04-19 01:41:46 +02:00
Max W.
a8411b6e63 update auth
- add tables and repos
- add dto
2025-01-19 19:31:43 +01:00
Max W.
38c619ae70 Update docker-compose.yml 2025-01-17 01:18:13 +01:00
20 changed files with 362 additions and 28 deletions

View File

@ -11,11 +11,3 @@ services:
- ./sql-scripts:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
adminer:
image: adminer
restart: always
environment:
ADMINER_DEFAULT_SERVER: biblenotes
ports:
- "8081:8081"

View File

@ -17,3 +17,102 @@ CREATE TABLE user_logins
login_ip VARCHAR(255) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);
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);

View File

@ -39,13 +39,13 @@ public class SecurityConfig {
http
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/v1/**")
.ignoringRequestMatchers("/**")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) // Disable CSRF for API routes
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No session will be created by Spring Security
)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/v1/secure/**").authenticated() // Secure these endpoints
.requestMatchers("/secure/**").authenticated() // Secure these endpoints
.anyRequest().permitAll() // All other requests are allowed without authentication
)
.headers(headers -> headers

View File

@ -0,0 +1,14 @@
package de.w665.biblenotes.config;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Map static resources to the root path
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/");
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -4,7 +4,6 @@ import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.Date;
@Entity
@Table(name = "user_logins")

View File

@ -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;
}

View File

@ -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<BibleBook, Long> {
}

View File

@ -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<BibleReadingPlan, Long> {
List<BibleReadingPlan> getBibleReadingPlanById(Long id);
}

View File

@ -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<UserStatistics, Long> {
}

View File

@ -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;
@ -16,7 +16,7 @@ import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/v1/auth")
@RequestMapping("/auth")
public class AuthenticationController {
private final AuthenticationService authenticationService;

View File

@ -1,4 +1,4 @@
package de.w665.biblenotes.rest.ro;
package de.w665.biblenotes.rest.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;

View File

@ -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;
}

View File

@ -0,0 +1,73 @@
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
@RequestMapping("/secure/bible-reading-plan")
public class BibleReadingPlanMapping {
private final EntityManager entityManager;
private final BibleReadingPlanRepository bibleReadingPlanRepository;
public BibleReadingPlanMapping(UserRepository userRepository, EntityManager entityManager, BibleReadingPlanRepository bibleReadingPlanRepository) {
this.entityManager = entityManager;
this.bibleReadingPlanRepository = bibleReadingPlanRepository;
}
@GetMapping
public ResponseEntity<Object> getBibleReadingPlans(@RequestParam(name = "id", required = true) Long id) {
Optional<BibleReadingPlan> 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
public ResponseEntity<Object> 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);
}
}

View File

@ -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<GrantedAuthority> 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);

View File

@ -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<? extends GrantedAuthority> authorities) {
public JwtAuthenticationToken(UserAuthenticationContext principal, String token, Collection<? extends GrantedAuthority> 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;
}
}

View File

@ -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;
}

View File

@ -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();

View File

@ -16,3 +16,6 @@ logging.level.de.w665.biblenotes=DEBUG
# Static path
spring.web.resources.static-locations=classpath:/static/
# If this is removed, this prefix must be added to the security config
spring.mvc.servlet.path=/api/v1