Limit users that can send to telegram bot

This commit is contained in:
Chris Veleris 2025-08-13 14:09:15 +03:00 committed by Chris
parent 4d86549ac6
commit d1f451da6a
6 changed files with 263 additions and 0 deletions

View file

@ -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');
},
};

View file

@ -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,

View file

@ -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',

View file

@ -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,
};

View file

@ -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
});
});
});

View file

@ -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<ProfileSettingsProps> = ({
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<ProfileSettingsProps> = ({
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<ProfileSettingsProps> = ({
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<ProfileSettingsProps> = ({
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t(
'profile.telegramAllowedUsers',
'Allowed Users'
)}
</label>
<input
type="text"
name="telegram_allowed_users"
value={
formData.telegram_allowed_users || ''
}
onChange={handleChange}
placeholder="@username1, 123456789, @username2"
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400 space-y-1">
<p>
{t(
'profile.telegramAllowedUsersDescription',
'Control who can send messages to your bot. Leave empty to allow all users.'
)}
</p>
<div className="space-y-1">
<p className="font-semibold text-gray-600 dark:text-gray-300">
{t('profile.examples', 'Examples:')}
</p>
<ul className="list-disc list-inside ml-2 space-y-0.5">
<li>
<span className="font-mono bg-gray-100 dark:bg-gray-800 px-1 rounded">
@alice, @bob
</span>
{' - '}
{t('profile.exampleUsernames', 'Allow specific usernames')}
</li>
<li>
<span className="font-mono bg-gray-100 dark:bg-gray-800 px-1 rounded">
123456789, 987654321
</span>
{' - '}
{t('profile.exampleUserIds', 'Allow specific user IDs')}
</li>
<li>
<span className="font-mono bg-gray-100 dark:bg-gray-800 px-1 rounded">
@alice, 123456789
</span>
{' - '}
{t('profile.exampleMixed', 'Mix usernames and user IDs')}
</li>
</ul>
</div>
</div>
</div>
{profile?.telegram_chat_id && (
<div className="p-2 bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-800 rounded text-green-800 dark:text-green-200">
<p className="text-sm">