Compare commits

..

No commits in common. "main" and "sample-java-jwt-auth-app" have entirely different histories.

20 changed files with 28 additions and 362 deletions

View File

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

View File

@ -16,103 +16,4 @@ CREATE TABLE user_logins
login_time TIMESTAMP NOT NULL, login_time TIMESTAMP NOT NULL,
login_ip VARCHAR(255) NOT NULL, login_ip VARCHAR(255) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id) 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 http
.csrf(csrf -> csrf .csrf(csrf -> csrf
.ignoringRequestMatchers("/**") .ignoringRequestMatchers("/api/v1/**")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) // Disable CSRF for API routes .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) // Disable CSRF for API routes
.sessionManagement(sessionManagement -> sessionManagement .sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No session will be created by Spring Security .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No session will be created by Spring Security
) )
.authorizeHttpRequests(authorize -> authorize .authorizeHttpRequests(authorize -> authorize
.requestMatchers("/secure/**").authenticated() // Secure these endpoints .requestMatchers("/api/v1/secure/**").authenticated() // Secure these endpoints
.anyRequest().permitAll() // All other requests are allowed without authentication .anyRequest().permitAll() // All other requests are allowed without authentication
) )
.headers(headers -> headers .headers(headers -> headers

View File

@ -1,14 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,34 +0,0 @@
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,6 +4,7 @@ import jakarta.persistence.*;
import lombok.*; import lombok.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
@Entity @Entity
@Table(name = "user_logins") @Table(name = "user_logins")

View File

@ -1,31 +0,0 @@
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

@ -1,7 +0,0 @@
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

@ -1,10 +0,0 @@
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

@ -1,7 +0,0 @@
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; package de.w665.biblenotes.rest;
import de.w665.biblenotes.rest.dto.AuthenticationRequest; import de.w665.biblenotes.rest.ro.AuthenticationRequest;
import de.w665.biblenotes.service.AuthenticationService; import de.w665.biblenotes.service.AuthenticationService;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -16,7 +16,7 @@ import java.util.Map;
@Slf4j @Slf4j
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/api/v1/auth")
public class AuthenticationController { public class AuthenticationController {
private final AuthenticationService authenticationService; private final AuthenticationService authenticationService;

View File

@ -1,18 +0,0 @@
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

@ -1,73 +0,0 @@
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,4 +1,4 @@
package de.w665.biblenotes.rest.dto; package de.w665.biblenotes.rest.ro;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;

View File

@ -1,6 +1,5 @@
package de.w665.biblenotes.rest.security; package de.w665.biblenotes.rest.security;
import de.w665.biblenotes.db.entity.Role;
import de.w665.biblenotes.service.AuthenticationService; import de.w665.biblenotes.service.AuthenticationService;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@ -61,15 +60,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
return; return;
} }
// Extract the role and username from the JWT and set it to Spring AuthenticationContext for access control String username = authenticationService.extractSubject(jwt);
String username = authenticationService.getClaimValue(jwt, "username", String.class); // Extract the role from the JWT and set it to Spring AuthenticationContext for access control
String role = authenticationService.getClaimValue(jwt, "role", 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)); List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role));
JwtAuthenticationToken auth = new JwtAuthenticationToken(uac, jwt, authorities); JwtAuthenticationToken auth = new JwtAuthenticationToken(username, jwt, authorities);
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth); SecurityContextHolder.getContext().setAuthentication(auth);

View File

@ -1,6 +1,5 @@
package de.w665.biblenotes.rest.security; package de.w665.biblenotes.rest.security;
import lombok.Getter;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
@ -8,24 +7,24 @@ import java.util.Collection;
public class JwtAuthenticationToken extends AbstractAuthenticationToken { public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final UserAuthenticationContext principal; private final String principal;
@Getter
private final String token; private final String token;
public JwtAuthenticationToken(UserAuthenticationContext principal, String token, Collection<? extends GrantedAuthority> authorities) { public JwtAuthenticationToken(String principal, String token, Collection<? extends GrantedAuthority> authorities) {
super(authorities); super(authorities);
this.principal = principal; this.principal = principal;
this.token = token; this.token = token;
super.setAuthenticated(true); super.setAuthenticated(true); // Set this to true only if authentication is verified
} }
@Override @Override
public Object getCredentials() { public Object getCredentials() {
return "Credentials are not stored."; return null;
} }
@Override @Override
public UserAuthenticationContext getPrincipal() { public Object getPrincipal() {
return this.principal; return null;
} }
} }

View File

@ -1,13 +0,0 @@
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,11 +73,10 @@ public class AuthenticationService {
Date expiryDate = new Date(nowMillis + expirationTime); Date expiryDate = new Date(nowMillis + expirationTime);
return Jwts.builder() return Jwts.builder()
.subject("BibleNotes Authentication Token") .subject("SharePulse Authentication Token")
.issuedAt(now) .issuedAt(now)
.claim("role", user.getRole()) .claim("role", user.getRole())
.claim("username", user.getUsername()) .claim("username", user.getUsername())
.claim("userId", user.getId())
.expiration(expiryDate) .expiration(expiryDate)
.signWith(secretKey) .signWith(secretKey)
.compact(); .compact();

View File

@ -15,7 +15,4 @@ spring.jpa.database=postgresql
logging.level.de.w665.biblenotes=DEBUG logging.level.de.w665.biblenotes=DEBUG
# Static path # Static path
spring.web.resources.static-locations=classpath:/static/ 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