// Voir factory/docs/adr/20260509-telegram-gateway-auth.md package main import ( "context" "fmt" "log" "strings" "time" ) // AuthHandler is the handler for the principal bot (factory). It accepts // the auth-related commands (/start, /auth , /whoami, /logout) and // falls back to a help message for anything else. On a successful /auth, // the original chat message is best-effort deleted so the secret doesn't // linger in the user's chat history. type AuthHandler struct { tg *TelegramClient auth *Auth } const helpMessage = "Commandes disponibles :\n" + " /auth — ouvrir une session (24 h)\n" + " /whoami — afficher l'état de session\n" + " /logout — fermer la session\n" const startMessage = "Bonjour 👋\n\n" + "Je suis le bot d'authentification du gateway Arcodange.\n" + "Envoie `/auth ` pour ouvrir une session, puis utilise les autres bots du gateway normalement.\n\n" + helpMessage func (h *AuthHandler) Handle(ctx context.Context, update Update, bot Bot) error { chatID, ok := update.ChatID() if !ok { return nil } userID, _ := update.UserID() text := strings.TrimSpace(update.Text()) switch { case text == "/start" || text == "/start@"+bot.Slug: return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: startMessage}) case strings.HasPrefix(text, "/auth"): provided := strings.TrimSpace(strings.TrimPrefix(text, "/auth")) // strip a possible "@" suffix on the command (e.g. /auth@arcodange_factory_bot s3cr3t) if at := strings.IndexAny(provided, " "); at > 0 && strings.HasPrefix(provided, "@") { provided = strings.TrimSpace(provided[at:]) } return h.handleAuthCommand(ctx, update, bot, chatID, userID, provided) case text == "/whoami" || strings.HasPrefix(text, "/whoami@"): return h.handleWhoami(ctx, bot, chatID, userID) case text == "/logout" || strings.HasPrefix(text, "/logout@"): return h.handleLogout(ctx, bot, chatID, userID) default: return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: helpMessage}) } } func (h *AuthHandler) handleAuthCommand(ctx context.Context, update Update, bot Bot, chatID, userID int64, provided string) error { if provided == "" { return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: "Usage : `/auth `"}) } if userID == 0 { log.Printf("auth attempt user= result=skip (no from.id)") return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: "Impossible d'identifier l'utilisateur."}) } ok, err := h.auth.Login(ctx, userID, provided) if err != nil { log.Printf("auth attempt user=%d result=error err=%v", userID, err) return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: "Erreur interne, réessaie plus tard."}) } if !ok { log.Printf("auth attempt user=%d result=fail", userID) return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: "❌ Mauvais code."}) } log.Printf("auth attempt user=%d result=ok", userID) // Replay defense — best-effort, ignore errors (insufficient permissions etc.) if msgID := messageID(update); msgID != 0 { if delErr := h.tg.DeleteMessage(ctx, bot.Token, chatID, msgID); delErr != nil { log.Printf("deleteMessage best-effort failed user=%d: %v", userID, delErr) } } return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ ChatID: chatID, Text: "✅ Authentifié pour 24 h. Tu peux maintenant utiliser les autres bots du gateway.", }) } func (h *AuthHandler) handleWhoami(ctx context.Context, bot Bot, chatID, userID int64) error { if userID == 0 { return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: "Impossible d'identifier l'utilisateur."}) } authed, _ := h.auth.IsAuthed(ctx, userID) if !authed { return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ ChatID: chatID, Text: fmt.Sprintf("user=%d : non authentifié.\nUtilise `/auth ` pour ouvrir une session.", userID), }) } ttl, _ := h.auth.Remaining(ctx, userID) return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ ChatID: chatID, Text: fmt.Sprintf("user=%d : ✅ authentifié, reste %s.", userID, ttl.Round(time.Second)), }) } func (h *AuthHandler) handleLogout(ctx context.Context, bot Bot, chatID, userID int64) error { if userID == 0 { return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: "Impossible d'identifier l'utilisateur."}) } if err := h.auth.Logout(ctx, userID); err != nil { log.Printf("logout user=%d err=%v", userID, err) return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: "Erreur interne, réessaie plus tard."}) } log.Printf("logout user=%d", userID) return h.tg.SendMessage(ctx, bot.Token, SendMessageParams{ChatID: chatID, Text: "Déconnecté."}) } // messageID extracts the original Telegram message ID from an Update so we // can delete it on a successful /auth (replay defense). func messageID(u Update) int64 { switch { case u.Message != nil: return u.Message.MessageID case u.EditedMessage != nil: return u.EditedMessage.MessageID } return 0 }