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
+ {t( + 'profile.telegramAllowedUsersDescription', + 'Control who can send messages to your bot. Leave empty to allow all users.' + )} +
++ {t('profile.examples', 'Examples:')} +
+