implemented new DB core, referal system

This commit is contained in:
AddictionGames
2026-01-10 00:48:14 +02:00
parent 248e9cd4c9
commit c125063c84
24 changed files with 1155 additions and 136 deletions

View File

@@ -72,6 +72,13 @@
<artifactId>flyway-mysql</artifactId> <artifactId>flyway-mysql</artifactId>
</dependency> </dependency>
<!-- MaxMind GeoIP2 -->
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>4.2.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -2,17 +2,16 @@ package com.honey.honey.controller;
import com.honey.honey.dto.CreateSessionRequest; import com.honey.honey.dto.CreateSessionRequest;
import com.honey.honey.dto.CreateSessionResponse; import com.honey.honey.dto.CreateSessionResponse;
import com.honey.honey.model.User; import com.honey.honey.model.UserA;
import com.honey.honey.repository.UserRepository;
import com.honey.honey.service.SessionService; import com.honey.honey.service.SessionService;
import com.honey.honey.service.TelegramAuthService; import com.honey.honey.service.TelegramAuthService;
import lombok.Data; import com.honey.honey.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime; import jakarta.servlet.http.HttpServletRequest;
import java.util.Map; import java.util.Map;
@Slf4j @Slf4j
@@ -23,33 +22,33 @@ public class AuthController {
private final TelegramAuthService telegramAuthService; private final TelegramAuthService telegramAuthService;
private final SessionService sessionService; private final SessionService sessionService;
private final UserRepository userRepository; private final UserService userService;
/** /**
* Creates a session by validating Telegram initData. * Creates a session by validating Telegram initData.
* This is the only endpoint that accepts initData. * This is the only endpoint that accepts initData.
* Handles user registration/login and referral system.
*/ */
@PostMapping("/tma/session") @PostMapping("/tma/session")
public CreateSessionResponse createSession(@RequestBody CreateSessionRequest request) { public CreateSessionResponse createSession(
@RequestBody CreateSessionRequest request,
HttpServletRequest httpRequest) {
String initData = request.getInitData(); String initData = request.getInitData();
if (initData == null || initData.isBlank()) { if (initData == null || initData.isBlank()) {
throw new IllegalArgumentException("initData is required"); throw new IllegalArgumentException("initData is required");
} }
// Validate Telegram initData signature // Validate Telegram initData signature and parse data
Map<String, Object> tgUser = telegramAuthService.validateAndParseInitData(initData); Map<String, Object> tgUserData = telegramAuthService.validateAndParseInitData(initData);
Long telegramId = ((Number) tgUser.get("id")).longValue(); // Get or create user (handles registration, login update, and referral system)
String username = (String) tgUser.get("username"); UserA user = userService.getOrCreateUser(tgUserData, httpRequest);
// Get or create user
User user = getOrCreateUser(telegramId, username);
// Create session // Create session
String sessionId = sessionService.createSession(user); 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() return CreateSessionResponse.builder()
.access_token(sessionId) .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. * Extracts Bearer token from Authorization header.
*/ */

View File

@@ -1,13 +1,14 @@
package com.honey.honey.controller; package com.honey.honey.controller;
import com.honey.honey.dto.UserDto; 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.security.UserContext;
import com.honey.honey.service.UserService;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RestController;
@Slf4j @Slf4j
@RestController @RestController
@@ -15,14 +16,31 @@ import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserController { public class UserController {
private final UserService userService;
@GetMapping("/current") @GetMapping("/current")
public UserDto getCurrentUser() { public UserDto getCurrentUser() {
User user = UserContext.get(); UserA user = UserContext.get();
return UserDto.builder() return UserDto.builder()
.telegram_id(user.getTelegramId()) .telegram_id(user.getTelegramId())
.username(user.getUsername()) .username(user.getTelegramName())
.build(); .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;
}
}

View File

@@ -21,9 +21,8 @@ public class Session {
@Column(name = "session_id_hash", unique = true, nullable = false, length = 255) @Column(name = "session_id_hash", unique = true, nullable = false, length = 255)
private String sessionIdHash; private String sessionIdHash;
@ManyToOne(fetch = FetchType.EAGER) @Column(name = "user_id", nullable = false)
@JoinColumn(name = "user_id", nullable = false) private Integer userId;
private User user;
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -19,15 +19,15 @@ public interface SessionRepository extends JpaRepository<Session, Long> {
/** /**
* Counts active (non-expired) sessions for a user. * Counts active (non-expired) sessions for a user.
*/ */
@Query("SELECT COUNT(s) FROM Session s WHERE s.user.id = :userId AND s.expiresAt > :now") @Query("SELECT COUNT(s) FROM Session s WHERE s.userId = :userId AND s.expiresAt > :now")
long countActiveSessionsByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now); long countActiveSessionsByUserId(@Param("userId") Integer userId, @Param("now") LocalDateTime now);
/** /**
* Finds oldest active sessions for a user, ordered by created_at ASC. * Finds oldest active sessions for a user, ordered by created_at ASC.
* Used to delete oldest sessions when max limit is exceeded. * 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") @Query("SELECT s FROM Session s WHERE s.userId = :userId AND s.expiresAt > :now ORDER BY s.createdAt ASC")
List<Session> findOldestActiveSessionsByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now, Pageable pageable); List<Session> findOldestActiveSessionsByUserId(@Param("userId") Integer userId, @Param("now") LocalDateTime now, Pageable pageable);
/** /**
* Batch deletes expired sessions (up to batchSize). * Batch deletes expired sessions (up to batchSize).

View File

@@ -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<UserA, Integer> {
Optional<UserA> findByTelegramId(Long telegramId);
}

View File

@@ -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<UserB, Integer> {
}

View File

@@ -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<UserD, Integer> {
/**
* 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);
}

View File

@@ -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<User, Long> {
List<User> findAllByTelegramId(Long telegramId);
Optional<User> findByTelegramId(Long telegramId);
}

View File

@@ -1,6 +1,6 @@
package com.honey.honey.security; package com.honey.honey.security;
import com.honey.honey.model.User; import com.honey.honey.model.UserA;
import com.honey.honey.service.SessionService; import com.honey.honey.service.SessionService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -38,7 +38,7 @@ public class AuthInterceptor implements HandlerInterceptor {
} }
// Validate session and get user // Validate session and get user
Optional<User> userOpt = sessionService.getUserBySession(sessionId); Optional<UserA> userOpt = sessionService.getUserBySession(sessionId);
if (userOpt.isEmpty()) { if (userOpt.isEmpty()) {
log.warn("❌ Invalid or expired session: {}", maskSessionId(sessionId)); log.warn("❌ Invalid or expired session: {}", maskSessionId(sessionId));
@@ -47,7 +47,7 @@ public class AuthInterceptor implements HandlerInterceptor {
} }
// Put user in context // Put user in context
User user = userOpt.get(); UserA user = userOpt.get();
UserContext.set(user); UserContext.set(user);
log.debug("🔑 Authenticated userId={} via session", user.getId()); log.debug("🔑 Authenticated userId={} via session", user.getId());

View File

@@ -1,16 +1,16 @@
package com.honey.honey.security; package com.honey.honey.security;
import com.honey.honey.model.User; import com.honey.honey.model.UserA;
public class UserContext { public class UserContext {
private static final ThreadLocal<User> current = new ThreadLocal<>(); private static final ThreadLocal<UserA> current = new ThreadLocal<>();
public static void set(User user) { public static void set(UserA user) {
current.set(user); current.set(user);
} }
public static User get() { public static UserA get() {
return current.get(); return current.get();
} }

View File

@@ -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";
}
}
}

View File

@@ -1,8 +1,10 @@
package com.honey.honey.service; package com.honey.honey.service;
import com.honey.honey.model.Session; 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.SessionRepository;
import com.honey.honey.repository.UserARepository;
import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -24,9 +26,15 @@ import java.util.Optional;
public class SessionService { public class SessionService {
private final SessionRepository sessionRepository; private final SessionRepository sessionRepository;
private final UserARepository userARepository;
private static final int SESSION_TTL_HOURS = 24; // 1 day private static final int SESSION_TTL_HOURS = 24; // 1 day
private static final SecureRandom secureRandom = new SecureRandom(); private static final SecureRandom secureRandom = new SecureRandom();
/**
* -- GETTER --
* Gets max active sessions per user.
*/
@Getter
@Value("${app.session.max-active-per-user:5}") @Value("${app.session.max-active-per-user:5}")
private int maxActiveSessionsPerUser; 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. * Returns the raw session ID (to be sent to frontend) and stores the hash in DB.
*/ */
@Transactional @Transactional
public String createSession(User user) { public String createSession(UserA user) {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
// Generate cryptographically random session ID // Generate cryptographically random session ID
@@ -56,7 +64,7 @@ public class SessionService {
// Create and save session // Create and save session
Session session = Session.builder() Session session = Session.builder()
.sessionIdHash(sessionIdHash) .sessionIdHash(sessionIdHash)
.user(user) .userId(user.getId())
.createdAt(now) .createdAt(now)
.expiresAt(expiresAt) .expiresAt(expiresAt)
.build(); .build();
@@ -70,7 +78,7 @@ public class SessionService {
/** /**
* Enforces MAX_ACTIVE_SESSIONS_PER_USER by deleting oldest active sessions if limit exceeded. * 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); long activeCount = sessionRepository.countActiveSessionsByUserId(userId, now);
if (activeCount >= maxActiveSessionsPerUser) { if (activeCount >= maxActiveSessionsPerUser) {
@@ -98,7 +106,7 @@ public class SessionService {
* Returns empty if session is invalid or expired. * Returns empty if session is invalid or expired.
*/ */
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Optional<User> getUserBySession(String sessionId) { public Optional<UserA> getUserBySession(String sessionId) {
if (sessionId == null || sessionId.isBlank()) { if (sessionId == null || sessionId.isBlank()) {
return Optional.empty(); return Optional.empty();
} }
@@ -120,12 +128,8 @@ public class SessionService {
return Optional.empty(); return Optional.empty();
} }
// Access user properties while still in transaction to initialize lazy proxy // Load user by ID
User user = session.getUser(); return userARepository.findById(session.getUserId());
// Force initialization by accessing a property
user.getTelegramId();
return Optional.of(user);
} }
/** /**
@@ -173,11 +177,5 @@ public class SessionService {
return SESSION_TTL_HOURS * 3600; return SESSION_TTL_HOURS * 3600;
} }
/**
* Gets max active sessions per user.
*/
public int getMaxActiveSessionsPerUser() {
return maxActiveSessionsPerUser;
}
} }

View File

@@ -30,6 +30,9 @@ public class TelegramAuthService {
/** /**
* Validates and parses Telegram initData string. * 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<String, Object> validateAndParseInitData(String initData) { public Map<String, Object> validateAndParseInitData(String initData) {
@@ -60,16 +63,21 @@ public class TelegramAuthService {
throw new UnauthorizedException("Invalid Telegram signature"); 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<String, String> decoded = decodeQueryParams(initData); Map<String, String> decoded = decodeQueryParams(initData);
String userJson = decoded.get("user"); String userJson = decoded.get("user");
String start = decoded.get("start"); // Referral parameter from URL: /honey?start=774876
if (userJson == null) { if (userJson == null) {
throw new UnauthorizedException("initData does not contain 'user' field"); throw new UnauthorizedException("initData does not contain 'user' field");
} }
// Step 6. Parse JSON into map. // Step 6. Parse JSON into map and add start parameter.
return objectMapper.readValue(userJson, Map.class); Map<String, Object> result = new HashMap<>();
result.put("user", objectMapper.readValue(userJson, Map.class));
result.put("start", start);
return result;
} catch (UnauthorizedException ex) { } catch (UnauthorizedException ex) {
throw ex; throw ex;

View File

@@ -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<String, Object> tgUserData, HttpServletRequest request) {
// Extract user data and start parameter (from URL: /honey?start=774876)
@SuppressWarnings("unchecked")
Map<String, Object> tgUser = (Map<String, Object>) 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<UserA> 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<UserA> 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<UserD> 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<UserD> level1Opt = userDRepository.findById(refererId);
if (level1Opt.isEmpty()) {
return;
}
UserD level1 = level1Opt.get();
// Level 2
if (level1.getRefererId1() > 0) {
userDRepository.incrementReferals2(level1.getRefererId1());
Optional<UserD> level2Opt = userDRepository.findById(level1.getRefererId1());
if (level2Opt.isPresent()) {
UserD level2 = level2Opt.get();
// Level 3
if (level2.getRefererId1() > 0) {
userDRepository.incrementReferals3(level2.getRefererId1());
Optional<UserD> level3Opt = userDRepository.findById(level2.getRefererId1());
if (level3Opt.isPresent()) {
UserD level3 = level3Opt.get();
// Level 4
if (level3.getRefererId1() > 0) {
userDRepository.incrementReferals4(level3.getRefererId1());
Optional<UserD> 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<UserA> getUserById(Integer userId) {
return userARepository.findById(userId);
}
/**
* Gets user by Telegram ID.
*/
public Optional<UserA> getUserByTelegramId(Long telegramId) {
return userARepository.findByTelegramId(telegramId);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -46,6 +46,12 @@ app:
# Maximum number of batches to process per cleanup run # Maximum number of batches to process per cleanup run
max-batches-per-run: ${APP_SESSION_CLEANUP_MAX_BATCHES:20} 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: logging:
level: level:
root: INFO root: INFO

View File

@@ -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;

Binary file not shown.