164 lines
5.4 KiB
Go
164 lines
5.4 KiB
Go
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
|
|
}
|