tokens and stuff

This commit is contained in:
Stepan Usatiuk
2023-11-25 20:22:37 +01:00
parent eb9825b33a
commit c2484b5e0d
25 changed files with 240 additions and 64 deletions

View File

@@ -1,30 +1,26 @@
package com.usatiuk.tjv.y.server.controller;
import com.usatiuk.tjv.y.server.dto.PersonAuthResponse;
import com.usatiuk.tjv.y.server.dto.PersonLoginRequest;
import com.usatiuk.tjv.y.server.dto.PersonTo;
import com.usatiuk.tjv.y.server.dto.PersonSignupRequest;
import com.usatiuk.tjv.y.server.dto.converters.PersonMapper;
import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.service.PersonService;
import com.usatiuk.tjv.y.server.service.PersonTokenService;
import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException;
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/person", produces = MediaType.APPLICATION_JSON_VALUE)
public class PersonController {
private final PersonService personService;
private final PersonTokenService personTokenService;
public PersonController(PersonService personService, PersonTokenService personTokenService) {
public PersonController(PersonService personService) {
this.personService = personService;
this.personTokenService = personTokenService;
}
@PostMapping
public PersonAuthResponse signup(@RequestBody PersonSignupRequest signupRequest) {
public PersonTo signup(@RequestBody PersonSignupRequest signupRequest) throws UserAlreadyExistsException {
Person toCreate = new Person();
toCreate.setUsername(signupRequest.username())
.setPassword(signupRequest.password())
@@ -32,6 +28,14 @@ public class PersonController {
Person created = personService.signup(toCreate);
return new PersonAuthResponse(created, personTokenService.generateToken(created.getId()));
return PersonMapper.makeDto(created);
}
@GetMapping(path = "/{username}")
public PersonTo get(@PathVariable String username) throws UserNotFoundException {
Person found = personService.readByUsername(username);
return PersonMapper.makeDto(found);
}
}

View File

@@ -2,6 +2,7 @@ package com.usatiuk.tjv.y.server.controller;
import com.usatiuk.tjv.y.server.dto.PostCreate;
import com.usatiuk.tjv.y.server.dto.PostTo;
import com.usatiuk.tjv.y.server.dto.converters.PostMapper;
import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.entity.Post;
import com.usatiuk.tjv.y.server.service.PostService;
@@ -31,13 +32,13 @@ public class PostController {
Post post = new Post();
post.setAuthor(entityManager.getReference(Person.class, principal.getName()));
post.setText(postCreate.text());
return new PostTo(postService.create(post));
return PostMapper.makeDto(postService.create(post));
}
@GetMapping
public Stream<PostTo> readAllByAuthor(@RequestParam Optional<String> author) {
if (author.isPresent())
return postService.readByAuthorId(author.get()).stream().map(PostTo::new);
return postService.readByAuthorId(author.get()).stream().map(PostMapper::makeDto);
else
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
@@ -46,7 +47,7 @@ public class PostController {
public PostTo get(@PathVariable long id) {
var post = postService.readById(id);
if (post.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
return new PostTo(post.get());
return PostMapper.makeDto(post.get());
}
}

View File

@@ -0,0 +1,32 @@
package com.usatiuk.tjv.y.server.controller;
import com.usatiuk.tjv.y.server.dto.TokenRequest;
import com.usatiuk.tjv.y.server.dto.TokenResponse;
import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.service.PersonService;
import com.usatiuk.tjv.y.server.service.TokenService;
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "/token", produces = MediaType.APPLICATION_JSON_VALUE)
public class TokenController {
private final PersonService personService;
private final TokenService tokenService;
public TokenController(PersonService personService, TokenService tokenService) {
this.personService = personService;
this.tokenService = tokenService;
}
@PostMapping
public TokenResponse request(@RequestBody TokenRequest tokenRequest) throws UserNotFoundException {
Person found = personService.login(tokenRequest.username(), tokenRequest.password());
return new TokenResponse(tokenService.generateToken(found.getId()));
}
}

View File

@@ -1,9 +0,0 @@
package com.usatiuk.tjv.y.server.dto;
import com.usatiuk.tjv.y.server.entity.Person;
public record PersonAuthResponse(String uuid, String username, String fullName, String token) {
public PersonAuthResponse(Person person, String token) {
this(person.getId(), person.getUsername(), person.getFullName(), token);
}
}

View File

@@ -1,4 +0,0 @@
package com.usatiuk.tjv.y.server.dto;
public record PersonLoginRequest(String username, String password) {
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.tjv.y.server.dto;
public record PersonTo(String uuid, String username, String fullName) {
}

View File

@@ -3,7 +3,4 @@ package com.usatiuk.tjv.y.server.dto;
import com.usatiuk.tjv.y.server.entity.Post;
public record PostTo(Long id, String authorUuid, String text) {
public PostTo(Post post) {
this(post.getId(), post.getAuthor().getUuid(), post.getText());
}
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.tjv.y.server.dto;
public record TokenRequest(String username, String password) {
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.tjv.y.server.dto;
public record TokenResponse(String token) {
}

View File

@@ -0,0 +1,10 @@
package com.usatiuk.tjv.y.server.dto.converters;
import com.usatiuk.tjv.y.server.dto.PersonTo;
import com.usatiuk.tjv.y.server.entity.Person;
public class PersonMapper {
public static PersonTo makeDto(Person person) {
return new PersonTo(person.getUuid(), person.getUsername(), person.getFullName());
}
}

View File

@@ -0,0 +1,12 @@
package com.usatiuk.tjv.y.server.dto.converters;
import com.usatiuk.tjv.y.server.dto.PersonTo;
import com.usatiuk.tjv.y.server.dto.PostTo;
import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.entity.Post;
public class PostMapper {
public static PostTo makeDto(Post post) {
return new PostTo(post.getId(), post.getAuthor().getUuid(), post.getText());
}
}

View File

@@ -7,4 +7,6 @@ import java.util.Optional;
public interface PersonRepository extends CrudRepository<Person, String> {
Optional<Person> findByUsername(String username);
boolean existsByUsername(String username);
}

View File

@@ -1,6 +1,6 @@
package com.usatiuk.tjv.y.server.security;
import com.usatiuk.tjv.y.server.service.PersonTokenService;
import com.usatiuk.tjv.y.server.service.TokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -18,11 +18,11 @@ import java.util.Optional;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private final PersonTokenService personTokenService;
private final TokenService tokenService;
private final JwtUserDetailsService jwtUserDetailsService;
public JwtRequestFilter(PersonTokenService personTokenService, JwtUserDetailsService jwtUserDetailsService) {
this.personTokenService = personTokenService;
public JwtRequestFilter(TokenService tokenService, JwtUserDetailsService jwtUserDetailsService) {
this.tokenService = tokenService;
this.jwtUserDetailsService = jwtUserDetailsService;
}
@@ -36,7 +36,7 @@ public class JwtRequestFilter extends OncePerRequestFilter {
}
String token = header.substring(7);
Optional<String> userUuid = personTokenService.parseToken(token);
Optional<String> userUuid = tokenService.parseToken(token);
if (userUuid.isEmpty()) {
filterChain.doFilter(request, response);
return;

View File

@@ -35,7 +35,8 @@ public class WebSecurityConfig {
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers(mvc.pattern(HttpMethod.GET, "/post/*")).permitAll()
.requestMatchers(mvc.pattern(HttpMethod.POST, "/person")).permitAll()
.requestMatchers(mvc.pattern(HttpMethod.GET, "/person")).permitAll()
.requestMatchers(mvc.pattern(HttpMethod.GET, "/person/*")).permitAll()
.requestMatchers(mvc.pattern(HttpMethod.POST, "/token")).permitAll()
.anyRequest().hasAuthority(UserRoles.ROLE_USER.name()))
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)

View File

@@ -1,7 +1,13 @@
package com.usatiuk.tjv.y.server.service;
import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException;
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
public interface PersonService extends CrudService<Person, String> {
Person signup(Person person);
Person signup(Person person) throws UserAlreadyExistsException;
Person login(String username, String password) throws UserNotFoundException;
Person readByUsername(String username) throws UserNotFoundException;
}

View File

@@ -2,6 +2,8 @@ package com.usatiuk.tjv.y.server.service;
import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.repository.PersonRepository;
import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException;
import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException;
import org.springframework.data.repository.CrudRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@@ -23,8 +25,29 @@ public class PersonServiceImpl extends CrudServiceImpl<Person, String> implement
}
@Override
public Person signup(Person person) {
public Person signup(Person person) throws UserAlreadyExistsException {
if (personRepository.existsByUsername(person.getUsername()))
throw new UserAlreadyExistsException();
person.setPassword(passwordEncoder.encode(person.getPassword()));
return create(person);
}
@Override
public Person login(String username, String password) throws UserNotFoundException {
var found = personRepository.findByUsername(username);
if (found.isEmpty() || !passwordEncoder.matches(password, found.get().getPassword()))
throw new UserNotFoundException();
return found.get();
}
@Override
public Person readByUsername(String username) throws UserNotFoundException {
var found = personRepository.findByUsername(username);
if (found.isEmpty())
throw new UserNotFoundException();
return found.get();
}
}

View File

@@ -2,7 +2,7 @@ package com.usatiuk.tjv.y.server.service;
import java.util.Optional;
public interface PersonTokenService {
public interface TokenService {
String generateToken(String personUuid);
Optional<String> parseToken(String token);

View File

@@ -16,12 +16,12 @@ import java.util.Date;
import java.util.Optional;
@Service
public class PersonTokenServiceImpl implements PersonTokenService {
public class TokenServiceImpl implements TokenService {
private static final Duration JWT_EXPIRY = Duration.ofMinutes(20);
private final SecretKey key;
public PersonTokenServiceImpl(@Value("${jwt.secret}") String secret) {
public TokenServiceImpl(@Value("${jwt.secret}") String secret) {
// FIXME:
this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(Encoders.BASE64.encode(secret.getBytes())));
}

View File

@@ -0,0 +1,15 @@
package com.usatiuk.tjv.y.server.service.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.CONFLICT)
public class UserAlreadyExistsException extends Exception {
public UserAlreadyExistsException() {
super();
}
public UserAlreadyExistsException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,15 @@
package com.usatiuk.tjv.y.server.service.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends Exception {
public UserNotFoundException() {
super();
}
public UserNotFoundException(String message) {
super(message);
}
}

View File

@@ -1,11 +1,11 @@
package com.usatiuk.tjv.y.server.controller;
import com.usatiuk.tjv.y.server.dto.PersonAuthResponse;
import com.usatiuk.tjv.y.server.dto.TokenResponse;
import com.usatiuk.tjv.y.server.entity.Person;
import com.usatiuk.tjv.y.server.entity.Post;
import com.usatiuk.tjv.y.server.repository.PersonRepository;
import com.usatiuk.tjv.y.server.repository.PostRepository;
import com.usatiuk.tjv.y.server.service.PersonTokenService;
import com.usatiuk.tjv.y.server.service.TokenService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
@@ -31,21 +31,23 @@ public abstract class DemoDataDbTest {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private PersonTokenService personTokenService;
private TokenService tokenService;
@Autowired
private PersonRepository personRepository;
@Autowired
private PostRepository postRepository;
protected static final String person1Password = "p1p";
protected Person person1;
protected PersonAuthResponse person1Auth;
protected TokenResponse person1Auth;
protected static final String person2Password = "p2p";
protected Person person2;
protected PersonAuthResponse person2Auth;
protected TokenResponse person2Auth;
protected Post post1;
protected Post post2;
protected HttpHeaders createAuthHeaders(PersonAuthResponse personAuth) {
protected HttpHeaders createAuthHeaders(TokenResponse personAuth) {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + personAuth.token());
@@ -58,14 +60,14 @@ public abstract class DemoDataDbTest {
new Person()
.setUsername("person1")
.setFullName("Person 1")
.setPassword(passwordEncoder.encode("p1p")));
person1Auth = new PersonAuthResponse(person1, personTokenService.generateToken(person1.getUuid()));
.setPassword(passwordEncoder.encode(person1Password)));
person1Auth = new TokenResponse(tokenService.generateToken(person1.getUuid()));
person2 = personRepository.save(
new Person()
.setUsername("person2")
.setFullName("Person 2")
.setPassword(passwordEncoder.encode("p2p")));
person2Auth = new PersonAuthResponse(person1, personTokenService.generateToken(person1.getUuid()));
.setPassword(passwordEncoder.encode(person2Password)));
person2Auth = new TokenResponse(tokenService.generateToken(person1.getUuid()));
post1 = postRepository.save(new Post().setAuthor(person1).setText("post 1"));
post2 = postRepository.save(new Post().setAuthor(person2).setText("post 2"));

View File

@@ -1,15 +1,14 @@
package com.usatiuk.tjv.y.server.controller;
import com.usatiuk.tjv.y.server.dto.PersonAuthResponse;
import com.usatiuk.tjv.y.server.dto.PersonSignupRequest;
import com.usatiuk.tjv.y.server.dto.PersonTo;
import com.usatiuk.tjv.y.server.repository.PersonRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
public class PersonControllerTest extends DemoDataDbTest {
@Autowired
@@ -17,12 +16,34 @@ public class PersonControllerTest extends DemoDataDbTest {
@Test
void shouldSignUp() {
var response = restTemplate.postForObject(addr + "/person",
new PersonSignupRequest("usernew", "full name", "pass"), PersonAuthResponse.class);
var response = restTemplate.exchange(addr + "/person", HttpMethod.POST,
new HttpEntity<>(new PersonSignupRequest("usernew", "full name", "pass")),
PersonTo.class);
Assertions.assertNotNull(response);
Assertions.assertEquals(response.username(), "usernew");
Assertions.assertEquals(response.fullName(), "full name");
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
PersonTo personToResponse = response.getBody();
Assertions.assertNotNull(personToResponse);
Assertions.assertEquals(personToResponse.username(), "usernew");
Assertions.assertEquals(personToResponse.fullName(), "full name");
Assertions.assertTrue(personRepository.findByUsername("usernew").isPresent());
}
@Test
void shouldGet() {
var response = restTemplate.exchange(addr + "/person/" + person1.getUsername(),
HttpMethod.GET, HttpEntity.EMPTY, PersonTo.class);
Assertions.assertNotNull(response);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
PersonTo personToResponse = response.getBody();
Assertions.assertNotNull(personToResponse);
Assertions.assertEquals(personToResponse.username(), person1.getUsername());
Assertions.assertEquals(personToResponse.fullName(), person1.getFullName());
}
}

View File

@@ -32,12 +32,12 @@ public class PostControllerTest extends DemoDataDbTest {
entity, PostTo.class);
Assertions.assertNotNull(response);
Assertions.assertEquals(response.getStatusCode(), HttpStatus.OK);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
PostTo reponsePostTo = response.getBody();
Assertions.assertNotNull(reponsePostTo);
Assertions.assertEquals(reponsePostTo.text(), "test text");
Assertions.assertEquals(reponsePostTo.authorUuid(), person1Auth.uuid());
Assertions.assertEquals(reponsePostTo.authorUuid(), person1.getUuid());
}
@Test
@@ -46,7 +46,7 @@ public class PostControllerTest extends DemoDataDbTest {
Assertions.assertNotNull(response);
Assertions.assertEquals(response.text(), post1.getText());
Assertions.assertEquals(response.authorUuid(), person1Auth.uuid());
Assertions.assertEquals(response.authorUuid(), person1.getUuid());
}
}

View File

@@ -0,0 +1,33 @@
package com.usatiuk.tjv.y.server.controller;
import com.usatiuk.tjv.y.server.dto.TokenRequest;
import com.usatiuk.tjv.y.server.dto.TokenResponse;
import com.usatiuk.tjv.y.server.service.TokenService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
public class TokenControllerTest extends DemoDataDbTest {
@Autowired
private TokenService tokenService;
@Test
void shouldLogin() {
var response = restTemplate.exchange(addr + "/token", HttpMethod.POST,
new HttpEntity<>(new TokenRequest(person1.getUsername(), person1Password)), TokenResponse.class);
Assertions.assertNotNull(response);
Assertions.assertEquals(HttpStatus.OK, response.getStatusCode());
TokenResponse parsedResponse = response.getBody();
Assertions.assertNotNull(parsedResponse);
Assertions.assertTrue(tokenService.parseToken(parsedResponse.token()).isPresent());
}
}

View File

@@ -0,0 +1,3 @@
junit.jupiter.execution.parallel.enabled== true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=concurrent