added jwt

This commit is contained in:
Ksan 2025-10-31 13:20:42 +01:00
parent 78ba74cd0d
commit 43011c1509
14 changed files with 518 additions and 83 deletions

View File

@ -33,9 +33,12 @@ dependencies {
runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation "org.springframework.boot:spring-boot-starter-security"
implementation("org.simplejavamail:simple-java-mail:8.0.1")
implementation("net.sourceforge.htmlunit:htmlunit:2.70.0")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.13.0")
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
}
generateJava {

View File

@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS public.users
email character varying(255) COLLATE pg_catalog."default" NOT NULL,
password character varying(255) COLLATE pg_catalog."default" NOT NULL,
reg_time timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
notification_type character varying(255) COLLATE pg_catalog."default" NOT NULL DEFAULT 'NO_NOTIFICATION'::character varying,
CONSTRAINT users_pkey PRIMARY KEY (id),
CONSTRAINT unique_email UNIQUE (email)
);

View File

@ -0,0 +1,53 @@
package dev.ksan.etfoglasiserver.config;
import dev.ksan.etfoglasiserver.service.JWTService;
import dev.ksan.etfoglasiserver.service.MyUserDetailsService;
import dev.ksan.etfoglasiserver.service.UserService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JWTService jwtService;
@Autowired
ApplicationContext context;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
String token = null;
String username = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
username = jwtService.extractEmail(token);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = context.getBean(MyUserDetailsService.class).loadUserByUsername(username);
if (jwtService.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,63 @@
package dev.ksan.etfoglasiserver.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtFilter jwtFilter;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(customizer -> customizer.disable()).
authorizeHttpRequests(request -> request
.requestMatchers("login", "register").permitAll()
.anyRequest().authenticated()).
httpBasic(Customizer.withDefaults()).
sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(new BCryptPasswordEncoder(12));
return provider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}

View File

@ -1,50 +1,75 @@
package dev.ksan.etfoglasiserver.controller;
import dev.ksan.etfoglasiserver.dto.JwtResponseDTO;
import dev.ksan.etfoglasiserver.dto.UserCreationDTO;
import dev.ksan.etfoglasiserver.dto.UserDTO;
import dev.ksan.etfoglasiserver.dto.UserLoginDTO;
import dev.ksan.etfoglasiserver.model.NotificationMethod;
import dev.ksan.etfoglasiserver.model.User;
import dev.ksan.etfoglasiserver.service.UserService;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class UserController {
@Autowired UserService service;
@GetMapping("/users")
public List<UserDTO> getUsers() {
return service.getUsers().stream().map(this::getUserDTO).collect(Collectors.toList());
public ResponseEntity<List<UserDTO>> getUsers() {
return new ResponseEntity(service.getUsers(), HttpStatus.OK);
}
@GetMapping("users/{userId}")
public UserDTO getUserById(@PathVariable UUID userId) {
public ResponseEntity<UserDTO> getUserById(@PathVariable UUID userId) {
return getUserDTO(service.getUserById(userId));
}
UserDTO user = service.getUserById(userId);
public UserDTO getUserDTO(User user) {
return new UserDTO(user.getId(), user.getEmail(), user.getSubjectSet());
if (user != null) {
return new ResponseEntity<>(user, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
@PutMapping("/users")
public void updateUser(@RequestBody UserCreationDTO user) {
public ResponseEntity<UserDTO> updateUser(@RequestBody UserCreationDTO user) {
NotificationMethod method = NotificationMethod.valueOf(user.getNotification());
user.setNotificationMethod(method);
service.updateUser(user);
UserDTO userUpdated = service.updateUser(user);
if (userUpdated != null) {
return new ResponseEntity<>(userUpdated, HttpStatus.OK);
}
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
/*
@PostMapping("/users")
public void addUser(@RequestBody UserCreationDTO user) {
NotificationMethod method = NotificationMethod.valueOf(user.getNotification());
user.setNotificationMethod(method);
service.addUser(user);
}
*/
@PostMapping("/login")
public ResponseEntity<JwtResponseDTO> login(@RequestBody UserLoginDTO user) {
JwtResponseDTO userVer = service.verify(user);
if (userVer != null) return new ResponseEntity<>(userVer, HttpStatus.OK);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
@PostMapping("/register")
public ResponseEntity<UserDTO> register(@RequestBody UserCreationDTO user) {
UserDTO newUser = service.register(user);
if (newUser != null) return new ResponseEntity<>(newUser, HttpStatus.CREATED);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
@DeleteMapping("/users/{userId}")
public void deleteUser(@PathVariable UUID userId) {

View File

@ -0,0 +1,38 @@
package dev.ksan.etfoglasiserver.dto;
public class JwtResponseDTO {
private String token;
private String email;
private String message;
public JwtResponseDTO(String token, String email, String message) {
this.token = token;
this.email = email;
this.message = message;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@ -2,49 +2,53 @@ package dev.ksan.etfoglasiserver.dto;
import dev.ksan.etfoglasiserver.model.NotificationMethod;
import dev.ksan.etfoglasiserver.model.Subject;
import java.util.Set;
import java.util.UUID;
public class UserDTO {
private UUID id;
private String email;
private Set<Subject> subjectSet;
private NotificationMethod notificationMethod;
public UserDTO() {}
private UUID id;
private String email;
private Set<Subject> subjectSet;
private NotificationMethod notificationMethod;
public UserDTO(UUID id, String email, Set<Subject> subjectSet) {
this.id = id;
this.email = email;
this.subjectSet = subjectSet;
}
public UserDTO() {}
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public UserDTO(
UUID id, String email, Set<Subject> subjectSet, NotificationMethod notificationMethod2) {
this.id = id;
this.email = email;
this.subjectSet = subjectSet;
}
public Set<Subject> getSubjectSet() {
return subjectSet;
}
public UUID getId() {
return id;
}
public void setSubjectSet(Set<Subject> subjectSet) {
this.subjectSet = subjectSet;
}
public void setId(UUID id) {
this.id = id;
}
public NotificationMethod getNotificationMethod() {
return notificationMethod;
}
public String getEmail() {
return email;
}
public void setNotificationMethod(NotificationMethod notificationMethod) {
this.notificationMethod = notificationMethod;
}
public void setEmail(String email) {
this.email = email;
}
public Set<Subject> getSubjectSet() {
return subjectSet;
}
public void setSubjectSet(Set<Subject> subjectSet) {
this.subjectSet = subjectSet;
}
public NotificationMethod getNotificationMethod() {
return notificationMethod;
}
public void setNotificationMethod(NotificationMethod notificationMethod) {
this.notificationMethod = notificationMethod;
}
}

View File

@ -0,0 +1,23 @@
package dev.ksan.etfoglasiserver.dto;
public class UserLoginDTO {
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@ -0,0 +1,52 @@
package dev.ksan.etfoglasiserver.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
public class UserPrincipal implements UserDetails {
private User user;
public UserPrincipal(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority("USER"));
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

View File

@ -0,0 +1,76 @@
package dev.ksan.etfoglasiserver.service;
import dev.ksan.etfoglasiserver.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.function.Function;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
@Service
public class JWTService {
private String secretKey = "";
private JWTService() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("HmacSHA256");
SecretKey secKey = keyGen.generateKey();
secretKey = Base64.getEncoder().encodeToString(secKey.getEncoded());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public String generateToken(String email) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder().claims().add(claims).subject(email)
.issuedAt(new Date(System.currentTimeMillis())).expiration(new Date(System.currentTimeMillis()+60*60*300))
.and().signWith(getKey()).compact();
}
private SecretKey getKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
public String extractEmail(String token) {
return extractClaim(token, Claims::getSubject);
}
private <T> T extractClaim(String token, Function<Claims, T> claimResolver) {
final Claims claims = extractAllClaims(token);
return claimResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String userName = extractEmail(token);
return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}

View File

@ -11,15 +11,18 @@ import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
@Service
public class MailService {
String address;
String passwd;
Mailer mailer;
public MailService(){
this(".creds");
}
public MailService(String filename) {
try(BufferedReader br = new BufferedReader((new FileReader(filename)))) {
address = br.readLine();
System.out.println(address);
passwd = br.readLine();
}catch(IOException e) {
e.printStackTrace();

View File

@ -0,0 +1,30 @@
package dev.ksan.etfoglasiserver.service;
import dev.ksan.etfoglasiserver.model.User;
import dev.ksan.etfoglasiserver.model.UserPrincipal;
import dev.ksan.etfoglasiserver.repository.UserRepo;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired private UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<User> user = userRepo.findByEmail(email);
if (!user.isPresent()) {
throw new UsernameNotFoundException("user not found");
}
return new UserPrincipal(user.get());
}
}

View File

@ -16,12 +16,15 @@ import org.springframework.stereotype.Service;
@Service
public class SubjectService {
@Autowired SubjectRepo subjectRepo;
@Autowired private SubjectRepo subjectRepo;
@Autowired
UserRepo userRepo;
private UserRepo userRepo;
@Autowired
private UserService userService;
@Autowired
private MailService mailService;
public List<Subject> getSubjects() {
return subjectRepo.findAll();
}
@ -80,8 +83,7 @@ public class SubjectService {
public void sendEmailNotification(Entry entry,User user) {
MailService mail = new MailService(".creds");
mail.sendEmail(user.getEmail(),entry.getTitle(),entry.getParagraph());
mailService.sendEmail(user.getEmail(),entry.getTitle(),entry.getParagraph());
}
//todo

View File

@ -1,13 +1,27 @@
package dev.ksan.etfoglasiserver.service;
import dev.ksan.etfoglasiserver.dto.JwtResponseDTO;
import dev.ksan.etfoglasiserver.dto.UserCreationDTO;
import dev.ksan.etfoglasiserver.model.NotificationMethod;
import dev.ksan.etfoglasiserver.dto.UserDTO;
import dev.ksan.etfoglasiserver.dto.UserLoginDTO;
import dev.ksan.etfoglasiserver.model.User;
import dev.ksan.etfoglasiserver.model.UserPrincipal;
import dev.ksan.etfoglasiserver.repository.UserRepo;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -16,12 +30,20 @@ import org.springframework.transaction.annotation.Transactional;
public class UserService {
@Autowired UserRepo userRepo;
public List<User> getUsers() {
return userRepo.findAll();
@Autowired private AuthenticationManager authManager;
@Autowired private JWTService jwtService;
private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
public List<UserDTO> getUsers() {
return userRepo.findAll().stream().map(this::getUserDTO).collect(Collectors.toList());
}
public User getUserById(UUID userId) {
return userRepo.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
public UserDTO getUserById(UUID userId) {
User user = userRepo.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
return getUserDTO(user);
}
public void addUser(UserCreationDTO user) {
@ -34,47 +56,77 @@ public class UserService {
}
}
public void updateUser(UserCreationDTO user) {
public UserDTO updateUser(UserCreationDTO user) {
Optional<User> existingUserOpt = userRepo.findByEmail(user.getEmail());
if(userRepo.findByEmail(user.getNewEmail()).isPresent()) {
if (userRepo.findByEmail(user.getNewEmail()).isPresent()) {
throw new RuntimeException("Email taken");
}
if (userRepo.findByEmail(user.getEmail()).isPresent()) {
if (existingUserOpt.isPresent()) {
if (this.isValidEmail(user.getEmail())) {
User existingUser = existingUserOpt.get();
if(user.getPassword() != null && user.getPassword().length() > 0) {
if (user.getPassword() != null && user.getPassword().length() > 0) {
if (this.isValidPassword(user.getPassword())) {
existingUser.setPassword(user.getPassword());
}else throw new RuntimeException("Password too short");
} else throw new RuntimeException("Password too short");
}
if(user.getNewEmail() != null && user.getNewEmail() != existingUser.getEmail()) {
if (user.getNewEmail() != null && user.getNewEmail() != existingUser.getEmail()) {
existingUser.setEmail(user.getNewEmail());
}else{
existingUser.setEmail(user.getNewEmail());
} else {
existingUser.setEmail(user.getEmail());
}
existingUser.setEmail(user.getEmail());
}
existingUser.setSubjectSet(user.getSubjectSet());
existingUser.setNotificationMethod(user.getNotificationMethod());
userRepo.save(existingUser);
return getUserDTO(existingUser);
existingUser.setSubjectSet(user.getSubjectSet());
existingUser.setNotificationMethod(user.getNotificationMethod());
userRepo.save(existingUser);
} else throw new RuntimeException("Invalid email");
} else throw new RuntimeException("User not found");
}
public void deleteUser(UUID userId) {
userRepo.deleteById(userId);
}
public UserDTO register(UserCreationDTO user) {
if (userRepo.findByEmail(user.getEmail()).isPresent()) {
throw new RuntimeException("User already exists");
}
if (this.isValidEmail(user.getEmail()) && this.isValidPassword(user.getPassword())) {
User newUser = new User(user);
newUser.setPassword(encoder.encode(user.getPassword()));
userRepo.save(newUser);
return getUserDTO(newUser);
}
throw new RuntimeException("Error creating user");
}
public UserDTO getUserDTO(User user) {
return new UserDTO(
user.getId(), user.getEmail(), user.getSubjectSet(), user.getNotificationMethod());
}
public boolean isValidPassword(String pass) {
return pass.length() >= 8;
}
public List<User> findUsersBySubjectId(UUID id) {
List<User> users = userRepo.findUsersBySubjectId(id);
if (users.isEmpty()) return null;
return users;
}
public boolean isValidEmail(String email) {
if (email == null) {
return false;
@ -87,14 +139,24 @@ public class UserService {
throw new RuntimeException("Invalid email");
}
public boolean isValidPassword(String pass) {
return pass.length() >= 8;
public JwtResponseDTO verify(UserLoginDTO user) {
Authentication authentication = authManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword()));
if (authentication.isAuthenticated()) {
String token = jwtService.generateToken(user.getEmail());
return new JwtResponseDTO(token, user.getEmail(), HttpStatus.OK.toString());
} else {
throw new BadCredentialsException("Invalid username or password");
}
}
public List<User> findUsersBySubjectId(UUID id) {
List<User> users = userRepo.findUsersBySubjectId(id);
if (users.isEmpty()) return null;
return users;
public Optional<User> getAccountByEmail(String email) {
return userRepo.findByEmail(email);
}
}