Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fea/users #21

Merged
merged 27 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
645404c
feat: Add post schema
OskarWiedeweg Dec 13, 2023
4b4306d
feat: Add post dao with user and post row mapper
OskarWiedeweg Dec 13, 2023
9a898d7
feat: Add post and user dtos
OskarWiedeweg Dec 13, 2023
20fc03f
feat: add feed response dto
OskarWiedeweg Dec 13, 2023
3f6cbd8
feat: add model mapper
OskarWiedeweg Dec 13, 2023
d944bdb
feat: add feeds endpoint
OskarWiedeweg Dec 13, 2023
10a5dc9
feat: renamed user table to users and added created_at column
OskarWiedeweg Dec 13, 2023
b4da971
feat: add local date to user class
OskarWiedeweg Dec 13, 2023
d29747e
feat: add create user functionality
OskarWiedeweg Dec 13, 2023
b44e25b
feat: add spring security
OskarWiedeweg Dec 13, 2023
e206492
feat: disable form login, cors, csrf and authorize all requests
OskarWiedeweg Dec 13, 2023
01561d5
feat: added user details service
OskarWiedeweg Dec 13, 2023
3f13dbe
feat: added login
OskarWiedeweg Dec 13, 2023
670e353
feat: added json web tokens
OskarWiedeweg Dec 13, 2023
4455262
feat: return jwt and user data on login
OskarWiedeweg Dec 13, 2023
2ab2ab0
ref: remove unused dependency
OskarWiedeweg Dec 13, 2023
c281bb0
ref: make dto fields final
OskarWiedeweg Dec 13, 2023
7eb51bb
feat: add auth service tests
OskarWiedeweg Dec 13, 2023
1911110
feat: encode password
OskarWiedeweg Dec 13, 2023
7c4cf6e
feat: add user service tests
OskarWiedeweg Dec 13, 2023
9ae1cc0
feat: add username and email to jwt
OskarWiedeweg Dec 14, 2023
cd04b65
feat: moved TokenService
OskarWiedeweg Dec 14, 2023
33c1bfb
feat: Add token filter and authentication
OskarWiedeweg Dec 14, 2023
8505232
fix: check duplicate keys
OskarWiedeweg Dec 14, 2023
dcc8def
fix: add register
OskarWiedeweg Dec 14, 2023
2791d0d
fix: make fields in jwt token final
OskarWiedeweg Dec 14, 2023
eb433ec
feat: add validation
OskarWiedeweg Dec 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,36 @@
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.oskarwiedeweg.cloudwork;

import org.modelmapper.ModelMapper;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class BackendApplication {
Expand All @@ -10,4 +12,9 @@ public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}

@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.oskarwiedeweg.cloudwork.auth;

import com.oskarwiedeweg.cloudwork.user.UserService;
import lombok.Data;
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;

@Data
@Service
public class AppUserDetailsService implements UserDetailsService {

private final UserService userService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new UserUserDetails(userService
.getUserByName(username)
.orElseThrow(() -> new UsernameNotFoundException("User with username '%s' not found!".formatted(username))));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.oskarwiedeweg.cloudwork.auth;

import com.oskarwiedeweg.cloudwork.auth.dto.AuthenticationDto;
import com.oskarwiedeweg.cloudwork.auth.dto.LoginDto;
import com.oskarwiedeweg.cloudwork.auth.dto.RegisterDto;
import jakarta.validation.Valid;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@Data
@RestController
@RequestMapping("/v1/auth")
public class AuthController {

private final AuthService authService;

@PostMapping("/login")
@ResponseStatus(HttpStatus.CREATED)
public AuthenticationDto login(@Valid @RequestBody LoginDto body) {
return authService.login(body);
}

@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public AuthenticationDto register(@Valid @RequestBody RegisterDto body) {
return authService.register(body);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.oskarwiedeweg.cloudwork.auth;

import com.oskarwiedeweg.cloudwork.auth.dto.AuthenticationDto;
import com.oskarwiedeweg.cloudwork.auth.dto.LoginDto;
import com.oskarwiedeweg.cloudwork.auth.dto.RegisterDto;
import com.oskarwiedeweg.cloudwork.auth.token.TokenService;
import com.oskarwiedeweg.cloudwork.exception.DuplicateUserException;
import com.oskarwiedeweg.cloudwork.user.User;
import com.oskarwiedeweg.cloudwork.user.UserDto;
import com.oskarwiedeweg.cloudwork.user.UserService;
import lombok.Data;
import org.modelmapper.ModelMapper;
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.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

@Data
@Service
public class AuthService {

private final AuthenticationManager authenticationManager;
private final TokenService tokenService;
private final ModelMapper modelMapper;
private final UserService userService;

public AuthenticationDto login(LoginDto loginDto) {
String username = transformUsername(loginDto.getUsername());
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, loginDto.getPassword());

Authentication authenticated;
try {
authenticated = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
} catch (BadCredentialsException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Bad credentials");
}

if (!(authenticated.getPrincipal() instanceof UserUserDetails userDetails)) {
throw new RuntimeException("User Details are not expected UserUserDetails!");
}

User user = userDetails.getUser();

String token = tokenService.generateToken(user);

return new AuthenticationDto(token, modelMapper.map(user, UserDto.class));
}

public AuthenticationDto register(RegisterDto body) {
String username = transformUsername(body.getUsername());


Long userId;
try {
userId = userService.createUser(username, body.getEmail(), body.getPassword());
} catch (DuplicateUserException e) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Duplicate username '%s'".formatted(username));
}

User tempUser = User.builder()
.id(userId)
.email(body.getEmail())
.name(username)
.build();
String token = tokenService.generateToken(tempUser);

return new AuthenticationDto(token, modelMapper.map(tempUser, UserDto.class));
}

private String transformUsername(String username) {
return username.toLowerCase();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.oskarwiedeweg.cloudwork.auth;

import com.oskarwiedeweg.cloudwork.user.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@Data
public class UserUserDetails implements UserDetails {

private final User user;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getName();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.oskarwiedeweg.cloudwork.auth.dto;

import com.oskarwiedeweg.cloudwork.user.UserDto;
import lombok.Data;

@Data
public class AuthenticationDto {

private final String accessToken;
private final UserDto userData;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.oskarwiedeweg.cloudwork.auth.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
public class LoginDto {

@NotBlank
private final String username;

@NotBlank
private final String password;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.oskarwiedeweg.cloudwork.auth.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;

@Data
public class RegisterDto {

@NotBlank
@Pattern(regexp = "([A-Za-z0-9-._]{3,})")
private final String username;

@Email
@NotBlank
private final String email;

@NotBlank
@Pattern(regexp = "((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])).{8,}")
private final String password;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.oskarwiedeweg.cloudwork.auth.token;

import org.springframework.security.authentication.AbstractAuthenticationToken;

import java.util.Collections;

public class JwtAuthenticationToken extends AbstractAuthenticationToken {

private final Long userId;
private final String jwtToken;

public JwtAuthenticationToken(Long userId, String jwtToken) {
super(Collections.emptyList());
this.userId = userId;
this.jwtToken = jwtToken;
setAuthenticated(true);
}

@Override
public Object getCredentials() {
return jwtToken;
}

@Override
public Long getPrincipal() {
return userId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.oskarwiedeweg.cloudwork.auth.token;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;

@Data
@Slf4j
@Component
public class TokenFilter extends OncePerRequestFilter {

public static final String BEARER_PREFIX = "Bearer ";

private final TokenService tokenService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null) {
filterChain.doFilter(request, response);
return;
}
if (!authorizationHeader.startsWith(BEARER_PREFIX)) {
log.debug("Authorization header not prefixed with '{}'", BEARER_PREFIX);
filterChain.doFilter(request, response);
return;
}

String token = authorizationHeader.substring(BEARER_PREFIX.length());
if (!tokenService.isTokenValid(token)) {
log.debug("Invalid jwt token '{}'", token);
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Token invalid or expired.");
}

Long userId = tokenService.getUserId(token);
JwtAuthenticationToken authentication = new JwtAuthenticationToken(userId, token);

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);

log.debug("Token valid. Set authentication.");

filterChain.doFilter(request, response);
}

}
Loading
Loading