diff --git a/api/src/main/java/com/whalewatch/WhaleWatchApplication.java b/api/src/main/java/com/whalewatch/WhaleWatchApplication.java index 54004d2..2122ded 100644 --- a/api/src/main/java/com/whalewatch/WhaleWatchApplication.java +++ b/api/src/main/java/com/whalewatch/WhaleWatchApplication.java @@ -18,4 +18,5 @@ public CommandLineRunner run(WebSocketManager webSocketManager) { webSocketManager.startAll(); }; } + } diff --git a/api/src/main/java/com/whalewatch/controller/UserController.java b/api/src/main/java/com/whalewatch/controller/UserController.java index 6b1a7a4..0c57b99 100644 --- a/api/src/main/java/com/whalewatch/controller/UserController.java +++ b/api/src/main/java/com/whalewatch/controller/UserController.java @@ -8,6 +8,8 @@ import com.whalewatch.service.UserService; import org.springframework.web.bind.annotation.*; +import java.util.Map; + @RestController @RequestMapping("/api/users") public class UserController { @@ -23,17 +25,20 @@ public UserController(UserService userService, this.jwtService = jwtService; } - @PostMapping - public UserDto registerUser(@RequestBody UserDto userDto) { - User entity = userMapper.toEntity(userDto); - User saved = userService.registerUser(entity); - return userMapper.toDto(saved); - } @PostMapping("/login") - public TokenResponseDto loginUser(@RequestBody UserDto userDto) { - // JwtService로 로그인 + 토큰 발급 - return jwtService.login(userDto.getEmail(),userDto.getPassword()); + public TokenResponseDto loginUser(@RequestBody Map request) { + String email = request.get("email"); + String otp = request.get("otp"); + return jwtService.login(email, otp); + } + + // 사용자의 이메일을 받아 OTP를 생성 후 텔레그램으로 전송 + @PostMapping("/request-otp") + public String requestOtp(@RequestBody Map request) { + String email = request.get("email"); + userService.requestLoginOtp(email); + return "OTP has been sent to Telegram."; } @PostMapping("/refresh") @@ -41,7 +46,7 @@ public TokenResponseDto refreshToken(@RequestBody TokenResponseDto tokenDto){ return jwtService.refreshAccessToken(tokenDto.getRefreshToken()); } - @GetMapping("{id}") + @GetMapping("/info/{id}") public UserDto getUserInfo(@PathVariable int id) { User user = userService.getUserInfo(id); return userMapper.toDto(user); diff --git a/application.yml b/application.yml index c81ca5e..0cc548d 100644 --- a/application.yml +++ b/application.yml @@ -71,3 +71,8 @@ exchanges: BTC: 0.8 ETH: 22.0 SOL: 300.0 + +telegram: + bot: + username: "${TELEGRAM_USERNAME}" + token: "${TELEGRAM_TOKEN}" \ No newline at end of file diff --git a/common/src/main/java/com/whalewatch/config/SecurityConfig.java b/common/src/main/java/com/whalewatch/config/SecurityConfig.java index 50ddd98..16536d0 100644 --- a/common/src/main/java/com/whalewatch/config/SecurityConfig.java +++ b/common/src/main/java/com/whalewatch/config/SecurityConfig.java @@ -39,7 +39,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // URL별 권한 설정 .authorizeHttpRequests(auth -> auth .requestMatchers("/h2-console/**").permitAll() - .requestMatchers("/api/users", "/api/users/login").permitAll() // 회원가입 및 로그인 허용 + .requestMatchers("/api/users/request-otp", "/api/users/login").permitAll() // 회원가입 및 로그인 허용 .requestMatchers("/api/users/**").authenticated() .anyRequest().authenticated() ) diff --git a/common/src/main/java/com/whalewatch/config/TelegramBotProperties.java b/common/src/main/java/com/whalewatch/config/TelegramBotProperties.java new file mode 100644 index 0000000..df80a8a --- /dev/null +++ b/common/src/main/java/com/whalewatch/config/TelegramBotProperties.java @@ -0,0 +1,24 @@ +package com.whalewatch.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "telegram.bot") +public class TelegramBotProperties { + private String username; + private String token; + + public String getUsername() { + return username; + } + public void setUsername(String username) { + this.username = username; + } + public String getToken() { + return token; + } + public void setToken(String token) { + this.token = token; + } +} diff --git a/common/src/main/java/com/whalewatch/dto/UserDto.java b/common/src/main/java/com/whalewatch/dto/UserDto.java index 914e3a7..106c475 100644 --- a/common/src/main/java/com/whalewatch/dto/UserDto.java +++ b/common/src/main/java/com/whalewatch/dto/UserDto.java @@ -4,13 +4,11 @@ public class UserDto { private int id; private String email; private String username; - private String password; - public UserDto(int id,String email, String username, String password) { + public UserDto(int id,String email, String username) { this.id = id; this.email = email; this.username = username; - this.password = password; } public void setId(int id) { @@ -25,10 +23,6 @@ public String getEmail() { return email; } - public String getPassword() { - return password; - } - public void setEmail(String email) { this.email = email; } @@ -41,7 +35,4 @@ public void setUsername(String username) { this.username = username; } - public void setPassword(String password) { - this.password = password; - } } diff --git a/service/build.gradle b/service/build.gradle index ad1455f..14e5e5a 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -20,4 +20,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.boot:spring-boot-starter-test' + + //telegramBot + implementation 'org.telegram:telegrambots-spring-boot-starter:6.9.7.1' + + //JAXB + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.1' } \ No newline at end of file diff --git a/service/src/main/java/com/whalewatch/domain/User.java b/service/src/main/java/com/whalewatch/domain/User.java index e2ef8ae..b28da40 100644 --- a/service/src/main/java/com/whalewatch/domain/User.java +++ b/service/src/main/java/com/whalewatch/domain/User.java @@ -11,14 +11,31 @@ public class User { private String email; private String username; - private String password; + + private Long telegramChatId; + private String otpHash; + + public Long getTelegramChatId() { + return telegramChatId; + } + + public void setTelegramChatId(Long telegramChatId) { + this.telegramChatId = telegramChatId; + } + + public String getOtpHash() { + return otpHash; + } + + public void setOtpHash(String otpHash) { + this.otpHash = otpHash; + } protected User() {} - public User(String email, String username, String password) { + public User(String email, String username) { this.email = email; this.username = username; - this.password = password; } public int getId() { @@ -33,10 +50,6 @@ public String getUsername() { return username; } - public String getPassword() { - return password; - } - public void setEmail(String email) { this.email = email; } @@ -45,7 +58,4 @@ public void setUsername(String username) { this.username = username; } - public void setPassword(String password) { - this.password = password; - } } diff --git a/service/src/main/java/com/whalewatch/mapper/UserMapper.java b/service/src/main/java/com/whalewatch/mapper/UserMapper.java index cfcb9ba..253f9dc 100644 --- a/service/src/main/java/com/whalewatch/mapper/UserMapper.java +++ b/service/src/main/java/com/whalewatch/mapper/UserMapper.java @@ -11,13 +11,11 @@ public interface UserMapper { @Mapping(target = "id", source = "id") @Mapping(target = "email", source = "email") @Mapping(target = "username", source = "username") - @Mapping(target = "password", source = "password") UserDto toDto(User entity); // DTO -> Entity @Mapping(target = "id", ignore = true) @Mapping(target = "email", source = "email") @Mapping(target = "username", source = "username") - @Mapping(target = "password", source = "password") User toEntity(UserDto dto); } diff --git a/service/src/main/java/com/whalewatch/repository/UserRepository.java b/service/src/main/java/com/whalewatch/repository/UserRepository.java index 2b8ca0e..ab2da03 100644 --- a/service/src/main/java/com/whalewatch/repository/UserRepository.java +++ b/service/src/main/java/com/whalewatch/repository/UserRepository.java @@ -7,6 +7,4 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); - - Optional findByEmailAndPassword(String email, String password); } diff --git a/service/src/main/java/com/whalewatch/service/JwtService.java b/service/src/main/java/com/whalewatch/service/JwtService.java index 5ce8f24..22ea08d 100644 --- a/service/src/main/java/com/whalewatch/service/JwtService.java +++ b/service/src/main/java/com/whalewatch/service/JwtService.java @@ -17,43 +17,34 @@ public class JwtService { private final JwtTokenRepository jwtTokenRepository; private final JwtTokenProvider tokenProvider; private final PasswordEncoder passwordEncoder; + private final UserService userService; public JwtService(UserRepository userRepository, - JwtTokenRepository jwtTokenRepository, - JwtTokenProvider tokenProvider, - PasswordEncoder passwordEncoder) { + JwtTokenRepository jwtTokenRepository, + JwtTokenProvider tokenProvider, + PasswordEncoder passwordEncoder, + UserService userService) { this.userRepository = userRepository; this.jwtTokenRepository = jwtTokenRepository; this.tokenProvider = tokenProvider; this.passwordEncoder = passwordEncoder; + this.userService = userService; } - public TokenResponseDto login(String email, String Password) { - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new RuntimeException("Invalid email or password")); - - if (!passwordEncoder.matches(Password, user.getPassword())) { - throw new RuntimeException("Invalid password"); - } - + public TokenResponseDto login(String email, String otp) { + User user = userService.loginWithOtp(email, otp); String accessToken = tokenProvider.generateAccessToken(user.getEmail()); String refreshToken = tokenProvider.generateRefreshToken(user.getEmail()); - - return new TokenResponseDto(accessToken, refreshToken); } public TokenResponseDto refreshAccessToken(String refreshToken) { - - // RefreshToken 자체가 유효한지 + // RefreshToken 유효성 검사 if (!tokenProvider.validateToken(refreshToken)) { throw new RuntimeException("Invalid or expired refresh token."); } - - // 새 Access Token 발급 String email = tokenProvider.getEmailFromToken(refreshToken); String newAccessToken = tokenProvider.generateAccessToken(email); - return new TokenResponseDto(newAccessToken, refreshToken); } } diff --git a/service/src/main/java/com/whalewatch/service/UserService.java b/service/src/main/java/com/whalewatch/service/UserService.java index d7c05fd..b362bd7 100644 --- a/service/src/main/java/com/whalewatch/service/UserService.java +++ b/service/src/main/java/com/whalewatch/service/UserService.java @@ -2,6 +2,10 @@ import com.whalewatch.domain.User; import com.whalewatch.repository.UserRepository; +import com.whalewatch.telegram.TelegramMessageEvent; +import com.whalewatch.telegram.TelegramUserBot; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Lazy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -9,21 +13,52 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final ApplicationEventPublisher eventPublisher; public UserService(UserRepository userRepository, - PasswordEncoder passwordEncoder) { + PasswordEncoder passwordEncoder, + ApplicationEventPublisher eventPublisher) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; + this.eventPublisher = eventPublisher; } public User registerUser(User user) { - String hashed = passwordEncoder.encode(user.getPassword()); - user.setPassword(hashed); - return userRepository.save(user); } public User getUserInfo(int id) { - return userRepository.findById(id).orElseThrow(() -> new RuntimeException("Not found")); + return userRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Not found")); + } + + // 이메일을 받아 OTP 생성 후, 해당 사용자의 otpHash 업데이트 및 텔레그램 메시지 전송 이벤트 발행 + public void requestLoginOtp(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + String otp = String.valueOf((int) ((Math.random() * 900000) + 100000)); + String otpHash = passwordEncoder.encode(otp); + user.setOtpHash(otpHash); + userRepository.save(user); + + if (user.getTelegramChatId() != null) { + eventPublisher.publishEvent(new TelegramMessageEvent(user.getTelegramChatId(), "Your login OTP: " + otp)); + } else { + throw new RuntimeException("User is not registered with Telegram"); + } + } + + // 입력받은 OTP가 저장된 otpHash와 일치하면 로그인 성공 처리 + public User loginWithOtp(String email, String otp) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + if (user.getOtpHash() != null && passwordEncoder.matches(otp, user.getOtpHash())) { + // OTP는 한 번 사용 후 삭제 + user.setOtpHash(null); + userRepository.save(user); + return user; + } else { + throw new RuntimeException("Invalid OTP"); + } } } diff --git a/service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java b/service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java new file mode 100644 index 0000000..47d1d55 --- /dev/null +++ b/service/src/main/java/com/whalewatch/telegram/TelegramBotConfig.java @@ -0,0 +1,17 @@ +package com.whalewatch.telegram; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.telegram.telegrambots.meta.TelegramBotsApi; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; +import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; + +@Configuration +public class TelegramBotConfig { + @Bean + public TelegramBotsApi telegramBotsApi(TelegramUserBot telegramUserBot) throws TelegramApiException { + TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class); + botsApi.registerBot(telegramUserBot); + return botsApi; + } +} diff --git a/service/src/main/java/com/whalewatch/telegram/TelegramMessageEvent.java b/service/src/main/java/com/whalewatch/telegram/TelegramMessageEvent.java new file mode 100644 index 0000000..3fd1d4c --- /dev/null +++ b/service/src/main/java/com/whalewatch/telegram/TelegramMessageEvent.java @@ -0,0 +1,20 @@ +package com.whalewatch.telegram; + +public class TelegramMessageEvent { + private final Long chatId; + private final String message; + + public TelegramMessageEvent(Long chatId, String message) { + this.chatId = chatId; + this.message = message; + } + + public Long getChatId() { + return chatId; + } + + public String getMessage() { + return message; + } +} + diff --git a/service/src/main/java/com/whalewatch/telegram/TelegramUserBot.java b/service/src/main/java/com/whalewatch/telegram/TelegramUserBot.java new file mode 100644 index 0000000..fa45b0a --- /dev/null +++ b/service/src/main/java/com/whalewatch/telegram/TelegramUserBot.java @@ -0,0 +1,96 @@ +package com.whalewatch.telegram; + +import com.whalewatch.config.TelegramBotProperties; +import com.whalewatch.domain.User; +import com.whalewatch.service.UserService; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.bots.TelegramLongPollingBot; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +import java.util.HashMap; +import java.util.Map; + +@Component +public class TelegramUserBot extends TelegramLongPollingBot { + + private final UserService userService; + private final TelegramBotProperties telegramBotProperties; + + private Map registrationDataMap = new HashMap<>(); + + public TelegramUserBot(UserService userService, TelegramBotProperties telegramBotProperties) { + this.userService = userService; + this.telegramBotProperties = telegramBotProperties; + } + + @Override + public String getBotUsername() { + return telegramBotProperties.getUsername(); + } + + @Override + public String getBotToken() { + return telegramBotProperties.getToken(); + } + + @Override + public void onUpdateReceived(Update update) { + if (update.hasMessage() && update.getMessage().hasText()) { + String messageText = update.getMessage().getText(); + Long chatId = update.getMessage().getChatId(); + + if (messageText.equalsIgnoreCase("/start")) { + registrationDataMap.put(chatId, new RegistrationData()); + sendTextMessage(chatId, "Welcome! Please enter your email to sign up."); + return; + } + + RegistrationData data = registrationDataMap.get(chatId); + if (data != null) { + if (data.getEmail() == null) { + data.setEmail(messageText.trim()); + sendTextMessage(chatId, "Email received. Now, please enter your username."); + } else if (data.getUsername() == null) { + data.setUsername(messageText.trim()); + User newUser = new User(data.getEmail(), data.getUsername()); + newUser.setTelegramChatId(chatId); + userService.registerUser(newUser); + sendTextMessage(chatId, "Registration completed! You can request an OTP to log in."); + registrationDataMap.remove(chatId); + } + return; + } + sendTextMessage(chatId, "Unrecognized command. Please type /start to begin registration."); + } + } + + public void sendTextMessage(Long chatId, String text) { + SendMessage message = new SendMessage(); + message.setChatId(chatId.toString()); + message.setText(text); + try { + execute(message); + } catch (TelegramApiException e) { + e.printStackTrace(); + } + } + + @EventListener + public void handleTelegramMessageEvent(TelegramMessageEvent event) { + sendTextMessage(event.getChatId(), event.getMessage()); + } + + // 내부 대화 상태 저장 클래스 + private static class RegistrationData { + private String email; + private String username; + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + } +}