Added users!
This commit is contained in:
parent
1034f7e65a
commit
0e96d74203
|
@ -33,3 +33,6 @@ build/
|
|||
|
||||
.sample_data
|
||||
exercise_submission_temp_files/
|
||||
private_key.der
|
||||
private_key.pem
|
||||
public_key.der
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env rdmd
|
||||
/**
|
||||
* A simple script that generates the keys needed by DigiOPP Auth for signing
|
||||
* tokens and other cryptographic needs. Use this in your development
|
||||
* environment to ensure that your local auth project can issue keys to other
|
||||
* development services.
|
||||
*
|
||||
* Authors: Andrew Lalis
|
||||
*/
|
||||
module gen_keys;
|
||||
|
||||
import std.stdio;
|
||||
import std.file;
|
||||
|
||||
const privateKeyFile = "private_key.pem";
|
||||
const privateKeyDerFile = "private_key.der";
|
||||
const publicKeyDerFile = "public_key.der";
|
||||
|
||||
const cmdGenRSAPrivateKey = "openssl genrsa -out private_key.pem 2048";
|
||||
const cmdGenDERPrivateKey = "openssl pkcs8 -topk8 -inform PEM -outform DER -in private_key.pem -out private_key.der -nocrypt";
|
||||
const cmdGenDERPublicKey = "openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der";
|
||||
|
||||
void removeIfExists(string[] files...) {
|
||||
foreach (f; files) if (exists(f)) remove(f);
|
||||
}
|
||||
|
||||
void genKeys(string[] files...) {
|
||||
import std.process : executeShell;
|
||||
import std.algorithm : canFind;
|
||||
if (canFind(files, privateKeyFile)) executeShell(cmdGenRSAPrivateKey);
|
||||
if (canFind(files, privateKeyDerFile)) executeShell(cmdGenDERPrivateKey);
|
||||
if (canFind(files, publicKeyDerFile)) executeShell(cmdGenDERPublicKey);
|
||||
}
|
||||
|
||||
void main() {
|
||||
if (!exists(privateKeyFile)) {
|
||||
writeln("No RSA private key found. Regenerating all key files.");
|
||||
removeIfExists(privateKeyDerFile, publicKeyDerFile);
|
||||
genKeys(privateKeyFile, privateKeyDerFile, publicKeyDerFile);
|
||||
} else if (!exists(privateKeyDerFile)) {
|
||||
writeln("No DER private key found. Regenerating private and public DER files.");
|
||||
removeIfExists(privateKeyDerFile, publicKeyDerFile);
|
||||
genKeys(privateKeyDerFile, publicKeyDerFile);
|
||||
} else if (!exists(publicKeyDerFile)) {
|
||||
writeln("No DER public key found. Regenerating it.");
|
||||
genKeys(publicKeyDerFile);
|
||||
}
|
||||
writeln("All keys are now generated.");
|
||||
}
|
|
@ -21,6 +21,10 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
|
@ -29,6 +33,10 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
|
@ -41,6 +49,25 @@
|
|||
<version>1.9.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT dependencies -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.2</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
|
||||
<version>0.11.2</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
jay.cutler@example.com,testpass,Jay Cutler,
|
||||
mike.mentzer@example.com,testpass,Mike Mentzer,
|
||||
ronnie.coleman@example.com,testpass,Ronnie 'Lightweight' Coleman,
|
||||
andrew.lalis@example.com,testpass,Andrew Lalis,admin
|
|
|
@ -0,0 +1,14 @@
|
|||
package nl.andrewlalis.gymboard_api.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
@Configuration
|
||||
public class SecurityComponents {
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder(10);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package nl.andrewlalis.gymboard_api.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
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.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
private final TokenAuthenticationFilter tokenAuthenticationFilter;
|
||||
|
||||
public SecurityConfig(TokenAuthenticationFilter tokenAuthenticationFilter) {
|
||||
this.tokenAuthenticationFilter = tokenAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.httpBasic().disable()
|
||||
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
|
||||
.csrf().disable()
|
||||
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.authorizeHttpRequests().anyRequest().permitAll();
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager() {
|
||||
return null;// Disable the standard spring authentication manager.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package nl.andrewlalis.gymboard_api.config;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.TokenAuthentication;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.TokenService;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
private final TokenService tokenService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public TokenAuthenticationFilter(TokenService tokenService, UserRepository userRepository) {
|
||||
this.tokenService = tokenService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
String token = tokenService.extractBearerToken(request);
|
||||
Jws<Claims> jws = tokenService.getToken(token);
|
||||
if (jws != null) {
|
||||
userRepository.findByIdWithRoles(jws.getBody().getSubject())
|
||||
.ifPresent(user -> SecurityContextHolder.getContext().setAuthentication(new TokenAuthentication(user, token)));
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package nl.andrewlalis.gymboard_api.controller;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.TokenCredentials;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.TokenService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class AuthController {
|
||||
private final TokenService tokenService;
|
||||
|
||||
public AuthController(TokenService tokenService) {
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
@PostMapping(path = "/auth/token")
|
||||
public TokenResponse getToken(@RequestBody TokenCredentials credentials) {
|
||||
return tokenService.generateAccessToken(credentials);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
|
||||
public record TokenCredentials(
|
||||
String email,
|
||||
String password
|
||||
) {}
|
|
@ -0,0 +1,3 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
|
||||
public record TokenResponse(String token) {}
|
|
@ -0,0 +1,7 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
|
||||
public record UserCreationPayload(
|
||||
String email,
|
||||
String password,
|
||||
String name
|
||||
) {}
|
|
@ -0,0 +1,19 @@
|
|||
package nl.andrewlalis.gymboard_api.controller.dto;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
|
||||
public record UserResponse(
|
||||
String id,
|
||||
boolean activated,
|
||||
String email,
|
||||
String name
|
||||
) {
|
||||
public UserResponse(User user) {
|
||||
this(
|
||||
user.getId(),
|
||||
user.isActivated(),
|
||||
user.getEmail(),
|
||||
user.getName()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package nl.andrewlalis.gymboard_api.dao.auth;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.auth.Role;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface RoleRepository extends JpaRepository<Role, String> {
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package nl.andrewlalis.gymboard_api.dao.auth;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, String>, JpaSpecificationExecutor<User> {
|
||||
boolean existsByEmail(String email);
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.id = :id")
|
||||
Optional<User> findByIdWithRoles(String id);
|
||||
|
||||
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email = :email")
|
||||
Optional<User> findByEmailWithRoles(String email);
|
||||
}
|
|
@ -2,14 +2,20 @@ package nl.andrewlalis.gymboard_api.model;
|
|||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.CompoundGymId;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.ExerciseSubmissionPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserCreationPayload;
|
||||
import nl.andrewlalis.gymboard_api.dao.CityRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.CountryRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.GymRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.RoleRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.dao.exercise.ExerciseRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.Role;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.Exercise;
|
||||
import nl.andrewlalis.gymboard_api.model.exercise.ExerciseSubmission;
|
||||
import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
|
||||
import nl.andrewlalis.gymboard_api.service.UploadService;
|
||||
import nl.andrewlalis.gymboard_api.service.auth.UserService;
|
||||
import nl.andrewlalis.gymboard_api.service.submission.ExerciseSubmissionService;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -39,19 +45,27 @@ public class SampleDataLoader implements ApplicationListener<ContextRefreshedEve
|
|||
private final ExerciseRepository exerciseRepository;
|
||||
private final ExerciseSubmissionService submissionService;
|
||||
private final UploadService uploadService;
|
||||
private final RoleRepository roleRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final UserService userService;
|
||||
|
||||
public SampleDataLoader(
|
||||
CountryRepository countryRepository,
|
||||
CityRepository cityRepository,
|
||||
GymRepository gymRepository,
|
||||
ExerciseRepository exerciseRepository,
|
||||
ExerciseSubmissionService submissionService, UploadService uploadService) {
|
||||
ExerciseSubmissionService submissionService,
|
||||
UploadService uploadService,
|
||||
RoleRepository roleRepository, UserRepository userRepository, UserService userService) {
|
||||
this.countryRepository = countryRepository;
|
||||
this.cityRepository = cityRepository;
|
||||
this.gymRepository = gymRepository;
|
||||
this.exerciseRepository = exerciseRepository;
|
||||
this.submissionService = submissionService;
|
||||
this.uploadService = uploadService;
|
||||
this.roleRepository = roleRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -122,6 +136,24 @@ public class SampleDataLoader implements ApplicationListener<ContextRefreshedEve
|
|||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
loadCsv("users", record -> {
|
||||
String email = record.get(0);
|
||||
String password = record.get(1);
|
||||
String name = record.get(2);
|
||||
String[] roleNames = record.get(3).split("\\s*\\|\\s*");
|
||||
|
||||
UserCreationPayload payload = new UserCreationPayload(email, password, name);
|
||||
var resp = userService.createUser(payload);
|
||||
User user = userRepository.findByIdWithRoles(resp.id()).orElseThrow();
|
||||
for (var roleName : roleNames) {
|
||||
if (roleName.isBlank()) continue;
|
||||
Role role = roleRepository.findById(roleName.strip().toLowerCase())
|
||||
.orElseGet(() -> roleRepository.save(new Role(roleName.strip().toLowerCase())));
|
||||
user.getRoles().add(role);
|
||||
}
|
||||
userRepository.save(user);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadCsv(String csvName, Consumer<CSVRecord> recordConsumer) throws IOException {
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package nl.andrewlalis.gymboard_api.model.auth;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "auth_role")
|
||||
public class Role {
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
private String shortName;
|
||||
|
||||
public Role() {}
|
||||
|
||||
public Role(String shortName) {
|
||||
this.shortName = shortName;
|
||||
}
|
||||
|
||||
public String getShortName() {
|
||||
return shortName;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package nl.andrewlalis.gymboard_api.model.auth;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
public record TokenAuthentication(
|
||||
User user,
|
||||
String token
|
||||
) implements Authentication {
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDetails() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
||||
// Not allowed.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return user.getEmail();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package nl.andrewlalis.gymboard_api.model.auth;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "auth_user")
|
||||
public class User {
|
||||
@Id
|
||||
@Column(nullable = false, updatable = false, length = 26)
|
||||
private String id;
|
||||
|
||||
@CreationTimestamp
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean activated;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String passwordHash;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@ManyToMany(fetch = FetchType.LAZY)
|
||||
@JoinTable(
|
||||
name = "auth_user_roles",
|
||||
joinColumns = @JoinColumn(name = "user_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "role_short_name")
|
||||
)
|
||||
private Set<Role> roles;
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(String id, boolean activated, String email, String passwordHash, String name) {
|
||||
this.id = id;
|
||||
this.activated = activated;
|
||||
this.email = email;
|
||||
this.passwordHash = passwordHash;
|
||||
this.name = name;
|
||||
this.roles = new HashSet<>();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public boolean isActivated() {
|
||||
return activated;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Set<Role> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package nl.andrewlalis.gymboard_api.service.auth;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.TokenCredentials;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.TokenResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.Role;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class TokenService {
|
||||
private static final Logger log = LoggerFactory.getLogger(TokenService.class);
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
private static final String ISSUER = "Gymboard";
|
||||
private PrivateKey privateKey = null;
|
||||
|
||||
@Value("${app.auth.private-key-location}")
|
||||
private String privateKeyLocation;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public TokenService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
public String generateAccessToken(User user) {
|
||||
Instant expiration = Instant.now().plus(30, ChronoUnit.MINUTES);
|
||||
return Jwts.builder()
|
||||
.setSubject(user.getId())
|
||||
.setIssuer(ISSUER)
|
||||
.setAudience("Gymboard App")
|
||||
.setExpiration(Date.from(expiration))
|
||||
.claim("email", user.getEmail())
|
||||
.claim("name", user.getName())
|
||||
.claim("roles", user.getRoles().stream()
|
||||
.map(Role::getShortName)
|
||||
.collect(Collectors.joining(",")))
|
||||
.signWith(getPrivateKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
public TokenResponse generateAccessToken(TokenCredentials credentials) {
|
||||
User user = userRepository.findByEmailWithRoles(credentials.email())
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED));
|
||||
if (!passwordEncoder.matches(credentials.password(), user.getPasswordHash())) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
String token = generateAccessToken(user);
|
||||
return new TokenResponse(token);
|
||||
}
|
||||
|
||||
public String extractBearerToken(HttpServletRequest request) {
|
||||
String authorizationHeader = request.getHeader("Authorization");
|
||||
if (authorizationHeader == null || authorizationHeader.isBlank()) return null;
|
||||
if (authorizationHeader.startsWith(BEARER_PREFIX)) {
|
||||
return authorizationHeader.substring(BEARER_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Jws<Claims> getToken(String token) {
|
||||
if (token == null) return null;
|
||||
var builder = Jwts.parserBuilder()
|
||||
.setSigningKey(this.getPrivateKey())
|
||||
.requireIssuer(ISSUER);
|
||||
return builder.build().parseClaimsJws(token);
|
||||
}
|
||||
|
||||
private PrivateKey getPrivateKey() {
|
||||
if (privateKey == null) {
|
||||
try {
|
||||
byte[] keyBytes = Files.readAllBytes(Path.of(this.privateKeyLocation));
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
privateKey = kf.generatePrivate(spec);
|
||||
} catch (Exception e) {
|
||||
log.error("Could not obtain private key.", e);
|
||||
throw new RuntimeException("Cannot obtain private key.", e);
|
||||
}
|
||||
}
|
||||
return privateKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package nl.andrewlalis.gymboard_api.service.auth;
|
||||
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserCreationPayload;
|
||||
import nl.andrewlalis.gymboard_api.controller.dto.UserResponse;
|
||||
import nl.andrewlalis.gymboard_api.dao.auth.UserRepository;
|
||||
import nl.andrewlalis.gymboard_api.model.auth.User;
|
||||
import nl.andrewlalis.gymboard_api.util.ULID;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
private final UserRepository userRepository;
|
||||
private final ULID ulid;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public UserService(UserRepository userRepository, ULID ulid, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.ulid = ulid;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public UserResponse getUser(String id) {
|
||||
User user = userRepository.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return new UserResponse(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserResponse createUser(UserCreationPayload payload) {
|
||||
// TODO: Validate user payload.
|
||||
User user = userRepository.save(new User(
|
||||
ulid.nextULID(),
|
||||
true, // TODO: Change this to false once email activation is in.
|
||||
payload.email(),
|
||||
passwordEncoder.encode(payload.password()),
|
||||
payload.name()
|
||||
));
|
||||
return new UserResponse(user);
|
||||
}
|
||||
}
|
|
@ -8,3 +8,6 @@ spring.jpa.show-sql=false
|
|||
|
||||
spring.task.execution.pool.core-size=3
|
||||
spring.task.execution.pool.max-size=10
|
||||
|
||||
app.auth.private-key-location=./private_key.der
|
||||
|
||||
|
|
Loading…
Reference in New Issue