diff --git a/pom.xml b/pom.xml index 590b802..e5093ae 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,13 @@ flyway-mysql + + + com.maxmind.geoip2 + geoip2 + 4.2.0 + + diff --git a/src/main/java/com/honey/honey/controller/AuthController.java b/src/main/java/com/honey/honey/controller/AuthController.java index cfe1e2a..3cedc89 100644 --- a/src/main/java/com/honey/honey/controller/AuthController.java +++ b/src/main/java/com/honey/honey/controller/AuthController.java @@ -2,17 +2,16 @@ package com.honey.honey.controller; import com.honey.honey.dto.CreateSessionRequest; import com.honey.honey.dto.CreateSessionResponse; -import com.honey.honey.model.User; -import com.honey.honey.repository.UserRepository; +import com.honey.honey.model.UserA; import com.honey.honey.service.SessionService; import com.honey.honey.service.TelegramAuthService; -import lombok.Data; +import com.honey.honey.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; +import jakarta.servlet.http.HttpServletRequest; import java.util.Map; @Slf4j @@ -23,33 +22,33 @@ public class AuthController { private final TelegramAuthService telegramAuthService; private final SessionService sessionService; - private final UserRepository userRepository; + private final UserService userService; /** * Creates a session by validating Telegram initData. * This is the only endpoint that accepts initData. + * Handles user registration/login and referral system. */ @PostMapping("/tma/session") - public CreateSessionResponse createSession(@RequestBody CreateSessionRequest request) { + public CreateSessionResponse createSession( + @RequestBody CreateSessionRequest request, + HttpServletRequest httpRequest) { String initData = request.getInitData(); if (initData == null || initData.isBlank()) { throw new IllegalArgumentException("initData is required"); } - // Validate Telegram initData signature - Map tgUser = telegramAuthService.validateAndParseInitData(initData); + // Validate Telegram initData signature and parse data + Map tgUserData = telegramAuthService.validateAndParseInitData(initData); - Long telegramId = ((Number) tgUser.get("id")).longValue(); - String username = (String) tgUser.get("username"); - - // Get or create user - User user = getOrCreateUser(telegramId, username); + // Get or create user (handles registration, login update, and referral system) + UserA user = userService.getOrCreateUser(tgUserData, httpRequest); // Create session String sessionId = sessionService.createSession(user); - log.info("Session created for userId={}, telegramId={}", user.getId(), telegramId); + log.info("Session created for userId={}, telegramId={}", user.getId(), user.getTelegramId()); return CreateSessionResponse.builder() .access_token(sessionId) @@ -76,33 +75,6 @@ public class AuthController { } } - /** - * Gets existing user or creates a new one. - */ - private User getOrCreateUser(Long telegramId, String username) { - var existingUser = userRepository.findByTelegramId(telegramId); - - if (existingUser.isPresent()) { - User user = existingUser.get(); - // Update username if it changed - if (username != null && !username.equals(user.getUsername())) { - user.setUsername(username); - userRepository.save(user); - } - return user; - } - - // Create new user - log.info("Creating new user for telegramId={}, username={}", telegramId, username); - return userRepository.save( - User.builder() - .telegramId(telegramId) - .username(username) - .createdAt(LocalDateTime.now()) - .build() - ); - } - /** * Extracts Bearer token from Authorization header. */ diff --git a/src/main/java/com/honey/honey/controller/UserController.java b/src/main/java/com/honey/honey/controller/UserController.java index fac05fa..c092f28 100644 --- a/src/main/java/com/honey/honey/controller/UserController.java +++ b/src/main/java/com/honey/honey/controller/UserController.java @@ -1,13 +1,14 @@ package com.honey.honey.controller; import com.honey.honey.dto.UserDto; -import com.honey.honey.model.User; +import com.honey.honey.model.UserA; import com.honey.honey.security.UserContext; +import com.honey.honey.service.UserService; +import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; @Slf4j @RestController @@ -15,14 +16,31 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class UserController { + private final UserService userService; + @GetMapping("/current") public UserDto getCurrentUser() { - User user = UserContext.get(); + UserA user = UserContext.get(); return UserDto.builder() .telegram_id(user.getTelegramId()) - .username(user.getUsername()) + .username(user.getTelegramName()) .build(); } + + /** + * Updates user's language code. + * Called when user changes language in app header. + */ + @PutMapping("/language") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateLanguage(@RequestBody UpdateLanguageRequest request) { + UserA user = UserContext.get(); + userService.updateLanguageCode(user.getId(), request.getLanguageCode()); + } + + @Data + public static class UpdateLanguageRequest { + private String languageCode; + } } - diff --git a/src/main/java/com/honey/honey/model/Session.java b/src/main/java/com/honey/honey/model/Session.java index 768519c..bc58daf 100644 --- a/src/main/java/com/honey/honey/model/Session.java +++ b/src/main/java/com/honey/honey/model/Session.java @@ -21,9 +21,8 @@ public class Session { @Column(name = "session_id_hash", unique = true, nullable = false, length = 255) private String sessionIdHash; - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "user_id", nullable = false) - private User user; + @Column(name = "user_id", nullable = false) + private Integer userId; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; diff --git a/src/main/java/com/honey/honey/model/User.java b/src/main/java/com/honey/honey/model/User.java deleted file mode 100644 index a75a6c9..0000000 --- a/src/main/java/com/honey/honey/model/User.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.honey.honey.model; - -import jakarta.persistence.*; -import lombok.*; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "users") -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class User { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "telegram_id", unique = true, nullable = false) - private Long telegramId; - - @Column(name = "username") - private String username; - - @Column(name = "created_at") - private LocalDateTime createdAt; - - @PrePersist - public void prePersist() { - if (createdAt == null) { - createdAt = LocalDateTime.now(); - } - } -} - diff --git a/src/main/java/com/honey/honey/model/UserA.java b/src/main/java/com/honey/honey/model/UserA.java new file mode 100644 index 0000000..24de1ba --- /dev/null +++ b/src/main/java/com/honey/honey/model/UserA.java @@ -0,0 +1,62 @@ +package com.honey.honey.model; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "db_users_a") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserA { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @Column(name = "screen_name", nullable = false, length = 75) + @Builder.Default + private String screenName = "-"; + + @Column(name = "telegram_id", unique = true) + private Long telegramId; + + @Column(name = "telegram_name", nullable = false, length = 33) + @Builder.Default + private String telegramName = "-"; + + @Column(name = "is_premium", nullable = false) + @Builder.Default + private Integer isPremium = 0; + + @Column(name = "language_code", nullable = false, length = 2) + @Builder.Default + private String languageCode = "XX"; + + @Column(name = "country_code", nullable = false, length = 2) + @Builder.Default + private String countryCode = "XX"; + + @Column(name = "device_code", nullable = false, length = 5) + @Builder.Default + private String deviceCode = "XX"; + + @Column(name = "ip", columnDefinition = "VARBINARY(16)") + private byte[] ip; + + @Column(name = "date_reg", nullable = false) + @Builder.Default + private Integer dateReg = 0; + + @Column(name = "date_login", nullable = false) + @Builder.Default + private Integer dateLogin = 0; + + @Column(name = "banned", nullable = false) + @Builder.Default + private Integer banned = 0; +} + diff --git a/src/main/java/com/honey/honey/model/UserB.java b/src/main/java/com/honey/honey/model/UserB.java new file mode 100644 index 0000000..cfea42f --- /dev/null +++ b/src/main/java/com/honey/honey/model/UserB.java @@ -0,0 +1,43 @@ +package com.honey.honey.model; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "db_users_b") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserB { + + @Id + @Column(name = "id") + private Integer id; + + @Column(name = "balance_a", nullable = false) + @Builder.Default + private Long balanceA = 0L; + + @Column(name = "balance_b", nullable = false) + @Builder.Default + private Long balanceB = 0L; + + @Column(name = "deposit_total", nullable = false) + @Builder.Default + private Long depositTotal = 0L; + + @Column(name = "deposit_count", nullable = false) + @Builder.Default + private Integer depositCount = 0; + + @Column(name = "withdraw_total", nullable = false) + @Builder.Default + private Long withdrawTotal = 0L; + + @Column(name = "withdraw_count", nullable = false) + @Builder.Default + private Integer withdrawCount = 0; +} + diff --git a/src/main/java/com/honey/honey/model/UserD.java b/src/main/java/com/honey/honey/model/UserD.java new file mode 100644 index 0000000..ace3413 --- /dev/null +++ b/src/main/java/com/honey/honey/model/UserD.java @@ -0,0 +1,103 @@ +package com.honey.honey.model; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "db_users_d") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserD { + + @Id + @Column(name = "id") + private Integer id; + + @Column(name = "referer_id_1", nullable = false) + @Builder.Default + private Integer refererId1 = 0; + + @Column(name = "referer_id_2", nullable = false) + @Builder.Default + private Integer refererId2 = 0; + + @Column(name = "referer_id_3", nullable = false) + @Builder.Default + private Integer refererId3 = 0; + + @Column(name = "referer_id_4", nullable = false) + @Builder.Default + private Integer refererId4 = 0; + + @Column(name = "referer_id_5", nullable = false) + @Builder.Default + private Integer refererId5 = 0; + + @Column(name = "master_id", nullable = false) + @Builder.Default + private Integer masterId = 0; + + @Column(name = "referals_1", nullable = false) + @Builder.Default + private Integer referals1 = 0; + + @Column(name = "referals_2", nullable = false) + @Builder.Default + private Integer referals2 = 0; + + @Column(name = "referals_3", nullable = false) + @Builder.Default + private Integer referals3 = 0; + + @Column(name = "referals_4", nullable = false) + @Builder.Default + private Integer referals4 = 0; + + @Column(name = "referals_5", nullable = false) + @Builder.Default + private Integer referals5 = 0; + + @Column(name = "from_referals_1", nullable = false) + @Builder.Default + private Long fromReferals1 = 0L; + + @Column(name = "from_referals_2", nullable = false) + @Builder.Default + private Long fromReferals2 = 0L; + + @Column(name = "from_referals_3", nullable = false) + @Builder.Default + private Long fromReferals3 = 0L; + + @Column(name = "from_referals_4", nullable = false) + @Builder.Default + private Long fromReferals4 = 0L; + + @Column(name = "from_referals_5", nullable = false) + @Builder.Default + private Long fromReferals5 = 0L; + + @Column(name = "to_referer_1", nullable = false) + @Builder.Default + private Long toReferer1 = 0L; + + @Column(name = "to_referer_2", nullable = false) + @Builder.Default + private Long toReferer2 = 0L; + + @Column(name = "to_referer_3", nullable = false) + @Builder.Default + private Long toReferer3 = 0L; + + @Column(name = "to_referer_4", nullable = false) + @Builder.Default + private Long toReferer4 = 0L; + + @Column(name = "to_referer_5", nullable = false) + @Builder.Default + private Long toReferer5 = 0L; +} + diff --git a/src/main/java/com/honey/honey/repository/SessionRepository.java b/src/main/java/com/honey/honey/repository/SessionRepository.java index 8939053..b77807c 100644 --- a/src/main/java/com/honey/honey/repository/SessionRepository.java +++ b/src/main/java/com/honey/honey/repository/SessionRepository.java @@ -19,15 +19,15 @@ public interface SessionRepository extends JpaRepository { /** * Counts active (non-expired) sessions for a user. */ - @Query("SELECT COUNT(s) FROM Session s WHERE s.user.id = :userId AND s.expiresAt > :now") - long countActiveSessionsByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now); + @Query("SELECT COUNT(s) FROM Session s WHERE s.userId = :userId AND s.expiresAt > :now") + long countActiveSessionsByUserId(@Param("userId") Integer userId, @Param("now") LocalDateTime now); /** * Finds oldest active sessions for a user, ordered by created_at ASC. * Used to delete oldest sessions when max limit is exceeded. */ - @Query("SELECT s FROM Session s WHERE s.user.id = :userId AND s.expiresAt > :now ORDER BY s.createdAt ASC") - List findOldestActiveSessionsByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now, Pageable pageable); + @Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.expiresAt > :now ORDER BY s.createdAt ASC") + List findOldestActiveSessionsByUserId(@Param("userId") Integer userId, @Param("now") LocalDateTime now, Pageable pageable); /** * Batch deletes expired sessions (up to batchSize). diff --git a/src/main/java/com/honey/honey/repository/UserARepository.java b/src/main/java/com/honey/honey/repository/UserARepository.java new file mode 100644 index 0000000..0e72c55 --- /dev/null +++ b/src/main/java/com/honey/honey/repository/UserARepository.java @@ -0,0 +1,13 @@ +package com.honey.honey.repository; + +import com.honey.honey.model.UserA; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserARepository extends JpaRepository { + Optional findByTelegramId(Long telegramId); +} + diff --git a/src/main/java/com/honey/honey/repository/UserBRepository.java b/src/main/java/com/honey/honey/repository/UserBRepository.java new file mode 100644 index 0000000..85482be --- /dev/null +++ b/src/main/java/com/honey/honey/repository/UserBRepository.java @@ -0,0 +1,10 @@ +package com.honey.honey.repository; + +import com.honey.honey.model.UserB; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserBRepository extends JpaRepository { +} + diff --git a/src/main/java/com/honey/honey/repository/UserDRepository.java b/src/main/java/com/honey/honey/repository/UserDRepository.java new file mode 100644 index 0000000..d726f60 --- /dev/null +++ b/src/main/java/com/honey/honey/repository/UserDRepository.java @@ -0,0 +1,48 @@ +package com.honey.honey.repository; + +import com.honey.honey.model.UserD; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserDRepository extends JpaRepository { + + /** + * Increments referals_1 for a user. + */ + @Modifying + @Query("UPDATE UserD u SET u.referals1 = u.referals1 + 1 WHERE u.id = :userId") + void incrementReferals1(@Param("userId") Integer userId); + + /** + * Increments referals_2 for a user. + */ + @Modifying + @Query("UPDATE UserD u SET u.referals2 = u.referals2 + 1 WHERE u.id = :userId") + void incrementReferals2(@Param("userId") Integer userId); + + /** + * Increments referals_3 for a user. + */ + @Modifying + @Query("UPDATE UserD u SET u.referals3 = u.referals3 + 1 WHERE u.id = :userId") + void incrementReferals3(@Param("userId") Integer userId); + + /** + * Increments referals_4 for a user. + */ + @Modifying + @Query("UPDATE UserD u SET u.referals4 = u.referals4 + 1 WHERE u.id = :userId") + void incrementReferals4(@Param("userId") Integer userId); + + /** + * Increments referals_5 for a user. + */ + @Modifying + @Query("UPDATE UserD u SET u.referals5 = u.referals5 + 1 WHERE u.id = :userId") + void incrementReferals5(@Param("userId") Integer userId); +} + diff --git a/src/main/java/com/honey/honey/repository/UserRepository.java b/src/main/java/com/honey/honey/repository/UserRepository.java deleted file mode 100644 index c6df6e4..0000000 --- a/src/main/java/com/honey/honey/repository/UserRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.honey.honey.repository; - -import com.honey.honey.model.User; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface UserRepository extends JpaRepository { - List findAllByTelegramId(Long telegramId); - Optional findByTelegramId(Long telegramId); -} - diff --git a/src/main/java/com/honey/honey/security/AuthInterceptor.java b/src/main/java/com/honey/honey/security/AuthInterceptor.java index 13c4e86..c43ae02 100644 --- a/src/main/java/com/honey/honey/security/AuthInterceptor.java +++ b/src/main/java/com/honey/honey/security/AuthInterceptor.java @@ -1,6 +1,6 @@ package com.honey.honey.security; -import com.honey.honey.model.User; +import com.honey.honey.model.UserA; import com.honey.honey.service.SessionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -38,7 +38,7 @@ public class AuthInterceptor implements HandlerInterceptor { } // Validate session and get user - Optional userOpt = sessionService.getUserBySession(sessionId); + Optional userOpt = sessionService.getUserBySession(sessionId); if (userOpt.isEmpty()) { log.warn("❌ Invalid or expired session: {}", maskSessionId(sessionId)); @@ -47,7 +47,7 @@ public class AuthInterceptor implements HandlerInterceptor { } // Put user in context - User user = userOpt.get(); + UserA user = userOpt.get(); UserContext.set(user); log.debug("🔑 Authenticated userId={} via session", user.getId()); diff --git a/src/main/java/com/honey/honey/security/UserContext.java b/src/main/java/com/honey/honey/security/UserContext.java index 33f6002..90b9f4f 100644 --- a/src/main/java/com/honey/honey/security/UserContext.java +++ b/src/main/java/com/honey/honey/security/UserContext.java @@ -1,16 +1,16 @@ package com.honey.honey.security; -import com.honey.honey.model.User; +import com.honey.honey.model.UserA; public class UserContext { - private static final ThreadLocal current = new ThreadLocal<>(); + private static final ThreadLocal current = new ThreadLocal<>(); - public static void set(User user) { + public static void set(UserA user) { current.set(user); } - public static User get() { + public static UserA get() { return current.get(); } diff --git a/src/main/java/com/honey/honey/service/CountryCodeService.java b/src/main/java/com/honey/honey/service/CountryCodeService.java new file mode 100644 index 0000000..f694aa9 --- /dev/null +++ b/src/main/java/com/honey/honey/service/CountryCodeService.java @@ -0,0 +1,156 @@ +package com.honey.honey.service; + +import com.maxmind.db.CHMCache; +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CountryResponse; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; + +/** + * Service for determining country code from IP address using MaxMind GeoLite2 database. + * Thread-safe singleton that maintains a single DatabaseReader instance. + */ +@Slf4j +@Service +public class CountryCodeService { + + private final String dbPath; + private final ResourceLoader resourceLoader; + private DatabaseReader reader; + + public CountryCodeService( + @Value("${geoip.db-path:}") String dbPath, + ResourceLoader resourceLoader) { + this.dbPath = dbPath; + this.resourceLoader = resourceLoader; + } + + @PostConstruct + public void init() throws IOException { + File dbFile = null; + + // Try external file path first (from config/env) + if (dbPath != null && !dbPath.isBlank()) { + dbFile = new File(dbPath); + if (dbFile.exists() && dbFile.isFile()) { + log.info("Loading GeoIP database from external path: {}", dbFile.getAbsolutePath()); + } else { + log.warn("GeoIP database file not found at configured path: {}, falling back to resources", dbFile.getAbsolutePath()); + dbFile = null; + } + } + + // Fallback to resources directory + if (dbFile == null) { + try { + Resource resource = resourceLoader.getResource("classpath:geoip/GeoLite2-Country.mmdb"); + if (resource.exists() && resource.isReadable()) { + // Try to get file path first (works if resource is a file system resource) + try { + dbFile = resource.getFile(); + log.info("Loading GeoIP database from resources: {}", dbFile.getAbsolutePath()); + } catch (Exception e) { + // Resource is in JAR, need to extract to temp file + log.debug("GeoIP database is in JAR, extracting to temp file"); + InputStream inputStream = resource.getInputStream(); + File tempFile = File.createTempFile("GeoLite2-Country", ".mmdb"); + tempFile.deleteOnExit(); + + try (inputStream) { + java.nio.file.Files.copy(inputStream, tempFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + + dbFile = tempFile; + log.info("Loading GeoIP database from JAR resources (extracted to temp file)"); + } + } else { + throw new IllegalStateException("GeoIP database not found in resources: classpath:geoip/GeoLite2-Country.mmdb"); + } + } catch (Exception e) { + throw new IllegalStateException("Failed to load GeoIP database from resources: " + e.getMessage(), e); + } + } + + // Initialize DatabaseReader with cache for better performance + try { + this.reader = new DatabaseReader.Builder(dbFile) + .withCache(new CHMCache()) + .build(); + log.info("GeoIP database loaded successfully"); + } catch (IOException e) { + log.error("Failed to initialize GeoIP database reader", e); + throw new IllegalStateException("Failed to initialize GeoIP database reader", e); + } + } + + @PreDestroy + public void destroy() { + if (reader != null) { + try { + reader.close(); + log.info("GeoIP database reader closed"); + } catch (IOException e) { + log.warn("Error closing GeoIP database reader", e); + } + } + } + + /** + * Determines country code from IP address using MaxMind GeoLite2 database. + * + * @param ipAddress IP address as string (IPv4 or IPv6) + * @return ISO 3166-1 alpha-2 country code (e.g., "UA", "PL", "DE"), or "XX" if unknown/invalid/private + */ + public String getCountryCode(String ipAddress) { + if (ipAddress == null || ipAddress.isBlank()) { + return "XX"; + } + + if (reader == null) { + log.warn("GeoIP database reader not initialized, returning 'XX' for IP: {}", ipAddress); + return "XX"; + } + + try { + InetAddress addr = InetAddress.getByName(ipAddress); + + // Check if it's a private/local IP (won't be in GeoIP database) + if (addr.isLoopbackAddress() || addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) { + log.debug("Private/local IP address detected: {}, returning 'XX'", ipAddress); + return "XX"; + } + + CountryResponse response = reader.country(addr); + String iso = response.getCountry().getIsoCode(); + + if (iso != null && iso.length() == 2) { + return iso.toUpperCase(); // Ensure uppercase (ISO codes should be uppercase) + } else { + log.debug("Invalid ISO code returned for IP {}: {}", ipAddress, iso); + return "XX"; + } + } catch (IOException e) { + log.debug("Failed to resolve IP address: {}", ipAddress, e); + return "XX"; + } catch (GeoIp2Exception e) { + log.debug("GeoIP lookup failed for IP {}: {}", ipAddress, e.getMessage()); + return "XX"; + } catch (IllegalArgumentException e) { + log.debug("Invalid IP address format: {}", ipAddress, e); + return "XX"; + } + } + +} + diff --git a/src/main/java/com/honey/honey/service/SessionService.java b/src/main/java/com/honey/honey/service/SessionService.java index c23572b..c436443 100644 --- a/src/main/java/com/honey/honey/service/SessionService.java +++ b/src/main/java/com/honey/honey/service/SessionService.java @@ -1,8 +1,10 @@ package com.honey.honey.service; import com.honey.honey.model.Session; -import com.honey.honey.model.User; +import com.honey.honey.model.UserA; import com.honey.honey.repository.SessionRepository; +import com.honey.honey.repository.UserARepository; +import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -24,9 +26,15 @@ import java.util.Optional; public class SessionService { private final SessionRepository sessionRepository; + private final UserARepository userARepository; private static final int SESSION_TTL_HOURS = 24; // 1 day private static final SecureRandom secureRandom = new SecureRandom(); - + + /** + * -- GETTER -- + * Gets max active sessions per user. + */ + @Getter @Value("${app.session.max-active-per-user:5}") private int maxActiveSessionsPerUser; @@ -36,7 +44,7 @@ public class SessionService { * Returns the raw session ID (to be sent to frontend) and stores the hash in DB. */ @Transactional - public String createSession(User user) { + public String createSession(UserA user) { LocalDateTime now = LocalDateTime.now(); // Generate cryptographically random session ID @@ -56,7 +64,7 @@ public class SessionService { // Create and save session Session session = Session.builder() .sessionIdHash(sessionIdHash) - .user(user) + .userId(user.getId()) .createdAt(now) .expiresAt(expiresAt) .build(); @@ -70,7 +78,7 @@ public class SessionService { /** * Enforces MAX_ACTIVE_SESSIONS_PER_USER by deleting oldest active sessions if limit exceeded. */ - private void enforceMaxActiveSessions(Long userId, LocalDateTime now) { + private void enforceMaxActiveSessions(Integer userId, LocalDateTime now) { long activeCount = sessionRepository.countActiveSessionsByUserId(userId, now); if (activeCount >= maxActiveSessionsPerUser) { @@ -98,7 +106,7 @@ public class SessionService { * Returns empty if session is invalid or expired. */ @Transactional(readOnly = true) - public Optional getUserBySession(String sessionId) { + public Optional getUserBySession(String sessionId) { if (sessionId == null || sessionId.isBlank()) { return Optional.empty(); } @@ -120,12 +128,8 @@ public class SessionService { return Optional.empty(); } - // Access user properties while still in transaction to initialize lazy proxy - User user = session.getUser(); - // Force initialization by accessing a property - user.getTelegramId(); - - return Optional.of(user); + // Load user by ID + return userARepository.findById(session.getUserId()); } /** @@ -172,12 +176,6 @@ public class SessionService { public int getSessionTtlSeconds() { return SESSION_TTL_HOURS * 3600; } - - /** - * Gets max active sessions per user. - */ - public int getMaxActiveSessionsPerUser() { - return maxActiveSessionsPerUser; - } + } diff --git a/src/main/java/com/honey/honey/service/TelegramAuthService.java b/src/main/java/com/honey/honey/service/TelegramAuthService.java index cb1426c..d45d8e7 100644 --- a/src/main/java/com/honey/honey/service/TelegramAuthService.java +++ b/src/main/java/com/honey/honey/service/TelegramAuthService.java @@ -30,6 +30,9 @@ public class TelegramAuthService { /** * Validates and parses Telegram initData string. + * Returns a map containing: + * - "user": parsed user data (Map) + * - "start": referral start parameter from URL (e.g., "774876" from /honey?start=774876) (String, can be null) */ public Map validateAndParseInitData(String initData) { @@ -60,16 +63,21 @@ public class TelegramAuthService { throw new UnauthorizedException("Invalid Telegram signature"); } - // Step 5. Extract the user JSON from initData. + // Step 5. Extract the user JSON and start parameter from initData. Map decoded = decodeQueryParams(initData); String userJson = decoded.get("user"); + String start = decoded.get("start"); // Referral parameter from URL: /honey?start=774876 if (userJson == null) { throw new UnauthorizedException("initData does not contain 'user' field"); } - // Step 6. Parse JSON into map. - return objectMapper.readValue(userJson, Map.class); + // Step 6. Parse JSON into map and add start parameter. + Map result = new HashMap<>(); + result.put("user", objectMapper.readValue(userJson, Map.class)); + result.put("start", start); + + return result; } catch (UnauthorizedException ex) { throw ex; diff --git a/src/main/java/com/honey/honey/service/UserService.java b/src/main/java/com/honey/honey/service/UserService.java new file mode 100644 index 0000000..59d23aa --- /dev/null +++ b/src/main/java/com/honey/honey/service/UserService.java @@ -0,0 +1,317 @@ +package com.honey.honey.service; + +import com.honey.honey.model.UserA; +import com.honey.honey.model.UserB; +import com.honey.honey.model.UserD; +import com.honey.honey.repository.UserARepository; +import com.honey.honey.repository.UserBRepository; +import com.honey.honey.repository.UserDRepository; +import com.honey.honey.util.IpUtils; +import com.honey.honey.util.TimeProvider; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Map; +import java.util.Optional; + +/** + * Service for user management with sharded tables. + * Handles registration, login, and referral system. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserARepository userARepository; + private final UserBRepository userBRepository; + private final UserDRepository userDRepository; + private final CountryCodeService countryCodeService; + + /** + * Gets or creates a user based on Telegram initData. + * Updates user data on each login. + * Handles referral system if start parameter is present. + * + * @param tgUserData Parsed Telegram data from initData (contains "user" map and "start" string) + * @param request HTTP request for IP extraction + * @return UserA entity + */ + @Transactional + public UserA getOrCreateUser(Map tgUserData, HttpServletRequest request) { + // Extract user data and start parameter (from URL: /honey?start=774876) + @SuppressWarnings("unchecked") + Map tgUser = (Map) tgUserData.get("user"); + String start = (String) tgUserData.get("start"); + Long telegramId = ((Number) tgUser.get("id")).longValue(); + String firstName = (String) tgUser.get("first_name"); + String lastName = (String) tgUser.get("last_name"); + String username = (String) tgUser.get("username"); + Boolean isPremium = (Boolean) tgUser.get("is_premium"); + String languageCode = (String) tgUser.get("language_code"); + + // Build screen_name from first_name and last_name + String screenName = buildScreenName(firstName, lastName); + + // device_code should be language_code from initData + String deviceCode = languageCode != null ? languageCode : "XX"; + + // Get client IP and convert to bytes + String clientIp = IpUtils.getClientIp(request); + byte[] ipBytes = IpUtils.ipToBytes(clientIp); + + // Get country code from IP + String countryCode = countryCodeService.getCountryCode(clientIp); + + // Get current timestamp + long nowSeconds = TimeProvider.nowSeconds(); + + // Check if user exists + Optional existingUserOpt = userARepository.findByTelegramId(telegramId); + + if (existingUserOpt.isPresent()) { + // User exists - update login data + UserA userA = existingUserOpt.get(); + updateUserOnLogin(userA, screenName, username, isPremium, languageCode, countryCode, deviceCode, ipBytes, nowSeconds); + return userA; + } else { + // New user - create in all 3 tables + return createNewUser(telegramId, screenName, username, isPremium, languageCode, countryCode, deviceCode, ipBytes, nowSeconds, start); + } + } + + /** + * Updates user data on login (when session is created). + * Note: language_code is NOT updated here - it should be updated via separate endpoint when user changes language in app. + */ + private void updateUserOnLogin(UserA userA, String screenName, String username, Boolean isPremium, + String languageCode, String countryCode, String deviceCode, + byte[] ipBytes, long nowSeconds) { + userA.setScreenName(screenName); + userA.setTelegramName(username != null ? username : "-"); + userA.setIsPremium(isPremium != null && isPremium ? 1 : 0); + userA.setCountryCode(countryCode); + userA.setDeviceCode(deviceCode != null ? deviceCode : "XX"); + userA.setIp(ipBytes); + userA.setDateLogin((int) nowSeconds); + // language_code is NOT updated here - it's updated via separate endpoint when user changes language + + userARepository.save(userA); + log.debug("Updated user data on login: userId={}", userA.getId()); + } + + /** + * Updates user's language code (called when user changes language in app header). + */ + @Transactional + public void updateLanguageCode(Integer userId, String languageCode) { + Optional userOpt = userARepository.findById(userId); + if (userOpt.isPresent()) { + UserA user = userOpt.get(); + user.setLanguageCode(languageCode != null && languageCode.length() == 2 ? languageCode.toUpperCase() : "XX"); + userARepository.save(user); + log.debug("Updated language_code for userId={}: {}", userId, user.getLanguageCode()); + } + } + + /** + * Creates a new user in all 3 tables with referral handling. + */ + private UserA createNewUser(Long telegramId, String screenName, String username, Boolean isPremium, + String languageCode, String countryCode, String deviceCode, + byte[] ipBytes, long nowSeconds, String start) { + + // Create UserA + UserA userA = UserA.builder() + .screenName(screenName) + .telegramId(telegramId) + .telegramName(username != null ? username : "-") + .isPremium(isPremium != null && isPremium ? 1 : 0) + .languageCode(languageCode != null ? languageCode : "XX") + .countryCode(countryCode) + .deviceCode(deviceCode != null ? deviceCode : "XX") + .ip(ipBytes) + .dateReg((int) nowSeconds) + .dateLogin((int) nowSeconds) + .banned(0) + .build(); + + userA = userARepository.save(userA); + Integer userId = userA.getId(); + + log.info("Created new user: userId={}, telegramId={}", userId, telegramId); + + // Create UserB with same ID + UserB userB = UserB.builder() + .id(userId) + .balanceA(0L) + .balanceB(0L) + .depositTotal(0L) + .depositCount(0) + .withdrawTotal(0L) + .withdrawCount(0) + .build(); + userBRepository.save(userB); + + // Create UserD with referral handling + UserD userD = createUserDWithReferral(userId, start); + userDRepository.save(userD); + + return userA; + } + + /** + * Creates UserD entity with referral chain setup. + * @param userId New user's ID + * @param start Referral parameter from URL (e.g., "774876" from /honey?start=774876) + */ + private UserD createUserDWithReferral(Integer userId, String start) { + UserD.UserDBuilder builder = UserD.builder() + .id(userId) + .refererId1(0) + .refererId2(0) + .refererId3(0) + .refererId4(0) + .refererId5(0) + .masterId(1) // Default master_id = 1 + .referals1(0) + .referals2(0) + .referals3(0) + .referals4(0) + .referals5(0) + .fromReferals1(0L) + .fromReferals2(0L) + .fromReferals3(0L) + .fromReferals4(0L) + .fromReferals5(0L) + .toReferer1(0L) + .toReferer2(0L) + .toReferer3(0L) + .toReferer4(0L) + .toReferer5(0L); + + if (start != null && !start.isEmpty()) { + try { + Integer refererId = Integer.parseInt(start); + Optional refererUserDOpt = userDRepository.findById(refererId); + + if (refererUserDOpt.isPresent()) { + UserD refererUserD = refererUserDOpt.get(); + + // Set referral chain: shift referer's chain down by 1 level + builder.refererId1(refererId) + .masterId(refererUserD.getMasterId()) + .refererId2(refererUserD.getRefererId1()) + .refererId3(refererUserD.getRefererId2()) + .refererId4(refererUserD.getRefererId3()) + .refererId5(refererUserD.getRefererId4()); + + // Increment referal counts for all 5 levels up the chain + setupReferralChain(userId, refererId); + } else { + // Referer doesn't exist, just set referer_id_1 + log.warn("Referer with id {} not found, setting only referer_id_1", refererId); + builder.refererId1(refererId); + } + } catch (NumberFormatException e) { + log.warn("Invalid start parameter format: {}", start); + } + } + + return builder.build(); + } + + /** + * Sets up referral chain and increments referal counts for all 5 levels. + * Example: If user F registers with referer E, increments: + * - referals_1 for E + * - referals_2 for D (E's referer_id_1) + * - referals_3 for C (D's referer_id_1) + * - referals_4 for B (C's referer_id_1) + * - referals_5 for A (B's referer_id_1) + */ + private void setupReferralChain(Integer newUserId, Integer refererId) { + // Level 1: Direct referer + userDRepository.incrementReferals1(refererId); + + Optional level1Opt = userDRepository.findById(refererId); + if (level1Opt.isEmpty()) { + return; + } + + UserD level1 = level1Opt.get(); + + // Level 2 + if (level1.getRefererId1() > 0) { + userDRepository.incrementReferals2(level1.getRefererId1()); + + Optional level2Opt = userDRepository.findById(level1.getRefererId1()); + if (level2Opt.isPresent()) { + UserD level2 = level2Opt.get(); + + // Level 3 + if (level2.getRefererId1() > 0) { + userDRepository.incrementReferals3(level2.getRefererId1()); + + Optional level3Opt = userDRepository.findById(level2.getRefererId1()); + if (level3Opt.isPresent()) { + UserD level3 = level3Opt.get(); + + // Level 4 + if (level3.getRefererId1() > 0) { + userDRepository.incrementReferals4(level3.getRefererId1()); + + Optional level4Opt = userDRepository.findById(level3.getRefererId1()); + if (level4Opt.isPresent()) { + UserD level4 = level4Opt.get(); + + // Level 5 + if (level4.getRefererId1() > 0) { + userDRepository.incrementReferals5(level4.getRefererId1()); + } + } + } + } + } + } + } + + log.info("Referral chain setup completed: newUserId={}, refererId={}", newUserId, refererId); + } + + /** + * Builds screen_name from first_name and last_name. + */ + private String buildScreenName(String firstName, String lastName) { + StringBuilder sb = new StringBuilder(); + if (firstName != null && !firstName.isEmpty()) { + sb.append(firstName); + } + if (lastName != null && !lastName.isEmpty()) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(lastName); + } + String result = sb.toString().trim(); + return result.isEmpty() ? "-" : (result.length() > 75 ? result.substring(0, 75) : result); + } + + /** + * Gets user by ID. + */ + public Optional getUserById(Integer userId) { + return userARepository.findById(userId); + } + + /** + * Gets user by Telegram ID. + */ + public Optional getUserByTelegramId(Long telegramId) { + return userARepository.findByTelegramId(telegramId); + } +} + diff --git a/src/main/java/com/honey/honey/util/IpUtils.java b/src/main/java/com/honey/honey/util/IpUtils.java new file mode 100644 index 0000000..4dc06e9 --- /dev/null +++ b/src/main/java/com/honey/honey/util/IpUtils.java @@ -0,0 +1,177 @@ +package com.honey.honey.util; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * Utility for IP address handling with reverse proxy support. + */ +@Slf4j +public class IpUtils { + + /** + * Extracts client IP address from request, handling reverse proxies. + * Priority: + * 1. X-Forwarded-For header (first IP in chain) + * 2. Forwarded header (RFC 7239) + * 3. X-Real-IP header + * 4. Remote address (fallback) + * + * @param request HTTP request + * @return Client IP address as string (normalized), or null if not available + */ + public static String getClientIp(HttpServletRequest request) { + // 1) Standard proxy header (most common) + String xff = request.getHeader("X-Forwarded-For"); + if (xff != null && !xff.isBlank() && !"unknown".equalsIgnoreCase(xff)) { + // Format: "client, proxy1, proxy2" - take first IP + String first = xff.split(",")[0].trim(); + if (!first.isEmpty()) { + return normalize(first); + } + } + + // 2) RFC 7239 Forwarded header (optional, more complex) + String forwarded = request.getHeader("Forwarded"); + if (forwarded != null && !forwarded.isBlank()) { + String ip = extractFromForwardedHeader(forwarded); + if (ip != null) { + return normalize(ip); + } + } + + // 3) X-Real-IP header (some proxies use this) + String xRealIp = request.getHeader("X-Real-IP"); + if (xRealIp != null && !xRealIp.isBlank() && !"unknown".equalsIgnoreCase(xRealIp)) { + return normalize(xRealIp); + } + + // 4) Fallback to remote address + return normalize(request.getRemoteAddr()); + } + + /** + * Extracts IP from RFC 7239 Forwarded header. + * Format: "for=192.0.2.60;proto=http;by=203.0.113.43" + */ + private static String extractFromForwardedHeader(String forwarded) { + try { + // Look for "for=" pattern + int forIndex = forwarded.toLowerCase().indexOf("for="); + if (forIndex == -1) { + return null; + } + + // Extract value after "for=" + String afterFor = forwarded.substring(forIndex + 4); + // Value can be quoted or unquoted, and may contain port + // Stop at semicolon, comma, quote, or space + StringBuilder ipBuilder = new StringBuilder(); + for (int i = 0; i < afterFor.length(); i++) { + char c = afterFor.charAt(i); + if (c == ';' || c == ',' || c == ' ' || c == '"') { + break; + } + ipBuilder.append(c); + } + + String candidate = ipBuilder.toString().trim(); + return candidate.isEmpty() ? null : candidate; + } catch (Exception e) { + log.debug("Failed to parse Forwarded header: {}", forwarded, e); + return null; + } + } + + /** + * Normalizes IP address string: + * - Removes brackets from IPv6 (e.g., "[2001:db8::1]" -> "2001:db8::1") + * - Removes port number if present (e.g., "192.168.1.1:8080" -> "192.168.1.1") + * - Trims whitespace + * + * @param ip IP address string (may contain brackets, port, etc.) + * @return Normalized IP address, or null if input is null + */ + private static String normalize(String ip) { + if (ip == null) { + return null; + } + + ip = ip.trim(); + if (ip.isEmpty()) { + return null; + } + + // Handle IPv6 in brackets, e.g., "[2001:db8::1]" + if (ip.startsWith("[") && ip.endsWith("]")) { + ip = ip.substring(1, ip.length() - 1); + } + + // Handle "ip:port" formats + // For IPv4: "192.168.1.1:8080" -> "192.168.1.1" + // For IPv6: "2001:db8::1:8080" is ambiguous, but we try to detect port + int lastColon = ip.lastIndexOf(':'); + if (lastColon > 0) { + // Check if part after last colon looks like a port number + String afterColon = ip.substring(lastColon + 1); + if (afterColon.chars().allMatch(Character::isDigit)) { + // Likely a port number + String beforeColon = ip.substring(0, lastColon); + // For IPv4, we can safely remove port + if (beforeColon.contains(".") && !beforeColon.contains("::")) { + ip = beforeColon; + } + // For IPv6, we need to be more careful - only remove if it's clearly a port + // IPv6 addresses can contain colons, so we check if it's a valid IPv6 format + // For simplicity, if it contains "::" or more than 2 colons, assume it's IPv6 and don't remove + } + } + + return ip; + } + + /** + * Converts IP address string to varbinary(16) format for database storage. + * Supports both IPv4 and IPv6. + * + * @param ipAddress IP address as string + * @return IP address as byte array (4 bytes for IPv4, 16 bytes for IPv6), or null if invalid + */ + public static byte[] ipToBytes(String ipAddress) { + if (ipAddress == null || ipAddress.isEmpty()) { + return null; + } + + try { + InetAddress inetAddress = InetAddress.getByName(ipAddress); + return inetAddress.getAddress(); + } catch (UnknownHostException e) { + log.warn("Failed to parse IP address: {}", ipAddress, e); + return null; + } + } + + /** + * Converts IP address from varbinary(16) format to string. + * + * @param ipBytes IP address as byte array + * @return IP address as string, or null if invalid + */ + public static String bytesToIp(byte[] ipBytes) { + if (ipBytes == null || ipBytes.length == 0) { + return null; + } + + try { + InetAddress inetAddress = InetAddress.getByAddress(ipBytes); + return inetAddress.getHostAddress(); + } catch (UnknownHostException e) { + log.warn("Failed to convert IP bytes to string", e); + return null; + } + } +} + diff --git a/src/main/java/com/honey/honey/util/TimeProvider.java b/src/main/java/com/honey/honey/util/TimeProvider.java new file mode 100644 index 0000000..9c4fee3 --- /dev/null +++ b/src/main/java/com/honey/honey/util/TimeProvider.java @@ -0,0 +1,27 @@ +package com.honey.honey.util; + +import java.time.Instant; + +/** + * Centralized time provider for consistent time handling across the application. + * All time-related operations should use this provider. + */ +public class TimeProvider { + + /** + * Gets current Unix timestamp in seconds. + * @return Current epoch seconds + */ + public static long nowSeconds() { + return Instant.now().getEpochSecond(); + } + + /** + * Gets current Unix timestamp in milliseconds. + * @return Current epoch milliseconds + */ + public static long nowMillis() { + return Instant.now().toEpochMilli(); + } +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2166dd6..4f7ddbf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,6 +46,12 @@ app: # Maximum number of batches to process per cleanup run max-batches-per-run: ${APP_SESSION_CLEANUP_MAX_BATCHES:20} +# GeoIP configuration +# Set GEOIP_DB_PATH environment variable to use external file (recommended for production) +# If not set, falls back to classpath:geoip/GeoLite2-Country.mmdb +geoip: + db-path: ${GEOIP_DB_PATH:} + logging: level: root: INFO diff --git a/src/main/resources/db/migration/V3__replace_users_with_sharded_tables.sql b/src/main/resources/db/migration/V3__replace_users_with_sharded_tables.sql new file mode 100644 index 0000000..6720f0c --- /dev/null +++ b/src/main/resources/db/migration/V3__replace_users_with_sharded_tables.sql @@ -0,0 +1,107 @@ +-- Drop foreign key constraint from sessions table if it exists +-- Find the foreign key name dynamically (MySQL auto-generates names like sessions_ibfk_1) +SET @fk_name = NULL; +SELECT CONSTRAINT_NAME INTO @fk_name +FROM information_schema.TABLE_CONSTRAINTS +WHERE CONSTRAINT_SCHEMA = DATABASE() +AND TABLE_NAME = 'sessions' +AND CONSTRAINT_TYPE = 'FOREIGN KEY' +LIMIT 1; + +SET @sql = IF(@fk_name IS NOT NULL, + CONCAT('ALTER TABLE sessions DROP FOREIGN KEY ', @fk_name), + 'DO 1'); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Drop old users table +DROP TABLE IF EXISTS users; + +-- Create db_users_a table +CREATE TABLE `db_users_a` ( + `id` int NOT NULL, + `screen_name` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '-', + `telegram_id` bigint UNSIGNED DEFAULT NULL, + `telegram_name` varchar(33) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '-', + `is_premium` int NOT NULL DEFAULT '0', + `language_code` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'XX', + `country_code` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'XX', + `device_code` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'XX', + `ip` varbinary(16) DEFAULT NULL, + `date_reg` int NOT NULL DEFAULT '0', + `date_login` int NOT NULL DEFAULT '0', + `banned` int NOT NULL DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- Create db_users_b table +CREATE TABLE `db_users_b` ( + `id` int NOT NULL DEFAULT '0', + `balance_a` bigint UNSIGNED NOT NULL DEFAULT '0', + `balance_b` bigint UNSIGNED NOT NULL DEFAULT '0', + `deposit_total` bigint NOT NULL DEFAULT '0', + `deposit_count` int NOT NULL DEFAULT '0', + `withdraw_total` bigint NOT NULL DEFAULT '0', + `withdraw_count` int NOT NULL DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- Create db_users_d table +CREATE TABLE `db_users_d` ( + `id` int NOT NULL DEFAULT '0', + `referer_id_1` int NOT NULL DEFAULT '0', + `referer_id_2` int NOT NULL DEFAULT '0', + `referer_id_3` int NOT NULL DEFAULT '0', + `referer_id_4` int NOT NULL DEFAULT '0', + `referer_id_5` int NOT NULL DEFAULT '0', + `master_id` int NOT NULL DEFAULT '0', + `referals_1` int NOT NULL DEFAULT '0', + `referals_2` int NOT NULL DEFAULT '0', + `referals_3` int NOT NULL DEFAULT '0', + `referals_4` int NOT NULL DEFAULT '0', + `referals_5` int NOT NULL DEFAULT '0', + `from_referals_1` bigint NOT NULL DEFAULT '0', + `from_referals_2` bigint NOT NULL DEFAULT '0', + `from_referals_3` bigint NOT NULL DEFAULT '0', + `from_referals_4` bigint NOT NULL DEFAULT '0', + `from_referals_5` bigint NOT NULL DEFAULT '0', + `to_referer_1` bigint NOT NULL DEFAULT '0', + `to_referer_2` bigint NOT NULL DEFAULT '0', + `to_referer_3` bigint NOT NULL DEFAULT '0', + `to_referer_4` bigint NOT NULL DEFAULT '0', + `to_referer_5` bigint NOT NULL DEFAULT '0' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- Add indexes for db_users_a +ALTER TABLE `db_users_a` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `telegram_id` (`telegram_id`), + ADD KEY `telegram_name` (`telegram_name`), + ADD KEY `ip` (`ip`); + +-- Add indexes for db_users_b +ALTER TABLE `db_users_b` + ADD KEY `id` (`id`); + +-- Add indexes for db_users_d +ALTER TABLE `db_users_d` + ADD KEY `id` (`id`), + ADD KEY `referer_id_1` (`referer_id_1`), + ADD KEY `referer_id_2` (`referer_id_2`), + ADD KEY `referer_id_3` (`referer_id_3`), + ADD KEY `referer_id_4` (`referer_id_4`), + ADD KEY `referer_id_5` (`referer_id_5`), + ADD KEY `master_id` (`master_id`); + +-- Set auto increment for db_users_a +ALTER TABLE `db_users_a` + MODIFY `id` int NOT NULL AUTO_INCREMENT; + +-- Update sessions table: change user_id from BIGINT to INT to match db_users_a.id +ALTER TABLE `sessions` + MODIFY `user_id` int NOT NULL; + +-- Add foreign key constraint from sessions to db_users_a +ALTER TABLE `sessions` + ADD CONSTRAINT `fk_sessions_user_id` FOREIGN KEY (`user_id`) REFERENCES `db_users_a`(`id`) ON DELETE CASCADE; + diff --git a/src/main/resources/geoip/GeoLite2-Country.mmdb b/src/main/resources/geoip/GeoLite2-Country.mmdb new file mode 100644 index 0000000..280252c Binary files /dev/null and b/src/main/resources/geoip/GeoLite2-Country.mmdb differ