diff --git a/backend/migrations/20250813103351-add-telegram-allowed-users.js b/backend/migrations/20250813103351-add-telegram-allowed-users.js new file mode 100644 index 0000000..b814e23 --- /dev/null +++ b/backend/migrations/20250813103351-add-telegram-allowed-users.js @@ -0,0 +1,23 @@ +'use strict'; + +const { safeAddColumns } = require('../utils/migration-utils'); + +module.exports = { + async up(queryInterface, Sequelize) { + await safeAddColumns(queryInterface, 'users', [ + { + name: 'telegram_allowed_users', + definition: { + type: Sequelize.TEXT, + allowNull: true, + comment: + 'Comma-separated list of allowed Telegram usernames or user IDs', + }, + }, + ]); + }, + + async down(queryInterface) { + await queryInterface.removeColumn('users', 'telegram_allowed_users'); + }, +}; diff --git a/backend/models/user.js b/backend/models/user.js index db08b4e..71187f3 100644 --- a/backend/models/user.js +++ b/backend/models/user.js @@ -93,6 +93,12 @@ module.exports = (sequelize) => { type: DataTypes.DATE, allowNull: true, }, + telegram_allowed_users: { + type: DataTypes.TEXT, + allowNull: true, + comment: + 'Comma-separated list of allowed Telegram usernames or user IDs', + }, task_intelligence_enabled: { type: DataTypes.BOOLEAN, allowNull: false, diff --git a/backend/routes/users.js b/backend/routes/users.js index 9f55ba0..7609b16 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -31,6 +31,7 @@ router.get('/profile', async (req, res) => { 'avatar_image', 'telegram_bot_token', 'telegram_chat_id', + 'telegram_allowed_users', 'task_summary_enabled', 'task_summary_frequency', 'task_intelligence_enabled', @@ -81,6 +82,7 @@ router.patch('/profile', async (req, res) => { timezone, avatar_image, telegram_bot_token, + telegram_allowed_users, task_intelligence_enabled, task_summary_enabled, task_summary_frequency, @@ -100,6 +102,8 @@ router.patch('/profile', async (req, res) => { allowedUpdates.avatar_image = avatar_image; if (telegram_bot_token !== undefined) allowedUpdates.telegram_bot_token = telegram_bot_token; + if (telegram_allowed_users !== undefined) + allowedUpdates.telegram_allowed_users = telegram_allowed_users; if (task_intelligence_enabled !== undefined) allowedUpdates.task_intelligence_enabled = task_intelligence_enabled; @@ -158,6 +162,7 @@ router.patch('/profile', async (req, res) => { 'avatar_image', 'telegram_bot_token', 'telegram_chat_id', + 'telegram_allowed_users', 'task_intelligence_enabled', 'task_summary_enabled', 'task_summary_frequency', diff --git a/backend/services/telegramPoller.js b/backend/services/telegramPoller.js index a2d037f..7300646 100644 --- a/backend/services/telegramPoller.js +++ b/backend/services/telegramPoller.js @@ -229,6 +229,50 @@ const createInboxItem = async (content, userId, messageId) => { }); }; +// Function to check if a Telegram user is authorized +const isAuthorizedTelegramUser = (user, message) => { + // If no whitelist is configured, allow all users (default behavior) + if ( + !user.telegram_allowed_users || + user.telegram_allowed_users.trim() === '' + ) { + return true; + } + + const allowedUsers = user.telegram_allowed_users + .split(',') + .map((u) => u.trim().toLowerCase()) + .filter((u) => u.length > 0); + + if (allowedUsers.length === 0) { + return true; // Empty whitelist means allow all + } + + const fromUser = message.from; + if (!fromUser) { + return false; // No sender information + } + + // Check by user ID (numeric) + const userId = fromUser.id.toString(); + if (allowedUsers.includes(userId)) { + return true; + } + + // Check by username (with or without @ prefix) + if (fromUser.username) { + const username = fromUser.username.toLowerCase(); + if ( + allowedUsers.includes(username) || + allowedUsers.includes(`@${username}`) + ) { + return true; + } + } + + return false; +}; + // Function to handle bot commands const handleBotCommand = async (command, user, chatId, messageId) => { const botToken = user.telegram_bot_token; @@ -268,6 +312,14 @@ const processMessage = async (user, update) => { const chatId = message.chat.id.toString(); const messageId = message.message_id; + // Check if the user is authorized to send messages to this bot + if (!isAuthorizedTelegramUser(user, message)) { + console.log( + `Ignoring message from unauthorized Telegram user ${message.from.id} (@${message.from.username || 'no_username'}) for bot owner ${user.id}` + ); + return; // Silently ignore unauthorized users + } + // If this user already has a chat bound and it doesn't match this update, ignore if (user.telegram_chat_id && user.telegram_chat_id !== chatId) { return; @@ -518,4 +570,5 @@ module.exports = { _getHighestUpdateId: getHighestUpdateId, _createMessageParams: createMessageParams, _createTelegramUrl: createTelegramUrl, + _isAuthorizedTelegramUser: isAuthorizedTelegramUser, }; diff --git a/backend/tests/unit/services/telegramAuth.test.js b/backend/tests/unit/services/telegramAuth.test.js new file mode 100644 index 0000000..e3579bb --- /dev/null +++ b/backend/tests/unit/services/telegramAuth.test.js @@ -0,0 +1,114 @@ +const { + _isAuthorizedTelegramUser, +} = require('../../../services/telegramPoller'); + +describe('Telegram Authorization', () => { + describe('isAuthorizedTelegramUser', () => { + it('should allow all users when no whitelist is configured', () => { + const user = { telegram_allowed_users: null }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should allow all users when whitelist is empty', () => { + const user = { telegram_allowed_users: '' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should allow all users when whitelist is whitespace only', () => { + const user = { telegram_allowed_users: ' ' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should allow user when ID is in whitelist', () => { + const user = { telegram_allowed_users: '123456,789012' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should allow user when username is in whitelist (without @)', () => { + const user = { telegram_allowed_users: 'testuser,anotheruser' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should allow user when username is in whitelist (with @)', () => { + const user = { telegram_allowed_users: '@testuser,@anotheruser' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should allow user in mixed whitelist by ID match', () => { + const user = { telegram_allowed_users: '123456,@anotheruser' }; + const message = { from: { id: 123456, username: 'differentuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should allow user in mixed whitelist by username match', () => { + const user = { telegram_allowed_users: '789012,testuser' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should deny user not in whitelist', () => { + const user = { telegram_allowed_users: '789012,@anotheruser' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(false); + }); + + it('should deny user without username if ID not in whitelist', () => { + const user = { telegram_allowed_users: '789012,@testuser' }; + const message = { from: { id: 123456 } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(false); + }); + + it('should allow user without username if ID is in whitelist', () => { + const user = { telegram_allowed_users: '123456,@testuser' }; + const message = { from: { id: 123456 } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should handle case insensitive username matching', () => { + const user = { telegram_allowed_users: 'TestUser,@AnotherUser' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should deny when no sender information is provided', () => { + const user = { telegram_allowed_users: 'testuser' }; + const message = {}; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(false); + }); + + it('should handle whitelist with extra spaces and commas', () => { + const user = { + telegram_allowed_users: ' testuser , @anotheruser , 123456 ', + }; + const message = { from: { id: 123456, username: 'differentuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); + }); + + it('should deny when whitelist contains only empty values after trimming', () => { + const user = { telegram_allowed_users: ' , , ' }; + const message = { from: { id: 123456, username: 'testuser' } }; + + expect(_isAuthorizedTelegramUser(user, message)).toBe(true); // Empty after filtering means allow all + }); + }); +}); diff --git a/frontend/components/Profile/ProfileSettings.tsx b/frontend/components/Profile/ProfileSettings.tsx index 4a99c8a..3332a64 100644 --- a/frontend/components/Profile/ProfileSettings.tsx +++ b/frontend/components/Profile/ProfileSettings.tsx @@ -44,6 +44,7 @@ interface Profile { avatar_image: string | null; telegram_bot_token: string | null; telegram_chat_id: string | null; + telegram_allowed_users: string | null; task_summary_enabled: boolean; task_summary_frequency: string; task_intelligence_enabled: boolean; @@ -103,6 +104,7 @@ const ProfileSettings: React.FC = ({ timezone: 'UTC', avatar_image: '', telegram_bot_token: '', + telegram_allowed_users: '', task_intelligence_enabled: true, task_summary_enabled: false, task_summary_frequency: 'daily', @@ -259,6 +261,7 @@ const ProfileSettings: React.FC = ({ timezone: data.timezone || 'UTC', avatar_image: data.avatar_image || '', telegram_bot_token: data.telegram_bot_token || '', + telegram_allowed_users: data.telegram_allowed_users || '', task_intelligence_enabled: data.task_intelligence_enabled !== undefined ? data.task_intelligence_enabled @@ -643,6 +646,10 @@ const ProfileSettings: React.FC = ({ updatedProfile.telegram_bot_token !== undefined ? updatedProfile.telegram_bot_token : prev.telegram_bot_token || '', + telegram_allowed_users: + updatedProfile.telegram_allowed_users !== undefined + ? updatedProfile.telegram_allowed_users + : prev.telegram_allowed_users || '', task_intelligence_enabled: updatedProfile.task_intelligence_enabled !== undefined ? updatedProfile.task_intelligence_enabled @@ -1423,6 +1430,61 @@ const ProfileSettings: React.FC = ({

+
+ + +
+

+ {t( + 'profile.telegramAllowedUsersDescription', + 'Control who can send messages to your bot. Leave empty to allow all users.' + )} +

+
+

+ {t('profile.examples', 'Examples:')} +

+
    +
  • + + @alice, @bob + + {' - '} + {t('profile.exampleUsernames', 'Allow specific usernames')} +
  • +
  • + + 123456789, 987654321 + + {' - '} + {t('profile.exampleUserIds', 'Allow specific user IDs')} +
  • +
  • + + @alice, 123456789 + + {' - '} + {t('profile.exampleMixed', 'Mix usernames and user IDs')} +
  • +
+
+
+
+ {profile?.telegram_chat_id && (