package db import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "gestion/models" "log" "time" ) // MigrateAddTelegramColumns ajoute les colonnes telegram_chat_id si elles n'existent pas func (d *Database) MigrateAddTelegramColumns() { migrations := []string{ `ALTER TABLE clients ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`, `ALTER TABLE users ADD COLUMN IF NOT EXISTS telegram_chat_id BIGINT`, `ALTER TABLE clients ADD COLUMN IF NOT EXISTS two_fa_enabled BOOLEAN NOT NULL DEFAULT FALSE`, } for _, q := range migrations { if err := d.GDB.Exec(q).Error; err != nil { log.Printf("⚠️ [TELEGRAM_MIGRATION] %v", err) } } log.Println("✅ [TELEGRAM] Colonnes telegram_chat_id vérifiées") } const linkTokenTTL = 10 * time.Minute // GenerateLinkToken crée un token aléatoire sécurisé et le stocke dans Redis (10 min) func GenerateLinkToken(username, role string) (string, error) { raw := make([]byte, 16) if _, err := rand.Read(raw); err != nil { return "", fmt.Errorf("génération token: %w", err) } token := hex.EncodeToString(raw) data := models.TelegramLinkData{Username: username, Role: role} val, err := json.Marshal(data) if err != nil { return "", err } key := fmt.Sprintf("telegram:link:%s", token) if err := Redis.Set(RedisCtx, key, val, linkTokenTTL).Err(); err != nil { return "", fmt.Errorf("Redis SET: %w", err) } return token, nil } // ValidateAndConsumeLinkToken valide le token, retourne les données, puis le supprime func ValidateAndConsumeLinkToken(token string) (username, role string, err error) { key := fmt.Sprintf("telegram:link:%s", token) val, err := Redis.Get(RedisCtx, key).Bytes() if err != nil { return "", "", fmt.Errorf("token invalide ou expiré") } var data models.TelegramLinkData if err := json.Unmarshal(val, &data); err != nil { return "", "", fmt.Errorf("données corrompues") } Redis.Del(RedisCtx, key) return data.Username, data.Role, nil } func (d *Database) SaveClientTelegramChatID(username string, chatID int64) error { return d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("telegram_chat_id", chatID).Error } func (d *Database) GetClientTelegramChatID(username string) (int64, bool, error) { var result struct { ChatID *int64 `gorm:"column:telegram_chat_id"` } if err := d.GDB.Table("clients").Select("telegram_chat_id").Where("username = ?", username).Limit(1).Scan(&result).Error; err != nil { return 0, false, err } if result.ChatID == nil { return 0, false, nil } return *result.ChatID, true, nil } func (d *Database) DeleteClientTelegramChatID(username string) error { return d.GDB.Model(&models.Client{}).Where("username = ?", username).Update("telegram_chat_id", nil).Error } func (d *Database) SaveUserTelegramChatID(username string, chatID int64) error { return d.GDB.Model(&models.User{}).Where("username = ?", username).Update("telegram_chat_id", chatID).Error } func (d *Database) GetUserTelegramChatID(username string) (int64, bool, error) { var result struct { ChatID *int64 `gorm:"column:telegram_chat_id"` } if err := d.GDB.Table("users").Select("telegram_chat_id").Where("username = ?", username).Limit(1).Scan(&result).Error; err != nil { return 0, false, err } if result.ChatID == nil { return 0, false, nil } return *result.ChatID, true, nil } func (d *Database) DeleteUserTelegramChatID(username string) error { return d.GDB.Model(&models.User{}).Where("username = ?", username).Update("telegram_chat_id", nil).Error } // GetUserByTelegramChatID retrouve un utilisateur (clients + users) par chat_id func (d *Database) GetUserByTelegramChatID(chatID int64) (username, role string, err error) { var clientResult struct { Username string `gorm:"column:username"` } if err = d.GDB.Table("clients").Select("username").Where("telegram_chat_id = ?", chatID).Limit(1).Scan(&clientResult).Error; err == nil && clientResult.Username != "" { return clientResult.Username, "client", nil } var userResult struct { Username string `gorm:"column:username"` Role string `gorm:"column:role"` } if err = d.GDB.Model(&models.User{}).Select("username, role").Where("telegram_chat_id = ?", chatID).Limit(1).Scan(&userResult).Error; err == nil && userResult.Username != "" { return userResult.Username, userResult.Role, nil } return "", "", fmt.Errorf("aucun compte lié à ce chat_id") } // ── 2FA sessions ───────────────────────────────────────────────────────────── const twoFASessionTTL = 5 * time.Minute type twoFASessionData struct { Username string `json:"username"` Code string `json:"code"` } func Store2FASession(sessionToken, username, code string) error { data, err := json.Marshal(twoFASessionData{Username: username, Code: code}) if err != nil { return err } return Redis.Set(RedisCtx, "2fa:session:"+sessionToken, data, twoFASessionTTL).Err() } // Verify2FASession valide le code et retourne le username. GETDEL = atomique (anti-replay). func Verify2FASession(sessionToken, code string) (string, error) { val, err := Redis.GetDel(RedisCtx, "2fa:session:"+sessionToken).Bytes() if err != nil { return "", fmt.Errorf("session invalide ou expirée") } var d twoFASessionData if err := json.Unmarshal(val, &d); err != nil { return "", fmt.Errorf("données corrompues") } if d.Code != code { return "", fmt.Errorf("code incorrect") } return d.Username, nil }