mirror of
https://github.com/cna-bootcamp/lifesub.git
synced 2026-06-12 20:49:09 +00:00
release
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
}
|
||||
bootJar {
|
||||
archiveFileName = "recommend.jar"
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:8083}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: recommend-service
|
||||
datasource:
|
||||
url: ${POSTGRES_URL}
|
||||
username: ${POSTGRES_USER}
|
||||
password: ${POSTGRES_PASSWORD}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
allowedorigins: ${ALLOWED_ORIGINS:*}
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
package com.unicorn.lifesub.recommend;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
|
||||
public class RecommendApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(RecommendApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.unicorn.lifesub.recommend.config;
|
||||
|
||||
import com.unicorn.lifesub.recommend.config.jwt.JwtAuthenticationFilter;
|
||||
import com.unicorn.lifesub.recommend.config.jwt.JwtTokenProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@SuppressWarnings("unused")
|
||||
public class SecurityConfig {
|
||||
protected final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Value("${allowedorigins}")
|
||||
private String allowedOrigins;
|
||||
|
||||
public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(cors -> cors
|
||||
.configurationSource(corsConfigurationSource())
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs.yaml", "/v3/api-docs/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.unicorn.lifesub.recommend.config;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@SuppressWarnings("unused")
|
||||
@SecurityScheme(
|
||||
name = "bearerAuth",
|
||||
type = SecuritySchemeType.HTTP,
|
||||
bearerFormat = "JWT",
|
||||
scheme = "bearer"
|
||||
)
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI openAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("구독추천 서비스 API")
|
||||
.version("v1.0.0")
|
||||
.description("구독추천 서비스 API 명세서입니다."));
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// CommonJwtAuthenticationFilter.java
|
||||
package com.unicorn.lifesub.recommend.config.jwt;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String token = resolveToken(request);
|
||||
|
||||
if (token != null && jwtTokenProvider.validateToken(token)) {
|
||||
SecurityContextHolder.getContext().setAuthentication(jwtTokenProvider.getAuthentication(token));
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// CommonJwtTokenProvider.java
|
||||
package com.unicorn.lifesub.recommend.config.jwt;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.unicorn.lifesub.common.exception.ErrorCode;
|
||||
import com.unicorn.lifesub.common.exception.InfraException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class JwtTokenProvider {
|
||||
private final Algorithm algorithm;
|
||||
|
||||
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
|
||||
this.algorithm = Algorithm.HMAC512(secretKey);
|
||||
}
|
||||
|
||||
public Authentication getAuthentication(String token) {
|
||||
try {
|
||||
DecodedJWT decodedJWT = JWT.decode(token);
|
||||
String username = decodedJWT.getSubject();
|
||||
String[] authStrings = decodedJWT.getClaim("auth").asArray(String.class);
|
||||
Collection<? extends GrantedAuthority> authorities = Arrays.stream(authStrings)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
UserDetails userDetails = new User(username, "", authorities);
|
||||
|
||||
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
|
||||
} catch (Exception e) {
|
||||
throw new InfraException(ErrorCode.INVALID_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
JWTVerifier verifier = JWT.require(algorithm).build();
|
||||
verifier.verify(token);
|
||||
return true;
|
||||
} catch (JWTVerificationException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.unicorn.lifesub.recommend.controller;
|
||||
|
||||
import com.unicorn.lifesub.common.dto.ApiResponse;
|
||||
import com.unicorn.lifesub.recommend.dto.RecommendCategoryDTO;
|
||||
import com.unicorn.lifesub.recommend.service.RecommendService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "구독추천 API", description = "구독추천 관련 API")
|
||||
@RestController
|
||||
@RequestMapping("/api/recommend")
|
||||
@RequiredArgsConstructor
|
||||
public class RecommendController {
|
||||
private final RecommendService recommendService;
|
||||
|
||||
@Operation(summary = "추천 카테고리 조회",
|
||||
description = "사용자의 지출 패턴을 분석하여 추천 구독 카테고리를 제공합니다.")
|
||||
@GetMapping("/categories")
|
||||
public ResponseEntity<ApiResponse<RecommendCategoryDTO>> getRecommendedCategory(
|
||||
@RequestParam String userId) {
|
||||
RecommendCategoryDTO response = recommendService.getRecommendedCategory(userId);
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.unicorn.lifesub.recommend.domain;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Getter
|
||||
public class RecommendedCategory {
|
||||
private final String spendingCategory;
|
||||
private final String recommendCategory;
|
||||
private final LocalDate baseDate;
|
||||
|
||||
@Builder
|
||||
public RecommendedCategory(String spendingCategory, String recommendCategory, LocalDate baseDate) {
|
||||
this.spendingCategory = spendingCategory;
|
||||
this.recommendCategory = recommendCategory;
|
||||
this.baseDate = baseDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.unicorn.lifesub.recommend.domain;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class SpendingCategory {
|
||||
private final String category;
|
||||
private final Long totalAmount;
|
||||
|
||||
@Builder
|
||||
public SpendingCategory(String category, Long totalAmount) {
|
||||
this.category = category;
|
||||
this.totalAmount = totalAmount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.unicorn.lifesub.recommend.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class RecommendCategoryDTO {
|
||||
private String categoryName;
|
||||
private String imagePath;
|
||||
private LocalDate baseDate;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.unicorn.lifesub.recommend.repository.entity;
|
||||
|
||||
import com.unicorn.lifesub.recommend.domain.RecommendedCategory;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "recommended_categories")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class RecommendedCategoryEntity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String spendingCategory;
|
||||
private String recommendCategory;
|
||||
|
||||
@Builder
|
||||
public RecommendedCategoryEntity(Long id, String spendingCategory, String recommendCategory) {
|
||||
this.id = id;
|
||||
this.spendingCategory = spendingCategory;
|
||||
this.recommendCategory = recommendCategory;
|
||||
}
|
||||
|
||||
public RecommendedCategory toDomain() {
|
||||
return RecommendedCategory.builder()
|
||||
.spendingCategory(spendingCategory)
|
||||
.recommendCategory(recommendCategory)
|
||||
.baseDate(java.time.LocalDate.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.unicorn.lifesub.recommend.repository.entity;
|
||||
|
||||
import com.unicorn.lifesub.common.entity.BaseTimeEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "spending_history")
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class SpendingEntity extends BaseTimeEntity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String userId;
|
||||
private String category;
|
||||
private Long amount;
|
||||
private LocalDate spendingDate;
|
||||
|
||||
@Builder
|
||||
public SpendingEntity(Long id, String userId, String category, Long amount, LocalDate spendingDate) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.category = category;
|
||||
this.amount = amount;
|
||||
this.spendingDate = spendingDate;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.unicorn.lifesub.recommend.repository.jpa;
|
||||
|
||||
import com.unicorn.lifesub.recommend.repository.entity.RecommendedCategoryEntity;
|
||||
import com.unicorn.lifesub.recommend.repository.entity.SpendingEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface RecommendRepository extends JpaRepository<RecommendedCategoryEntity, Long> {
|
||||
Optional<RecommendedCategoryEntity> findBySpendingCategory(String category);
|
||||
|
||||
@Query("SELECT s FROM SpendingEntity s WHERE s.userId = :userId AND s.spendingDate >= :startDate")
|
||||
List<SpendingEntity> findSpendingsByUserIdAndDateAfter(@Param("userId") String userId,
|
||||
@Param("startDate") LocalDate startDate);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.unicorn.lifesub.recommend.service;
|
||||
|
||||
import com.unicorn.lifesub.recommend.dto.RecommendCategoryDTO;
|
||||
|
||||
public interface RecommendService {
|
||||
RecommendCategoryDTO getRecommendedCategory(String userId);
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.unicorn.lifesub.recommend.service;
|
||||
|
||||
import com.unicorn.lifesub.common.exception.BusinessException;
|
||||
import com.unicorn.lifesub.common.exception.ErrorCode;
|
||||
import com.unicorn.lifesub.recommend.domain.RecommendedCategory;
|
||||
import com.unicorn.lifesub.recommend.domain.SpendingCategory;
|
||||
import com.unicorn.lifesub.recommend.dto.RecommendCategoryDTO;
|
||||
import com.unicorn.lifesub.recommend.repository.jpa.RecommendRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RecommendServiceImpl implements RecommendService {
|
||||
private final RecommendRepository recommendRepository;
|
||||
private final SpendingAnalyzer spendingAnalyzer;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public RecommendCategoryDTO getRecommendedCategory(String userId) {
|
||||
LocalDate startDate = LocalDate.now().minusMonths(1);
|
||||
|
||||
SpendingCategory topSpending = spendingAnalyzer.analyzeSpending(
|
||||
recommendRepository.findSpendingsByUserIdAndDateAfter(userId, startDate)
|
||||
);
|
||||
|
||||
if (topSpending == null) {
|
||||
throw new BusinessException(ErrorCode.NO_SPENDING_DATA);
|
||||
}
|
||||
|
||||
RecommendedCategory recommendedCategory = recommendRepository
|
||||
.findBySpendingCategory(topSpending.getCategory())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.NO_SPENDING_DATA))
|
||||
.toDomain();
|
||||
|
||||
return RecommendCategoryDTO.builder()
|
||||
.categoryName(recommendedCategory.getRecommendCategory())
|
||||
.imagePath(getCategoryImagePath(recommendedCategory.getRecommendCategory()))
|
||||
.baseDate(recommendedCategory.getBaseDate())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String getCategoryImagePath(String category) {
|
||||
return "/images/categories/" + category.toLowerCase() + ".png";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.unicorn.lifesub.recommend.service;
|
||||
|
||||
import com.unicorn.lifesub.recommend.domain.SpendingCategory;
|
||||
import com.unicorn.lifesub.recommend.repository.entity.SpendingEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class SpendingAnalyzer {
|
||||
public SpendingCategory analyzeSpending(List<SpendingEntity> spendings) {
|
||||
Map<String, Long> totalsByCategory = calculateTotalByCategory(spendings);
|
||||
return findTopCategory(totalsByCategory);
|
||||
}
|
||||
|
||||
private Map<String, Long> calculateTotalByCategory(List<SpendingEntity> spendings) {
|
||||
return spendings.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
SpendingEntity::getCategory,
|
||||
Collectors.summingLong(SpendingEntity::getAmount)
|
||||
));
|
||||
}
|
||||
|
||||
private SpendingCategory findTopCategory(Map<String, Long> totals) {
|
||||
return totals.entrySet().stream()
|
||||
.max(Map.Entry.comparingByValue())
|
||||
.map(entry -> SpendingCategory.builder()
|
||||
.category(entry.getKey())
|
||||
.totalAmount(entry.getValue())
|
||||
.build())
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:8083}
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: recommend-service
|
||||
datasource:
|
||||
url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DB:recommend}
|
||||
username: ${POSTGRES_USER:postgres}
|
||||
password: ${POSTGRES_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
|
||||
allowedorigins: ${ALLOWED_ORIGINS:*}
|
||||
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
Reference in New Issue
Block a user