Compare commits
11 Commits
920fbe489d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e5792f17c | ||
|
|
9f40a6c782 | ||
|
|
c095cdab3a | ||
|
|
cea35955e4 | ||
|
|
756953fbf9 | ||
|
|
367676f6d8 | ||
|
|
4a7b0b2daf | ||
|
|
89d78e6814 | ||
|
|
2bb46499bc | ||
|
|
cb07b71a88 | ||
|
|
a8046a1227 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -31,3 +31,8 @@ build/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
**/docker/postgresql/pgdata
|
||||||
|
|
||||||
|
**/node_modules
|
||||||
|
**/.angular
|
||||||
@@ -8,6 +8,13 @@ services:
|
|||||||
- "50001:5432"
|
- "50001:5432"
|
||||||
networks:
|
networks:
|
||||||
- "sportshub-local-network"
|
- "sportshub-local-network"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: sportshub_db
|
||||||
|
POSTGRES_USER: sportshub_admin
|
||||||
|
POSTGRES_PASSWORD: password
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
volumes:
|
||||||
|
- "./docker/postgresql/pgdata:/var/lib/postgresql/data/pgdata"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
sportshub-local-network:
|
sportshub-local-network:
|
||||||
6
pom.xml
6
pom.xml
@@ -17,6 +17,7 @@
|
|||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<jakarta.servlet-api.version>6.0.0</jakarta.servlet-api.version>
|
<jakarta.servlet-api.version>6.0.0</jakarta.servlet-api.version>
|
||||||
<java-jwt.version>4.4.0</java-jwt.version>
|
<java-jwt.version>4.4.0</java-jwt.version>
|
||||||
|
<postgresql.version>42.7.0</postgresql.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
@@ -66,6 +67,11 @@
|
|||||||
<artifactId>java-jwt</artifactId>
|
<artifactId>java-jwt</artifactId>
|
||||||
<version>${java-jwt.version}</version>
|
<version>${java-jwt.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>${postgresql.version}</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,6 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>jakarta.servlet</groupId>
|
|
||||||
<artifactId>jakarta.servlet-api</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.auth0</groupId>
|
<groupId>com.auth0</groupId>
|
||||||
<artifactId>java-jwt</artifactId>
|
<artifactId>java-jwt</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.sportshub.application.configuration;
|
||||||
|
|
||||||
|
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 ServiceConfiguration {
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.sportshub.application.security;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
// This class allow to retrieve connected user information through "Authentication" object.
|
||||||
|
@Component
|
||||||
|
public class AuthenticationFacade {
|
||||||
|
public Authentication getAuthentication() {
|
||||||
|
return SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,19 +13,19 @@ import org.springframework.stereotype.Service;
|
|||||||
public class CustomUserDetailsService implements UserDetailsService {
|
public class CustomUserDetailsService implements UserDetailsService {
|
||||||
private final UserUseCases userUseCases;
|
private final UserUseCases userUseCases;
|
||||||
|
|
||||||
public CustomUserDetailsService(final UserUseCases userUseCases) {
|
public CustomUserDetailsService(UserUseCases userUseCases) {
|
||||||
this.userUseCases = userUseCases;
|
this.userUseCases = userUseCases;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(final String userIdAsString) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String userIdAsString) throws UsernameNotFoundException {
|
||||||
UUID userId = parseUserId(userIdAsString);
|
UUID userId = parseUserId(userIdAsString);
|
||||||
return userUseCases.findById(userId)
|
return userUseCases.findById(userId)
|
||||||
.map(CustomUserDetails::new)
|
.map(CustomUserDetails::new)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException(userIdAsString));
|
.orElseThrow(() -> new UsernameNotFoundException(userIdAsString));
|
||||||
}
|
}
|
||||||
|
|
||||||
private UUID parseUserId(final String userIdAsString) {
|
private UUID parseUserId(String userIdAsString) {
|
||||||
try {
|
try {
|
||||||
return UUID.fromString(userIdAsString);
|
return UUID.fromString(userIdAsString);
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ public class JwtService {
|
|||||||
private final int tokenExpirationDelayInMinutes;
|
private final int tokenExpirationDelayInMinutes;
|
||||||
|
|
||||||
public JwtService(
|
public JwtService(
|
||||||
@Value("${application.security.secretKey}") String secretKey,
|
@Value("${application.security.jwt.secretKey}") String secretKey,
|
||||||
@Value("${application.security.tokenExpirationDelayInMinutes}") int tokenExpirationDelayInMinutes
|
@Value("${application.security.jwt.expirationDelayInMinutes}") int tokenExpirationDelayInMinutes
|
||||||
) {
|
) {
|
||||||
algorithm = Algorithm.HMAC512(secretKey);
|
algorithm = Algorithm.HMAC512(secretKey);
|
||||||
this.tokenExpirationDelayInMinutes = tokenExpirationDelayInMinutes;
|
this.tokenExpirationDelayInMinutes = tokenExpirationDelayInMinutes;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.sportshub.application.security.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@PreAuthorize("hasAuthority('ROLE_' + T(org.sportshub.domain.user.model.UserRole).ADMIN.name())")
|
||||||
|
public @interface AllowedToAdmins {
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.sportshub.application.security.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@PreAuthorize("permitAll()")
|
||||||
|
public @interface AllowedToAnonymous {
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
|||||||
public class CustomUserDetails implements UserDetails {
|
public class CustomUserDetails implements UserDetails {
|
||||||
private final User user;
|
private final User user;
|
||||||
|
|
||||||
public CustomUserDetails(final User user) {
|
public CustomUserDetails(User user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,108 @@
|
|||||||
package org.sportshub.application.user;
|
package org.sportshub.application.user;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.sportshub.application.security.AuthenticationFacade;
|
||||||
import org.sportshub.application.security.JwtService;
|
import org.sportshub.application.security.JwtService;
|
||||||
|
import org.sportshub.application.security.annotation.AllowedToAdmins;
|
||||||
import org.sportshub.domain.exception.LoginFailureException;
|
import org.sportshub.domain.exception.LoginFailureException;
|
||||||
|
import org.sportshub.domain.exception.RefreshTokenDoesNotExistException;
|
||||||
|
import org.sportshub.domain.exception.UserDoesNotExistException;
|
||||||
|
import org.sportshub.domain.user.model.RefreshToken;
|
||||||
import org.sportshub.domain.user.model.User;
|
import org.sportshub.domain.user.model.User;
|
||||||
|
import org.sportshub.domain.user.model.UserAuthenticationData;
|
||||||
import org.sportshub.domain.user.port.UserPort;
|
import org.sportshub.domain.user.port.UserPort;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserUseCases {
|
public class UserUseCases {
|
||||||
|
private static final String TOKEN_TYPE = "Bearer";
|
||||||
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final UserPort userPort;
|
private final UserPort userPort;
|
||||||
|
private final AuthenticationFacade authenticationFacade;
|
||||||
|
private final int refreshTokenExpirationDelayInDays;
|
||||||
|
|
||||||
public UserUseCases(
|
public UserUseCases(
|
||||||
PasswordEncoder passwordEncoder,
|
AuthenticationFacade authenticationFacade,
|
||||||
JwtService jwtService,
|
JwtService jwtService,
|
||||||
UserPort userPort
|
PasswordEncoder passwordEncoder,
|
||||||
|
UserPort userPort,
|
||||||
|
@Value("${application.security.refreshToken.expirationDelayInDays}")
|
||||||
|
int refreshTokenExpirationDelayInDays
|
||||||
) {
|
) {
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
this.userPort = userPort;
|
this.userPort = userPort;
|
||||||
|
this.authenticationFacade = authenticationFacade;
|
||||||
|
this.refreshTokenExpirationDelayInDays = refreshTokenExpirationDelayInDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<User> findById(UUID userId) {
|
public Optional<User> findById(UUID userId) {
|
||||||
return userPort.findById(userId);
|
return userPort.findById(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllowedToAdmins
|
||||||
public List<User> findAll() {
|
public List<User> findAll() {
|
||||||
return userPort.findAll();
|
return userPort.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String authenticate(final UUID id, final String password) {
|
public UserAuthenticationData authenticate(UUID userId, String password) {
|
||||||
return userPort.findById(id)
|
User user = userPort.findById(userId)
|
||||||
.filter(user -> passwordEncoder.matches(password, user.password()))
|
|
||||||
.map(jwtService::createJwt)
|
|
||||||
.orElseThrow(LoginFailureException::new);
|
.orElseThrow(LoginFailureException::new);
|
||||||
|
|
||||||
|
if (!passwordEncoder.matches(password, user.password())) {
|
||||||
|
throw new LoginFailureException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateAuthenticationData(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserAuthenticationData authenticate(UUID refreshTokenValue) {
|
||||||
|
RefreshToken refreshToken = userPort.findRefreshTokenById(refreshTokenValue)
|
||||||
|
.filter(RefreshToken::isNotExpired)
|
||||||
|
.orElseThrow(() -> new RefreshTokenDoesNotExistException(refreshTokenValue));
|
||||||
|
|
||||||
|
User user = userPort.findById(refreshToken.userId())
|
||||||
|
.orElseThrow(() -> new UserDoesNotExistException(refreshToken.userId()));
|
||||||
|
|
||||||
|
return generateAuthenticationData(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<User> getAuthenticatedUser() {
|
||||||
|
return Optional.of(authenticationFacade.getAuthentication())
|
||||||
|
.map(Authentication::getPrincipal)
|
||||||
|
.filter(String.class::isInstance)
|
||||||
|
.map(String.class::cast)
|
||||||
|
.map(UUID::fromString)
|
||||||
|
.flatMap(userPort::findById);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserAuthenticationData generateAuthenticationData(User user) {
|
||||||
|
String accessToken = jwtService.createJwt(user);
|
||||||
|
|
||||||
|
RefreshToken newRefreshToken = createNewRefreshToken(user);
|
||||||
|
|
||||||
|
return new UserAuthenticationData(
|
||||||
|
TOKEN_TYPE,
|
||||||
|
accessToken,
|
||||||
|
newRefreshToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RefreshToken createNewRefreshToken(User user) {
|
||||||
|
RefreshToken refreshToken = new RefreshToken(
|
||||||
|
user.id(),
|
||||||
|
ZonedDateTime.now().plusDays(refreshTokenExpirationDelayInDays)
|
||||||
|
);
|
||||||
|
userPort.save(refreshToken);
|
||||||
|
return refreshToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.sportshub.domain.exception;
|
package org.sportshub.domain.exception;
|
||||||
|
|
||||||
public abstract class FunctionnalException extends RuntimeException {
|
public abstract class FunctionnalException extends RuntimeException {
|
||||||
public FunctionnalException(final String message) {
|
public FunctionnalException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.sportshub.domain.exception;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class RefreshTokenDoesNotExistException extends FunctionnalException {
|
||||||
|
public RefreshTokenDoesNotExistException(UUID refreshTokenValue) {
|
||||||
|
super(String.format("Refresh token \"%s\" does not exist.", refreshTokenValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.sportshub.domain.exception;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class RefreshTokenExpiredException extends FunctionnalException {
|
||||||
|
public RefreshTokenExpiredException(UUID refreshTokenValue) {
|
||||||
|
super(String.format("Refresh token \"%s\" is expired.", refreshTokenValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.sportshub.domain.exception;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class UserDoesNotExistException extends FunctionnalException {
|
||||||
|
public UserDoesNotExistException(UUID userId) {
|
||||||
|
super(String.format("User \"%s\" does not exist.", userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.sportshub.domain.user.model;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record RefreshToken(
|
||||||
|
UUID userId,
|
||||||
|
UUID value,
|
||||||
|
ZonedDateTime expirationDate
|
||||||
|
) {
|
||||||
|
public RefreshToken(UUID userId, ZonedDateTime exporationDate) {
|
||||||
|
this(userId, UUID.randomUUID(), exporationDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return ZonedDateTime.now().isAfter(expirationDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNotExpired() {
|
||||||
|
return !isExpired();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.sportshub.domain.user.model;
|
||||||
|
|
||||||
|
public record UserAuthenticationData(
|
||||||
|
String tokenType,
|
||||||
|
String accessToken,
|
||||||
|
RefreshToken refreshToken
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -4,10 +4,21 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.sportshub.domain.user.model.RefreshToken;
|
||||||
import org.sportshub.domain.user.model.User;
|
import org.sportshub.domain.user.model.User;
|
||||||
|
|
||||||
public interface UserPort {
|
public interface UserPort {
|
||||||
Optional<User> findById(UUID userId);
|
Optional<User> findById(UUID userId);
|
||||||
|
|
||||||
List<User> findAll();
|
List<User> findAll();
|
||||||
|
|
||||||
|
void save(User user);
|
||||||
|
|
||||||
|
boolean existsById(UUID userId);
|
||||||
|
|
||||||
|
Optional<RefreshToken> findRefreshTokenByUserId(UUID userId);
|
||||||
|
|
||||||
|
Optional<RefreshToken> findRefreshTokenById(UUID refreshTokenId);
|
||||||
|
|
||||||
|
void save(RefreshToken refreshToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package org.sportshub.exposition.configuration;
|
package org.sportshub.exposition.configuration;
|
||||||
|
|
||||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
|
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||||
|
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
|
||||||
import org.sportshub.domain.exception.LoginFailureException;
|
import org.sportshub.domain.exception.LoginFailureException;
|
||||||
|
import org.sportshub.domain.exception.RefreshTokenDoesNotExistException;
|
||||||
|
import org.sportshub.domain.exception.RefreshTokenExpiredException;
|
||||||
|
import org.sportshub.domain.exception.UserDoesNotExistException;
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
@@ -14,4 +19,22 @@ public class GlobalControllerExceptionHandler {
|
|||||||
public void handleLoginFailureException() {
|
public void handleLoginFailureException() {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseStatus(NOT_FOUND)
|
||||||
|
@ExceptionHandler(UserDoesNotExistException.class)
|
||||||
|
public void handleUserDoesNotExistException() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseStatus(NOT_FOUND)
|
||||||
|
@ExceptionHandler(RefreshTokenDoesNotExistException.class)
|
||||||
|
public void handleRefreshTokenDoesNotExistException() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseStatus(UNAUTHORIZED)
|
||||||
|
@ExceptionHandler(RefreshTokenExpiredException.class)
|
||||||
|
public void handleRefreshTokenExpiredException() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package org.sportshub.application.security;
|
package org.sportshub.exposition.configuration.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
|
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
|
||||||
import static org.springframework.util.ObjectUtils.isEmpty;
|
import static org.springframework.util.ObjectUtils.isEmpty;
|
||||||
|
import org.sportshub.application.security.JwtService;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
@@ -20,6 +21,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
@Component
|
@Component
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
private static final String BEARER_PREFIX = "Bearer ";
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final UserDetailsService userDetailsService;
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
package org.sportshub.application.configuration;
|
package org.sportshub.exposition.configuration.security;
|
||||||
|
|
||||||
import static org.sportshub.domain.user.model.UserRole.ADMIN;
|
|
||||||
import static org.springframework.http.HttpMethod.GET;
|
import static org.springframework.http.HttpMethod.GET;
|
||||||
import static org.springframework.http.HttpMethod.OPTIONS;
|
import static org.springframework.http.HttpMethod.OPTIONS;
|
||||||
import static org.springframework.http.HttpMethod.POST;
|
import static org.springframework.http.HttpMethod.POST;
|
||||||
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
|
import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;
|
||||||
import org.sportshub.application.security.JwtAuthenticationFilter;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
|
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.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
@@ -23,6 +20,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity(securedEnabled = true)
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(
|
public SecurityFilterChain securityFilterChain(
|
||||||
@@ -45,13 +43,10 @@ public class SecurityConfiguration {
|
|||||||
"/api/health/check",
|
"/api/health/check",
|
||||||
"/error"
|
"/error"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.requestMatchers(
|
|
||||||
GET,
|
|
||||||
"/api/users"
|
|
||||||
).hasAuthority(ADMIN.name())
|
|
||||||
.requestMatchers(
|
.requestMatchers(
|
||||||
POST,
|
POST,
|
||||||
"/api/users/login"
|
"/api/users/login",
|
||||||
|
"/api/users/refresh-token"
|
||||||
).permitAll()
|
).permitAll()
|
||||||
.requestMatchers(OPTIONS).permitAll()
|
.requestMatchers(OPTIONS).permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
@@ -59,9 +54,4 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
return httpSecurity.build();
|
return httpSecurity.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,14 @@ package org.sportshub.exposition.user;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.sportshub.application.security.annotation.AllowedToAdmins;
|
||||||
|
import org.sportshub.application.security.annotation.AllowedToAnonymous;
|
||||||
import org.sportshub.application.user.UserUseCases;
|
import org.sportshub.application.user.UserUseCases;
|
||||||
import org.sportshub.domain.user.model.User;
|
import org.sportshub.domain.user.model.User;
|
||||||
|
import org.sportshub.domain.user.model.UserAuthenticationData;
|
||||||
import org.sportshub.exposition.user.model.LoginRequest;
|
import org.sportshub.exposition.user.model.LoginRequest;
|
||||||
|
import org.sportshub.exposition.user.model.LoginResponse;
|
||||||
|
import org.sportshub.exposition.user.model.RefreshTokenRequest;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
@@ -16,17 +21,26 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
public class UserController {
|
public class UserController {
|
||||||
private final UserUseCases userUseCases;
|
private final UserUseCases userUseCases;
|
||||||
|
|
||||||
public UserController(final UserUseCases userUseCases) {
|
public UserController(UserUseCases userUseCases) {
|
||||||
this.userUseCases = userUseCases;
|
this.userUseCases = userUseCases;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public String login(@RequestBody LoginRequest request) {
|
@AllowedToAnonymous
|
||||||
return userUseCases.authenticate(request.id(), request.password());
|
public LoginResponse login(@RequestBody LoginRequest request) {
|
||||||
|
UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.id(), request.password());
|
||||||
|
return new LoginResponse(userAuthenticationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
@AllowedToAdmins
|
||||||
public List<User> findAll() {
|
public List<User> findAll() {
|
||||||
return userUseCases.findAll();
|
return userUseCases.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/refresh-token")
|
||||||
|
public LoginResponse refreshToken(@RequestBody RefreshTokenRequest request) {
|
||||||
|
UserAuthenticationData userAuthenticationData = userUseCases.authenticate(request.refreshTokenValue());
|
||||||
|
return new LoginResponse(userAuthenticationData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.sportshub.exposition.user.model;
|
||||||
|
|
||||||
|
import org.sportshub.domain.user.model.UserAuthenticationData;
|
||||||
|
|
||||||
|
public record LoginResponse(
|
||||||
|
String tokenType,
|
||||||
|
String accessToken,
|
||||||
|
String refreshToken
|
||||||
|
) {
|
||||||
|
public LoginResponse(UserAuthenticationData userAuthenticationData) {
|
||||||
|
this(
|
||||||
|
userAuthenticationData.tokenType(),
|
||||||
|
userAuthenticationData.accessToken(),
|
||||||
|
userAuthenticationData.refreshToken().value().toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.sportshub.exposition.user.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record RefreshTokenRequest(
|
||||||
|
UUID refreshTokenValue
|
||||||
|
) {
|
||||||
|
}
|
||||||
16
sportshub-gui/.editorconfig
Normal file
16
sportshub-gui/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Editor configuration, see https://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
102
sportshub-gui/angular.json
Normal file
102
sportshub-gui/angular.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"sportshub-gui": {
|
||||||
|
"projectType": "application",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"style": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"prefix": "app",
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/sportshub-gui",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"browser": "src/main.ts",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.app.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/indigo-pink.css",
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"budgets": [
|
||||||
|
{
|
||||||
|
"type": "initial",
|
||||||
|
"maximumWarning": "500kb",
|
||||||
|
"maximumError": "1mb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "anyComponentStyle",
|
||||||
|
"maximumWarning": "2kb",
|
||||||
|
"maximumError": "4kb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputHashing": "all"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"optimization": false,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "production"
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"buildTarget": "sportshub-gui:build:production"
|
||||||
|
},
|
||||||
|
"development": {
|
||||||
|
"buildTarget": "sportshub-gui:build:development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultConfiguration": "development"
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"buildTarget": "sportshub-gui:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:jest",
|
||||||
|
"options": {
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"inlineStyleLanguage": "scss",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17658
sportshub-gui/package-lock.json
generated
Normal file
17658
sportshub-gui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
sportshub-gui/package.json
Normal file
50
sportshub-gui/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "sportshub-gui",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve --proxy-config proxy.conf.json",
|
||||||
|
"build": "ng build",
|
||||||
|
"watch": "ng build --watch --configuration development",
|
||||||
|
"test": "ng test"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^17.0.0",
|
||||||
|
"@angular/cdk": "^17.0.2",
|
||||||
|
"@angular/common": "^17.0.0",
|
||||||
|
"@angular/compiler": "^17.0.0",
|
||||||
|
"@angular/core": "^17.0.0",
|
||||||
|
"@angular/forms": "^17.0.0",
|
||||||
|
"@angular/material": "^17.0.2",
|
||||||
|
"@angular/platform-browser": "^17.0.0",
|
||||||
|
"@angular/platform-browser-dynamic": "^17.0.0",
|
||||||
|
"@angular/router": "^17.0.0",
|
||||||
|
"ngx-cookie-service": "^17.0.0",
|
||||||
|
"rxjs": "~7.8.0",
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"zone.js": "~0.14.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "^17.0.5",
|
||||||
|
"@angular/cli": "^17.0.5",
|
||||||
|
"@angular/compiler-cli": "^17.0.0",
|
||||||
|
"@types/jasmine": "~5.1.0",
|
||||||
|
"@types/jest": "^29.5.10",
|
||||||
|
"jasmine-core": "~5.1.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-preset-angular": "^13.1.4",
|
||||||
|
"karma": "~6.4.0",
|
||||||
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
"karma-coverage": "~2.2.0",
|
||||||
|
"karma-jasmine": "~5.1.0",
|
||||||
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
|
"typescript": "~5.2.2"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "jest-preset-angular",
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"<rootDir>/src/setupJest.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
6
sportshub-gui/proxy.conf.json
Normal file
6
sportshub-gui/proxy.conf.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:8080",
|
||||||
|
"secure": false
|
||||||
|
}
|
||||||
|
}
|
||||||
2
sportshub-gui/src/app/app.component.html
Normal file
2
sportshub-gui/src/app/app.component.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<app-header/>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
8
sportshub-gui/src/app/app.component.scss
Normal file
8
sportshub-gui/src/app/app.component.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
app-header {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
sportshub-gui/src/app/app.component.ts
Normal file
21
sportshub-gui/src/app/app.component.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
|
import {LoginModule} from "./components/login/login.module";
|
||||||
|
import {HeaderComponent} from "./header/header.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
RouterOutlet,
|
||||||
|
HeaderComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrl: './app.component.scss'
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'sportshub-gui';
|
||||||
|
}
|
||||||
14
sportshub-gui/src/app/app.config.ts
Normal file
14
sportshub-gui/src/app/app.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { ApplicationConfig } from '@angular/core';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
|
||||||
|
import { routes } from './app.routes';
|
||||||
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
|
import {provideHttpClient} from "@angular/common/http";
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideRouter(routes),
|
||||||
|
provideHttpClient(),
|
||||||
|
provideAnimations()
|
||||||
|
]
|
||||||
|
};
|
||||||
16
sportshub-gui/src/app/app.routes.ts
Normal file
16
sportshub-gui/src/app/app.routes.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadChildren: () => import('./components/home/home.module').then(module => module.HomeModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
loadChildren: () => import('./components/login/login.module').then(module => module.LoginModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'logout',
|
||||||
|
loadChildren: () => import('./components/logout/logout.module').then(module => module.LogoutModule)
|
||||||
|
}
|
||||||
|
];
|
||||||
17
sportshub-gui/src/app/app.service.spec.ts
Normal file
17
sportshub-gui/src/app/app.service.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {AppService} from "./app.service";
|
||||||
|
|
||||||
|
describe('In the service AppService', () => {
|
||||||
|
let service: AppService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new AppService();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('The method "test"', () => {
|
||||||
|
it('should return "true"', () => {
|
||||||
|
const result = service.test();
|
||||||
|
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
8
sportshub-gui/src/app/app.service.ts
Normal file
8
sportshub-gui/src/app/app.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
test(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<h1>Hello world!</h1>
|
||||||
10
sportshub-gui/src/app/components/home/home.component.ts
Normal file
10
sportshub-gui/src/app/components/home/home.component.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home',
|
||||||
|
templateUrl: './home.component.html',
|
||||||
|
styleUrls: ['./home.component.scss']
|
||||||
|
})
|
||||||
|
export class HomeComponent {
|
||||||
|
|
||||||
|
}
|
||||||
25
sportshub-gui/src/app/components/home/home.module.ts
Normal file
25
sportshub-gui/src/app/components/home/home.module.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {NgModule} from "@angular/core";
|
||||||
|
import {HomeComponent} from "./home.component";
|
||||||
|
import {RouterModule} from "@angular/router";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: HomeComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
HomeComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
HomeComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class HomeModule {
|
||||||
|
|
||||||
|
}
|
||||||
13
sportshub-gui/src/app/components/login/login.component.html
Normal file
13
sportshub-gui/src/app/components/login/login.component.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<form (ngSubmit)="onSubmit()" class="shadowed" [formGroup]="loginForm" ngNativeValidate>
|
||||||
|
<div>
|
||||||
|
<label for="id">Identifier</label>
|
||||||
|
<input id="id" name="id" formControlName="id" class="input" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input id="password" name="password" type="password" formControlName="password" class="input" required/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="btn">Validate</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
19
sportshub-gui/src/app/components/login/login.component.scss
Normal file
19
sportshub-gui/src/app/components/login/login.component.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: solid 1px #e8e8e8;
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: .5em;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
sportshub-gui/src/app/components/login/login.component.ts
Normal file
35
sportshub-gui/src/app/components/login/login.component.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {Component, OnInit} from "@angular/core";
|
||||||
|
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
|
||||||
|
import {UserRestService} from "../../core/rest-services/user.rest-service";
|
||||||
|
import {LoginRequest} from "../../core/model/login-request";
|
||||||
|
import {MatSnackBar} from "@angular/material/snack-bar";
|
||||||
|
import {LoginService} from "./login.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styleUrls: ['./login.component.scss']
|
||||||
|
})
|
||||||
|
export class LoginComponent {
|
||||||
|
loginForm: FormGroup;
|
||||||
|
isLoginPending: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private loginService: LoginService,
|
||||||
|
private matSnackBar: MatSnackBar,
|
||||||
|
private userRestService: UserRestService
|
||||||
|
) {
|
||||||
|
this.loginForm = this.formBuilder.group({
|
||||||
|
id: new FormControl(undefined, [Validators.required]),
|
||||||
|
password: new FormControl(undefined, [Validators.required])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.loginForm.valid) {
|
||||||
|
const loginRequest: LoginRequest = this.loginForm.value;
|
||||||
|
this.loginService.login(loginRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
sportshub-gui/src/app/components/login/login.module.ts
Normal file
34
sportshub-gui/src/app/components/login/login.module.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {NgModule} from "@angular/core";
|
||||||
|
import {LoginComponent} from "./login.component";
|
||||||
|
import {CoreModule} from "../../core/core.module";
|
||||||
|
import {MatSnackBarModule} from "@angular/material/snack-bar";
|
||||||
|
import {RouterModule} from "@angular/router";
|
||||||
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
|
import {LoginService} from "./login.service";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: LoginComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
LoginComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
LoginService
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreModule,
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
MatSnackBarModule
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
LoginComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LoginModule {
|
||||||
|
|
||||||
|
}
|
||||||
37
sportshub-gui/src/app/components/login/login.service.ts
Normal file
37
sportshub-gui/src/app/components/login/login.service.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import {UserRestService} from "../../core/rest-services/user.rest-service";
|
||||||
|
import {LoginRequest} from "../../core/model/login-request";
|
||||||
|
import {Subject} from "rxjs";
|
||||||
|
import {MessageService} from "../../core/services/message.service";
|
||||||
|
import {AuthenticationService} from "../../core/services/authentication.service";
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoginService {
|
||||||
|
private isLoginPending: Subject<boolean> = new Subject<boolean>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authenticationService: AuthenticationService,
|
||||||
|
private messageService: MessageService,
|
||||||
|
private router: Router,
|
||||||
|
private userRestService: UserRestService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
login(loginRequest: LoginRequest): void {
|
||||||
|
this.isLoginPending.next(true);
|
||||||
|
|
||||||
|
this.userRestService.login(loginRequest)
|
||||||
|
.then(loginResponse => {
|
||||||
|
this.messageService.display('Login success!');
|
||||||
|
this.authenticationService.setAuthenticated(loginResponse);
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error.status === 400) {
|
||||||
|
this.messageService.display('Login or password incorrect.')
|
||||||
|
} else {
|
||||||
|
this.messageService.display('An error occured while login.')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<h1>Disconnection...</h1>
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
15
sportshub-gui/src/app/components/logout/logout.component.ts
Normal file
15
sportshub-gui/src/app/components/logout/logout.component.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {Component, inject, OnInit} from "@angular/core";
|
||||||
|
import {LogoutService} from "./logout.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-logout',
|
||||||
|
templateUrl: './logout.component.html',
|
||||||
|
styleUrls: ['./logout.component.scss']
|
||||||
|
})
|
||||||
|
export class LogoutComponent implements OnInit {
|
||||||
|
private logoutService = inject(LogoutService);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.logoutService.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
sportshub-gui/src/app/components/logout/logout.module.ts
Normal file
29
sportshub-gui/src/app/components/logout/logout.module.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {NgModule} from "@angular/core";
|
||||||
|
import {RouterModule} from "@angular/router";
|
||||||
|
import {LogoutComponent} from "./logout.component";
|
||||||
|
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
|
||||||
|
import {LogoutService} from "./logout.service";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: LogoutComponent
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
LogoutComponent
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
LogoutService
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
MatProgressSpinnerModule
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
LogoutComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LogoutModule {}
|
||||||
16
sportshub-gui/src/app/components/logout/logout.service.ts
Normal file
16
sportshub-gui/src/app/components/logout/logout.service.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import {AuthenticationService} from "../../core/services/authentication.service";
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LogoutService {
|
||||||
|
constructor(
|
||||||
|
private authenticationService: AuthenticationService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
this.authenticationService.setAnonymous();
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
sportshub-gui/src/app/core/core.module.ts
Normal file
17
sportshub-gui/src/app/core/core.module.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {NgModule} from "@angular/core";
|
||||||
|
import {ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {HttpClientModule} from "@angular/common/http";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
HttpClientModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
HttpClientModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CoreModule {
|
||||||
|
|
||||||
|
}
|
||||||
4
sportshub-gui/src/app/core/model/login-request.ts
Normal file
4
sportshub-gui/src/app/core/model/login-request.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface LoginRequest {
|
||||||
|
id: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
5
sportshub-gui/src/app/core/model/login-response.ts
Normal file
5
sportshub-gui/src/app/core/model/login-response.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface LoginResponse {
|
||||||
|
tokenType: string;
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import {HttpClient} from "@angular/common/http";
|
||||||
|
import {LoginResponse} from "../model/login-response";
|
||||||
|
import {firstValueFrom} from "rxjs";
|
||||||
|
import {LoginRequest} from "../model/login-request";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UserRestService {
|
||||||
|
constructor(
|
||||||
|
private httpClient: HttpClient
|
||||||
|
) {}
|
||||||
|
|
||||||
|
login(request: LoginRequest): Promise<LoginResponse> {
|
||||||
|
return firstValueFrom(
|
||||||
|
this.httpClient.post<LoginResponse>('/api/users/login', request)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import {CookieService} from "ngx-cookie-service";
|
||||||
|
import {LoginResponse} from "../model/login-response";
|
||||||
|
import {BehaviorSubject, Observable} from "rxjs";
|
||||||
|
|
||||||
|
const COOKIE_JWT = 'jwt';
|
||||||
|
const COOKIE_REFRESH_TOKEN = 'refreshToken';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthenticationService {
|
||||||
|
private authenticationSubject: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cookieService: CookieService
|
||||||
|
) {
|
||||||
|
const isAuthenticated = this.isAuthenticated();
|
||||||
|
this.authenticationSubject = new BehaviorSubject<boolean>(isAuthenticated);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAuthenticated$(): Observable<boolean> {
|
||||||
|
return this.authenticationSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthenticated(loginResponse: LoginResponse): void {
|
||||||
|
const jwt = loginResponse.accessToken;
|
||||||
|
this.cookieService.set(COOKIE_JWT, jwt);
|
||||||
|
|
||||||
|
const refreshToken = loginResponse.refreshToken;
|
||||||
|
this.cookieService.set(COOKIE_REFRESH_TOKEN, refreshToken);
|
||||||
|
this.authenticationSubject.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnonymous(): void {
|
||||||
|
this.cookieService.delete(COOKIE_JWT);
|
||||||
|
this.cookieService.delete(COOKIE_REFRESH_TOKEN);
|
||||||
|
this.authenticationSubject.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
const jwt = this.cookieService.get(COOKIE_JWT);
|
||||||
|
return jwt?.length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
sportshub-gui/src/app/core/services/message.service.ts
Normal file
15
sportshub-gui/src/app/core/services/message.service.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {inject, Injectable} from "@angular/core";
|
||||||
|
import {MatSnackBar} from "@angular/material/snack-bar";
|
||||||
|
|
||||||
|
const MESSAGE_DURATION = 5000;
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class MessageService {
|
||||||
|
private matSnackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
|
display(message: string): void {
|
||||||
|
this.matSnackBar.open(message, 'Close', { duration: MESSAGE_DURATION });
|
||||||
|
}
|
||||||
|
}
|
||||||
11
sportshub-gui/src/app/header/header.component.html
Normal file
11
sportshub-gui/src/app/header/header.component.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<a class="title" routerLink="/">SportsHub</a>
|
||||||
|
<div id="menu">
|
||||||
|
<a routerLink="/login" *ngIf="(isAuthenticated$ | async) === false" class="btn">
|
||||||
|
<mat-icon>login</mat-icon>
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
<a routerLink="/logout" *ngIf="isAuthenticated$ | async" class="btn logout">
|
||||||
|
<mat-icon>logout</mat-icon>
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
34
sportshub-gui/src/app/header/header.component.scss
Normal file
34
sportshub-gui/src/app/header/header.component.scss
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: .5em 1em;
|
||||||
|
background-color: #004680;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2em;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: .5em;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1em;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
a {
|
||||||
|
flex: 0 1;
|
||||||
|
gap: .5em;
|
||||||
|
|
||||||
|
&.logout {
|
||||||
|
background-color: #c20000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
sportshub-gui/src/app/header/header.component.ts
Normal file
28
sportshub-gui/src/app/header/header.component.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {Component} from "@angular/core";
|
||||||
|
import {RouterLink} from "@angular/router";
|
||||||
|
import {AuthenticationService} from "../core/services/authentication.service";
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
import {AsyncPipe, NgIf} from "@angular/common";
|
||||||
|
import {MatIconModule} from "@angular/material/icon";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-header',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './header.component.html',
|
||||||
|
imports: [
|
||||||
|
RouterLink,
|
||||||
|
AsyncPipe,
|
||||||
|
NgIf,
|
||||||
|
MatIconModule
|
||||||
|
],
|
||||||
|
styleUrls: ['./header.component.scss']
|
||||||
|
})
|
||||||
|
export class HeaderComponent {
|
||||||
|
isAuthenticated$: Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authenticationService: AuthenticationService
|
||||||
|
) {
|
||||||
|
this.isAuthenticated$ = this.authenticationService.isAuthenticated$;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
sportshub-gui/src/assets/.gitkeep
Normal file
0
sportshub-gui/src/assets/.gitkeep
Normal file
BIN
sportshub-gui/src/favicon.ico
Normal file
BIN
sportshub-gui/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
15
sportshub-gui/src/index.html
Normal file
15
sportshub-gui/src/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>SportshubGui</title>
|
||||||
|
<base href="/">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body class="mat-typography">
|
||||||
|
<app-root></app-root>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
sportshub-gui/src/main.ts
Normal file
6
sportshub-gui/src/main.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { bootstrapApplication } from '@angular/platform-browser';
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { AppComponent } from './app/app.component';
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent, appConfig)
|
||||||
|
.catch((err) => console.error(err));
|
||||||
0
sportshub-gui/src/setupJest.ts
Normal file
0
sportshub-gui/src/setupJest.ts
Normal file
26
sportshub-gui/src/styles.scss
Normal file
26
sportshub-gui/src/styles.scss
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||||
|
|
||||||
|
.shadowed {
|
||||||
|
box-shadow: 0 3px 1px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background-color: #008cff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: .4em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: .5em 1em;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
padding: .5em;
|
||||||
|
border-radius: .4em;
|
||||||
|
border: 1px solid #d2d2d2
|
||||||
|
}
|
||||||
14
sportshub-gui/tsconfig.app.json
Normal file
14
sportshub-gui/tsconfig.app.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/main.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
32
sportshub-gui/tsconfig.json
Normal file
32
sportshub-gui/tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictInputAccessModifiers": true,
|
||||||
|
"strictTemplates": true
|
||||||
|
}
|
||||||
|
}
|
||||||
14
sportshub-gui/tsconfig.spec.json
Normal file
14
sportshub-gui/tsconfig.spec.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.spec.ts",
|
||||||
|
"src/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -25,5 +25,17 @@
|
|||||||
<groupId>org.springframework</groupId>
|
<groupId>org.springframework</groupId>
|
||||||
<artifactId>spring-context</artifactId>
|
<artifactId>spring-context</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.sportshub.infrastructure.configuration;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableJpaRepositories("org.sportshub.infrastructure")
|
||||||
|
@EntityScan("org.sportshub.infrastructure")
|
||||||
|
public class JpaConfiguration {
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package org.sportshub.infrastructure.user.adapter;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static org.sportshub.domain.user.model.UserRole.ADMIN;
|
|
||||||
import static org.sportshub.domain.user.model.UserRole.STANDARD;
|
|
||||||
import org.sportshub.domain.user.model.User;
|
|
||||||
import org.sportshub.domain.user.port.UserPort;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class UserInMemoryAdapter implements UserPort {
|
|
||||||
private static final List<User> users = List.of(
|
|
||||||
new User(
|
|
||||||
UUID.fromString("c1a0805f-c618-47dc-bae7-bee70503644e"),
|
|
||||||
"$2a$10$WPuLOKpvaQnMotNo5ijPwegBPwmMF1C04XkTNCBpeBFo4r2YJWy.2",
|
|
||||||
List.of(STANDARD)
|
|
||||||
),
|
|
||||||
new User(
|
|
||||||
UUID.fromString("4eff194d-dd8e-463e-974f-034bfd509f84"),
|
|
||||||
"$2a$10$WPuLOKpvaQnMotNo5ijPwegBPwmMF1C04XkTNCBpeBFo4r2YJWy.2",
|
|
||||||
List.of(STANDARD)
|
|
||||||
),
|
|
||||||
new User(
|
|
||||||
UUID.fromString("c78d7d7c-0386-415d-86dc-98a470591e07"),
|
|
||||||
"$2a$10$WPuLOKpvaQnMotNo5ijPwegBPwmMF1C04XkTNCBpeBFo4r2YJWy.2",
|
|
||||||
List.of(STANDARD, ADMIN)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<User> findById(final UUID userId) {
|
|
||||||
return users.stream()
|
|
||||||
.filter(user -> userId.equals(user.id()))
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<User> findAll() {
|
|
||||||
return users;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.sportshub.infrastructure.user.adapter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.sportshub.domain.user.model.RefreshToken;
|
||||||
|
import org.sportshub.domain.user.model.User;
|
||||||
|
import org.sportshub.domain.user.port.UserPort;
|
||||||
|
import org.sportshub.infrastructure.user.model.RefreshTokenEntity;
|
||||||
|
import org.sportshub.infrastructure.user.model.UserEntity;
|
||||||
|
import org.sportshub.infrastructure.user.repository.RefreshTokenJpaRepository;
|
||||||
|
import org.sportshub.infrastructure.user.repository.UserJpaRepository;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class UserJpaAdapter implements UserPort {
|
||||||
|
private final RefreshTokenJpaRepository refreshTokenJpaRepository;
|
||||||
|
private final UserJpaRepository userJpaRepository;
|
||||||
|
|
||||||
|
public UserJpaAdapter(
|
||||||
|
RefreshTokenJpaRepository refreshTokenJpaRepository,
|
||||||
|
UserJpaRepository userJpaRepository
|
||||||
|
) {
|
||||||
|
this.refreshTokenJpaRepository = refreshTokenJpaRepository;
|
||||||
|
this.userJpaRepository = userJpaRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<User> findById(UUID userId) {
|
||||||
|
return userJpaRepository.findById(userId)
|
||||||
|
.map(UserEntity::toUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<User> findAll() {
|
||||||
|
return userJpaRepository.findAll()
|
||||||
|
.stream()
|
||||||
|
.map(UserEntity::toUser)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(User user) {
|
||||||
|
UserEntity userEntity = new UserEntity(user);
|
||||||
|
userJpaRepository.save(userEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean existsById(UUID userId) {
|
||||||
|
return userJpaRepository.existsById(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<RefreshToken> findRefreshTokenByUserId(UUID userId) {
|
||||||
|
return refreshTokenJpaRepository.findByUserId(userId)
|
||||||
|
.map(RefreshTokenEntity::toRefreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<RefreshToken> findRefreshTokenById(UUID refreshTokenId) {
|
||||||
|
return refreshTokenJpaRepository.findByValue(refreshTokenId)
|
||||||
|
.map(RefreshTokenEntity::toRefreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(RefreshToken refreshToken) {
|
||||||
|
RefreshTokenEntity refreshTokenEntity = new RefreshTokenEntity(refreshToken);
|
||||||
|
refreshTokenJpaRepository.save(refreshTokenEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package org.sportshub.infrastructure.user.model;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.sportshub.domain.user.model.RefreshToken;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "refresh_token")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class RefreshTokenEntity {
|
||||||
|
@Id
|
||||||
|
private UUID userId;
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UUID value;
|
||||||
|
@Column(nullable = false)
|
||||||
|
private ZonedDateTime expirationDate;
|
||||||
|
|
||||||
|
public RefreshTokenEntity(RefreshToken refreshToken) {
|
||||||
|
userId = refreshToken.userId();
|
||||||
|
value = refreshToken.value();
|
||||||
|
expirationDate = refreshToken.expirationDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RefreshToken toRefreshToken() {
|
||||||
|
return new RefreshToken(userId, value, expirationDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package org.sportshub.infrastructure.user.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.sportshub.domain.user.model.User;
|
||||||
|
import org.sportshub.domain.user.model.UserRole;
|
||||||
|
|
||||||
|
import jakarta.persistence.CollectionTable;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.ElementCollection;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "`user`")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class UserEntity {
|
||||||
|
@Id
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@ElementCollection(targetClass = UserRole.class)
|
||||||
|
@CollectionTable(
|
||||||
|
name = "user_role",
|
||||||
|
joinColumns = @JoinColumn(name = "user_id")
|
||||||
|
)
|
||||||
|
@Column(name = "role")
|
||||||
|
private List<UserRole> roles;
|
||||||
|
|
||||||
|
public UserEntity(User user) {
|
||||||
|
id = user.id();
|
||||||
|
password = user.password();
|
||||||
|
roles = user.roles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User toUser() {
|
||||||
|
return new User(
|
||||||
|
id,
|
||||||
|
password,
|
||||||
|
roles
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.sportshub.infrastructure.user.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.sportshub.infrastructure.user.model.RefreshTokenEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RefreshTokenJpaRepository extends JpaRepository<RefreshTokenEntity, UUID> {
|
||||||
|
Optional<RefreshTokenEntity> findByUserId(UUID userId);
|
||||||
|
|
||||||
|
Optional<RefreshTokenEntity> findByValue(UUID refreshTokenId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package org.sportshub.infrastructure.user.repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.sportshub.infrastructure.user.model.UserEntity;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserJpaRepository extends JpaRepository<UserEntity, UUID> {
|
||||||
|
@Query("SELECT u FROM UserEntity u JOIN FETCH u.roles WHERE u.id = :userId")
|
||||||
|
Optional<UserEntity> findById(@Param("userId") UUID userId);
|
||||||
|
|
||||||
|
@Query("SELECT u FROM UserEntity u JOIN FETCH u.roles")
|
||||||
|
List<UserEntity> findAll();
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
\c sportshub_db
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
CREATE USER sportshub_user
|
||||||
|
WITH PASSWORD 'password'
|
||||||
|
NOCREATEDB;
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE
|
||||||
|
ON ALL TABLES
|
||||||
|
IN SCHEMA public
|
||||||
|
TO sportshub_user;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "user" (
|
||||||
|
id UUID NOT NULL,
|
||||||
|
password VARCHAR NOT NULL,
|
||||||
|
CONSTRAINT user_pk PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_role (
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
role SMALLINT,
|
||||||
|
CONSTRAINT user_role_pk PRIMARY KEY (user_id, role),
|
||||||
|
CONSTRAINT user_role_fk_user_id FOREIGN KEY (user_id) REFERENCES "user" (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX user_role_fk_user_id_idx ON user_role (user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS refresh_token (
|
||||||
|
user_id UUID NOT NULL,
|
||||||
|
value UUID NOT NULL,
|
||||||
|
expiration_date TIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT refresh_token_pk PRIMARY KEY (user_id),
|
||||||
|
CONSTRAINT refresh_token_fk_user_id FOREIGN KEY (user_id) REFERENCES "user" (id)
|
||||||
|
);
|
||||||
|
CREATE INDEX refresh_token_fk_user_id_idx ON user_role (user_id);
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
application:
|
application:
|
||||||
security:
|
security:
|
||||||
secretKey: "secret-key"
|
jwt:
|
||||||
tokenExpirationDelayInMinutes: 30
|
secretKey: "secret-key"
|
||||||
|
expirationDelayInMinutes: 30
|
||||||
|
refreshToken:
|
||||||
|
expirationDelayInDays: 7
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
@@ -11,4 +14,11 @@ server:
|
|||||||
error:
|
error:
|
||||||
whitelabel:
|
whitelabel:
|
||||||
enabled: false # Disable html error responses.
|
enabled: false # Disable html error responses.
|
||||||
include-stacktrace: never
|
include-stacktrace: never
|
||||||
|
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
driverClassName: org.postgresql.Driver
|
||||||
|
url: jdbc:postgresql://localhost:50001/sportshub_db
|
||||||
|
username: sportshub_user
|
||||||
|
password: password
|
||||||
|
|||||||
Reference in New Issue
Block a user