goldenChat base source add
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import com.goldenchart.service.AuthService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AdminUserInitializer implements ApplicationRunner {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
authService.ensureDefaultAdmin();
|
||||
log.info("[Auth] default admin user ensured (username=admin)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "goldenchart.cors")
|
||||
public class CorsConfig {
|
||||
|
||||
private List<String> allowedOrigins = List.of("http://localhost:5173", "http://localhost:3000");
|
||||
|
||||
public List<String> getAllowedOrigins() {
|
||||
return allowedOrigins;
|
||||
}
|
||||
|
||||
public void setAllowedOrigins(List<String> allowedOrigins) {
|
||||
this.allowedOrigins = allowedOrigins;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
// setAllowedOriginPatterns 사용: wildcard(*) + allowCredentials 동시 지원
|
||||
if (allowedOrigins.contains("*")) {
|
||||
config.setAllowedOriginPatterns(List.of("*"));
|
||||
} else {
|
||||
config.setAllowedOriginPatterns(allowedOrigins);
|
||||
}
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import com.google.firebase.FirebaseApp;
|
||||
import com.google.firebase.FirebaseOptions;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class FirebaseConfig {
|
||||
|
||||
@Value("${firebase.service-account-file:firebase-service-account.json}")
|
||||
private String serviceAccountFile;
|
||||
|
||||
@Value("${firebase.enabled:false}")
|
||||
private boolean firebaseEnabled;
|
||||
|
||||
@PostConstruct
|
||||
public void initialize() {
|
||||
if (!firebaseEnabled) {
|
||||
log.info("[Firebase] disabled (firebase.enabled=false)");
|
||||
return;
|
||||
}
|
||||
if (!FirebaseApp.getApps().isEmpty()) return;
|
||||
|
||||
try (InputStream in = openServiceAccount()) {
|
||||
if (in == null) {
|
||||
log.warn("[Firebase] service account not found — FCM disabled");
|
||||
return;
|
||||
}
|
||||
FirebaseOptions options = FirebaseOptions.builder()
|
||||
.setCredentials(GoogleCredentials.fromStream(in))
|
||||
.build();
|
||||
FirebaseApp.initializeApp(options);
|
||||
log.info("[Firebase] Admin SDK initialized — FCM enabled");
|
||||
} catch (Exception e) {
|
||||
log.warn("[Firebase] init failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream openServiceAccount() {
|
||||
try {
|
||||
var res = new ClassPathResource(serviceAccountFile);
|
||||
if (res.exists()) return res.getInputStream();
|
||||
} catch (Exception ignored) { }
|
||||
try {
|
||||
return new FileInputStream(serviceAccountFile);
|
||||
} catch (Exception ignored) { }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import com.goldenchart.entity.GcAppSettings;
|
||||
import com.goldenchart.entity.GcWatchlist;
|
||||
import com.goldenchart.repository.GcWatchlistRepository;
|
||||
import com.goldenchart.service.AppSettingsService;
|
||||
import com.goldenchart.service.LiveStrategyEvaluator;
|
||||
import com.goldenchart.websocket.DynamicSubscriptionManager;
|
||||
import com.goldenchart.websocket.UpbitWebSocketClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 서버 기동 시 isLiveCheck=true 인 모든 종목을 업비트 WS·Ta4jStorage 에 고정 구독한다.
|
||||
* (재시작 후에도 실시간 전략 평가가 끊기지 않도록)
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LiveStrategyStartupRunner implements ApplicationRunner {
|
||||
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final GcWatchlistRepository watchlistRepo;
|
||||
private final DynamicSubscriptionManager subscriptionManager;
|
||||
private final LiveStrategyEvaluator liveStrategyEvaluator;
|
||||
private final UpbitWebSocketClient upbitWebSocketClient;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
// 디바이스별 전역 설정이 켜져 있고 전략이 지정된 경우, DB 관심종목 전체 고정 구독
|
||||
int pinned = 0;
|
||||
for (GcWatchlist w : watchlistRepo.findAll()) {
|
||||
String deviceId = w.getDeviceId();
|
||||
if (deviceId == null) continue;
|
||||
GcAppSettings app = appSettingsService.getEntity(null, deviceId);
|
||||
if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) || app.getLiveStrategyId() == null) {
|
||||
continue;
|
||||
}
|
||||
subscriptionManager.ensureMarketPinned(w.getSymbol(), "1m");
|
||||
liveStrategyEvaluator.invalidateCache(w.getSymbol());
|
||||
pinned++;
|
||||
}
|
||||
if (pinned > 0) {
|
||||
log.info("[LiveStrategyStartup] 관심종목 기준 마켓 {}개 고정 구독 완료", pinned);
|
||||
}
|
||||
// WS 연결 직후 addMarket 이 구독 패킷을 놓치는 경우 대비
|
||||
upbitWebSocketClient.resubscribeAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
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.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http, CorsConfig corsConfig) throws Exception {
|
||||
http
|
||||
.cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 개발 단계: 전체 허용 (운영 시 JWT 필터 추가)
|
||||
.requestMatchers("/**").permitAll()
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import com.goldenchart.entity.GcAppSettings;
|
||||
import com.goldenchart.repository.GcAppSettingsRepository;
|
||||
import com.goldenchart.security.SecretCryptoService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 기존 평문 업비트 API 키를 AES 암호문으로 일괄 변환 (1회성·멱등).
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class UpbitKeyEncryptionMigrator implements ApplicationRunner {
|
||||
|
||||
private final GcAppSettingsRepository repo;
|
||||
private final SecretCryptoService secretCrypto;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void run(ApplicationArguments args) {
|
||||
int migrated = 0;
|
||||
for (GcAppSettings s : repo.findAll()) {
|
||||
boolean changed = false;
|
||||
String access = s.getUpbitAccessKey();
|
||||
if (access != null && !access.isBlank() && !secretCrypto.isEncrypted(access)) {
|
||||
s.setUpbitAccessKey(secretCrypto.encrypt(access));
|
||||
changed = true;
|
||||
}
|
||||
String secret = s.getUpbitSecretKey();
|
||||
if (secret != null && !secret.isBlank() && !secretCrypto.isEncrypted(secret)) {
|
||||
s.setUpbitSecretKey(secretCrypto.encrypt(secret));
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
repo.save(s);
|
||||
migrated++;
|
||||
}
|
||||
}
|
||||
if (migrated > 0) {
|
||||
log.info("[UpbitKeyEncryptionMigrator] {} settings row(s) API keys encrypted", migrated);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.goldenchart.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* STOMP WebSocket 브로커 설정.
|
||||
*
|
||||
* 클라이언트 연결 엔드포인트:
|
||||
* ws(s)://host/ws/trading (SockJS fallback 지원)
|
||||
*
|
||||
* 구독 채널 (명세서 5.2):
|
||||
* /sub/charts/{market}/{type} — 실시간 캔들 배포
|
||||
*
|
||||
* 발행 채널:
|
||||
* /pub/... — 클라이언트 → 서버 (향후 확장용)
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
// 인메모리 심플 브로커: /sub 접두사 구독 채널에 메시지 라우팅
|
||||
registry.enableSimpleBroker("/sub");
|
||||
// 클라이언트 발행 경로 접두사 (서버측 @MessageMapping 핸들러로 라우팅)
|
||||
registry.setApplicationDestinationPrefixes("/pub");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws/trading")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.withSockJS(); // SockJS fallback (브라우저 환경 대비)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user