Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/main/java/org/runimo/runimo/RunimoApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class RunimoApplication {

public static void main(String[] args) {
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JwtResolver {

private static final String ISSUER = "RUNIMO_SERVICE";
@Value("${jwt.secret}")
private String jwtSecret;
private final String jwtSecret;

public JwtResolver(String jwtSecret) {
this.jwtSecret = jwtSecret;
}

public DecodedJWT verifyAccessToken(String token) throws JWTVerificationException {
return JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(ISSUER).build().verify(token);
Expand Down
20 changes: 9 additions & 11 deletions src/main/java/org/runimo/runimo/auth/jwt/JwtTokenFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.service.dtos.TokenPair;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtTokenFactory {

private static final String ISSUER = "RUNIMO_SERVICE";

@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
@Value("${jwt.refresh.expiration}")
private long jwtRefreshExpiration;
private final String jwtSecret;
private final long jwtExpiration;
private final long jwtRefreshExpiration;

public JwtTokenFactory(String jwtSecret, long jwtExpiration, long jwtRefreshExpiration) {
this.jwtSecret = jwtSecret;
this.jwtExpiration = jwtExpiration;
this.jwtRefreshExpiration = jwtRefreshExpiration;
}

public String generateAccessToken(String userPublicId) {
Date now = new Date();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.runimo.runimo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}
31 changes: 31 additions & 0 deletions src/main/java/org/runimo/runimo/config/JwtConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.runimo.runimo.config;

import lombok.RequiredArgsConstructor;
import org.runimo.runimo.auth.jwt.JwtResolver;
import org.runimo.runimo.auth.jwt.JwtTokenFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class JwtConfig {

@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
@Value("${jwt.refresh.expiration}")
private long jwtRefreshExpiration;

@Bean
public JwtTokenFactory jwtTokenFactory() {
return new JwtTokenFactory(jwtSecret, jwtExpiration, jwtRefreshExpiration);
}

@Bean
public JwtResolver jwtResolver() {
return new JwtResolver(jwtSecret);
}

}
12 changes: 12 additions & 0 deletions src/main/java/org/runimo/runimo/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
Expand All @@ -18,6 +19,7 @@
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationFailureHandler customAuthenticationFailureHandler;

@Bean
@Profile({"prod", "test"})
Expand All @@ -27,6 +29,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2Login(oAuth2Login -> {
oAuth2Login
.loginPage("/api/v1/users/auth/login")
.failureHandler(customAuthenticationFailureHandler);
})
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/v1/users/auth/**").permitAll()
.anyRequest().authenticated()
Expand All @@ -44,6 +51,11 @@ public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exce
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2Login(oAuth2Login -> {
oAuth2Login
.loginPage("/api/v1/users/auth/login")
.failureHandler(customAuthenticationFailureHandler);
})
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/v1/users/auth/**").permitAll()
.requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.runimo.runimo.common.scale.Pace;
import org.runimo.runimo.records.service.usecases.dtos.SegmentPace;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -89,6 +90,9 @@ public Distance getTotalDistance() {
return new Distance(totalDistance.getAmount());
}

public Duration getRunningTime() {
return Duration.between(startedAt, endAt);
}
private void validateEditor(Long editorId) {
if (editorId == null || !Objects.equals(this.userId, editorId)) {
throw new IllegalArgumentException("Invalid editor id");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public String convertToDatabaseColumn(List<SegmentPace> segmentPaces) {
public List<SegmentPace> convertToEntityAttribute(String s) {
TypeReference<List<SegmentPace>> typeRef = new TypeReference<List<SegmentPace>>() {};
try {
if(s == null || s.isEmpty()) {
return List.of();
}
return objectMapper.readValue(s, typeRef);
} catch (Exception e) {
throw new RuntimeException("Failed to convert JSON to segmentPaces", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ Slice<RunningRecord> findFirstRunOfWeek(

@Query("SELECT COUNT(r.id) FROM RunningRecord r WHERE r.userId = :id")
Long countByUserId(Long id);

@Query("select r from RunningRecord r where r.userId = :userId")
Slice<RunningRecord> findLatestByUserId(Long userId, Pageable pageRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@ public Optional<RunningRecord> findFirstRunOfCurrentWeek(Long userId) {
public Long countByUserId(Long userId) {
return recordRepository.countByUserId(userId);
}

@Transactional(readOnly = true)
public Optional<RunningRecord> findLatestRunningRecordByUserId(Long userId) {
PageRequest pageRequest = PageRequest.of(0, 1, Sort.by("startedAt").descending());
return recordRepository.findLatestByUserId(userId, pageRequest).stream().findFirst();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.runimo.runimo.user.controller;

import lombok.RequiredArgsConstructor;
import org.runimo.runimo.common.response.SuccessResponse;
import org.runimo.runimo.user.service.dtos.MyPageViewResponse;
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.runimo.runimo.user.service.usecases.query.MyPageQueryUsecase;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/users/me")
@RequiredArgsConstructor
public class MyPageController {

private final MyPageQueryUsecase myPageQueryUsecase;

@GetMapping
public ResponseEntity<SuccessResponse<MyPageViewResponse>> queryMyPageView(
@UserId Long userId) {
MyPageViewResponse response = myPageQueryUsecase.execute(userId);
return ResponseEntity.ok(SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.runimo.runimo.user.service.dtos;

import java.time.LocalDateTime;

public record LatestRunningRecord(
String title,
LocalDateTime startDateTime,
Long distanceInMeters,
Long durationInSeconds,
Long averagePaceInMiliseconds
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.runimo.runimo.user.service.dtos;

public record MyPageViewResponse(
String nickname,
String profileImageUrl,
Long totalDistanceInMeters,
Long latestRunDateBefore,
LatestRunningRecord latestRunningRecord
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.runimo.runimo.user.service.usecases.query;

import org.runimo.runimo.user.service.dtos.MyPageViewResponse;

public interface MyPageQueryUsecase {
MyPageViewResponse execute(Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.runimo.runimo.user.service.usecases.query;

import lombok.RequiredArgsConstructor;
import org.runimo.runimo.records.domain.RunningRecord;
import org.runimo.runimo.records.service.RecordFinder;
import org.runimo.runimo.user.service.dtos.MyPageViewResponse;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.service.UserFinder;
import org.runimo.runimo.user.service.dtos.LatestRunningRecord;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.NoSuchElementException;

@Service
@RequiredArgsConstructor
public class MyPageQueryUsecaseImpl implements MyPageQueryUsecase {

private final UserFinder userFinder;
private final RecordFinder recordFinder;

@Override
public MyPageViewResponse execute(Long userId) {

User user = userFinder.findUserById(userId)
.orElseThrow(NoSuchElementException::new);
RunningRecord runningRecord = recordFinder.findLatestRunningRecordByUserId(userId)
.orElseThrow(NoSuchElementException::new);
Long differenceBetweenTodayAndLastRunningDate =
ChronoUnit.DAYS.between(runningRecord.getStartedAt(), LocalDateTime.now());
return new MyPageViewResponse(
user.getNickname(),
user.getImgUrl(),
user.getTotalDistanceInMeters(),
differenceBetweenTodayAndLastRunningDate,
new LatestRunningRecord(
runningRecord.getTitle(),
runningRecord.getStartedAt(),
runningRecord.getTotalDistance().getAmount(),
runningRecord.getRunningTime().getSeconds(),
runningRecord.getAveragePace().getPaceInMilliSeconds()
)
);
}
}
21 changes: 21 additions & 0 deletions src/test/java/org/runimo/runimo/configs/ControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.runimo.runimo.configs;

import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ActiveProfiles("test")
@WebMvcTest
@AutoConfigureMockMvc
@Import({TestConfig.class, TestWebMvcConfig.class, TestSecurityConfig.class})
public @interface ControllerTest {
Class<?>[] controllers() default {};
}
20 changes: 20 additions & 0 deletions src/test/java/org/runimo/runimo/configs/TestConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.runimo.runimo.configs;

import org.runimo.runimo.auth.jwt.JwtResolver;
import org.runimo.runimo.auth.jwt.JwtTokenFactory;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

@TestConfiguration
public class TestConfig {

@Bean
public JwtTokenFactory jwtTokenFactory() {
return new JwtTokenFactory("testSecret", 1000L, 3600L);
}

@Bean
public JwtResolver jwtResolver() {
return new JwtResolver("testSecret");
}
}
51 changes: 51 additions & 0 deletions src/test/java/org/runimo/runimo/configs/TestSecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.runimo.runimo.configs;

import org.runimo.runimo.auth.filters.JwtAuthenticationFilter;
import org.runimo.runimo.config.CustomAuthenticationFailureHandler;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@TestConfiguration
@EnableWebSecurity
@Import(CustomAuthenticationFailureHandler.class)
public class TestSecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationFailureHandler customAuthenticationFailureHandler;

public TestSecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
AuthenticationFailureHandler customAuthenticationFailureHandler) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.customAuthenticationFailureHandler = customAuthenticationFailureHandler;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2Login(oAuth2Login -> {
oAuth2Login
.loginPage("/api/v1/users/auth/login")
.failureHandler(customAuthenticationFailureHandler);
})
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/v1/users/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}

Loading