Skip to content

Commit b6cb4bf

Browse files
committed
Merge branch 'dev' into main: Marzban migration
2 parents 4385798 + 117d4c1 commit b6cb4bf

15 files changed

Lines changed: 1950 additions & 123 deletions

File tree

index.js

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const subscriptionRoutes = require('./src/routes/subscription');
3737
const authRoutes = require('./src/routes/auth');
3838
const panelRoutes = require('./src/routes/panel');
3939
const mcpRoutes = require('./src/routes/mcp');
40+
const marzbanCompat = require('./src/routes/marzbanCompat');
41+
const { subscriptionLimiter, applyRateLimits } = require('./src/utils/rateLimiters');
4042

4143
const helmet = require('helmet');
4244
const app = express();
@@ -268,40 +270,31 @@ app.post('/api/logout', (req, res) => {
268270
});
269271
});
270272

271-
const rateLimitSettings = {
272-
subscriptionPerMinute: 100,
273-
authPerSecond: 200,
274-
};
275-
276-
const subscriptionLimiter = rateLimit({
277-
windowMs: 60 * 1000,
278-
max: () => rateLimitSettings.subscriptionPerMinute,
279-
handler: (req, res) => {
280-
logger.warn(`[Sub] Rate limit: ${req.ip}`);
281-
res.status(429).type('text/plain').send('# Too many requests');
282-
},
283-
});
284-
285273
async function reloadSettings() {
286274
const Settings = require('./src/models/settingsModel');
287275
const hwidDeviceService = require('./src/services/hwidDeviceService');
288276
const settings = await Settings.get();
289-
277+
290278
cacheService.updateTTL(settings);
291279
hwidDeviceService.updateFromSettings(settings);
292-
293-
if (settings.rateLimit) {
294-
rateLimitSettings.subscriptionPerMinute = settings.rateLimit.subscriptionPerMinute || 100;
295-
rateLimitSettings.authPerSecond = settings.rateLimit.authPerSecond || 200;
296-
logger.info(`[Settings] Rate limits: sub=${rateLimitSettings.subscriptionPerMinute}/min`);
297-
}
280+
applyRateLimits(settings);
281+
282+
// Marzban compat regex/secret are cached at module scope and must be
283+
// rebuilt whenever Settings.migration.marzban changes (path, secret,
284+
// enabled flag, salt mode).
285+
marzbanCompat.invalidate();
298286
}
299287
module.exports = { reloadSettings };
300288

301289
app.use('/api/files', subscriptionLimiter);
302290
app.use('/api/info', subscriptionLimiter);
303291
app.use('/api', subscriptionRoutes);
304292

293+
// Marzban legacy-link compatibility — middleware no-ops when disabled in
294+
// settings. Mounted BEFORE panel/static handlers but AFTER /api so it never
295+
// shadows real endpoints; in-handler regex further refuses blacklisted paths.
296+
app.use(marzbanCompat);
297+
305298
app.use('/api/users', requireAuth, usersRoutes);
306299
app.use('/api/nodes', requireAuth, nodesRoutes);
307300
app.use('/api/cascade', requireAuth, cascadeRoutes);

src/locales/en.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,11 +665,13 @@
665665
"tabSecurity": "Security",
666666
"tabBackup": "Backups",
667667
"tabMaintenance": "Maintenance",
668+
"tabMigration": "Migration",
668669
"tabSystemDesc": "Performance, caching, rate limits and SSH pool.",
669670
"tabSubscriptionDesc": "Branding, buttons and client app behavior.",
670671
"tabSecurityDesc": "Administrator, 2FA, API keys, MCP and webhooks.",
671672
"tabBackupDesc": "Local and S3 backups, schedule, restore.",
672673
"tabMaintenanceDesc": "Maintenance tasks, cache, counters and danger zone.",
674+
"tabMigrationDesc": "Seamless import from a Marzban panel — old subscription links keep working.",
673675
"subBrandingTitle": "Page branding",
674676
"subProfileLinksTitle": "Hiddify profile",
675677
"sslNotConfigured": "SSL not configured",
@@ -1364,5 +1366,74 @@
13641366
"copyCredentials": "Copy link",
13651367
"retrySetup": "Retry setup",
13661368
"continueManual": "Go to Panel"
1369+
},
1370+
"migration": {
1371+
"armed": "Legacy compatibility is ARMED.",
1372+
"armedDetail": "Path: {path} · finalized {date} · imported {imported}, skipped {skipped}, errors {errors}",
1373+
"off": "Legacy compatibility is OFF. Complete the wizard below to import users and arm the compat route.",
1374+
"disableBtn": "Disable compat",
1375+
"disableConfirm": "Disable the Marzban compat route? Old links stop resolving until re-enabled.",
1376+
"step1": "1. Connect",
1377+
"step2": "2. Configure",
1378+
"step3": "3. Import",
1379+
"step4": "4. Finalize",
1380+
"connectTitle": "Connect to the source Marzban panel",
1381+
"connectDesc": "Provide the panel URL and the credentials of a sudo (or owning) admin. We never store these — they only live in this browser session until the import finishes.",
1382+
"marzbanUrl": "Marzban URL",
1383+
"marzbanUrlHint": "Include the protocol. Trailing slashes are stripped automatically.",
1384+
"adminUsername": "Admin username",
1385+
"adminPassword": "Admin password",
1386+
"testBtn": "Test connection",
1387+
"testing": "Testing…",
1388+
"continueBtn": "Continue",
1389+
"backBtn": "← Back",
1390+
"marzbanUsers": "Marzban users",
1391+
"admins": "Admins",
1392+
"sudoAccess": "Sudo access",
1393+
"status": "Status",
1394+
"sudo": "sudo",
1395+
"limited": "limited",
1396+
"configureTitle": "Migration options",
1397+
"configureDesc": "Choose which Celerity ServerGroup each Marzban admin's users land in, set the legacy subscription path, and paste the Marzban JWT secret so old links can be verified.",
1398+
"pathLabel": "Subscription path (XRAY_SUBSCRIPTION_PATH)",
1399+
"pathHint": "Must match the source panel's XRAY_SUBSCRIPTION_PATH. Default is sub. Reserved: api, panel, health, sse, docs, public, static.",
1400+
"acceptSalt": "Accept random URL salt (only if Marzban used XRAY_SUBSCRIPTION_URL_PREFIX with a *).",
1401+
"jwtSecret": "Marzban JWT secret",
1402+
"jwtSecretPlaceholder": "64-char hex string from `SELECT secret_key FROM jwt;`",
1403+
"jwtSecretHint": "The command below runs inside the Marzban container and reads the secret through Marzban's own Python/SQLAlchemy layer. That makes it work with SQLite, MySQL, or any other database configured in the panel. Celerity stores the secret AES-encrypted.",
1404+
"jwtSecretCommandTitle": "Command to get the JWT secret",
1405+
"jwtSecretCommandDesc": "Run it on the server where Marzban is running. The command automatically finds the Marzban container by image or name.",
1406+
"copyCommand": "Copy command",
1407+
"copied": "Copied",
1408+
"mappingTitle": "Admin → ServerGroup mapping",
1409+
"mappingDesc": "Each Marzban admin's users join the chosen Celerity ServerGroup. The dropdown lists active groups only.",
1410+
"thAdmin": "Marzban admin",
1411+
"thGroup": "Celerity ServerGroup",
1412+
"skipUsers": "— skip users of this admin —",
1413+
"fallbackGroup": "Fallback ServerGroup (for users whose admin is unmapped)",
1414+
"fallbackNone": "— none —",
1415+
"onlyActive": "Import only active / on_hold users (recommended).",
1416+
"importVless": "Re-use Marzban VLESS UUIDs (keeps Xray identity stable for users who already had VLESS).",
1417+
"startBtn": "Start import",
1418+
"starting": "Starting…",
1419+
"runTitle": "Import in progress",
1420+
"runDesc": "Do not close this page until the import finishes — the task lives in panel memory and a restart will abort progress.",
1421+
"processed": "processed",
1422+
"total": "total",
1423+
"imported": "imported",
1424+
"skipped": "skipped",
1425+
"errors": "errors",
1426+
"conflicts": "conflicts",
1427+
"finalizeTitle": "Activate legacy compatibility",
1428+
"finalizeDesc": "Imported users are already in Celerity. The last step enables the /{path}/{token} route so old subscription links keep working.",
1429+
"conflictsTitle": "Conflicts (manual review)",
1430+
"thReason": "Reason",
1431+
"activateBtn": "Activate compat route",
1432+
"activating": "Activating…",
1433+
"editOptions": "← Edit options",
1434+
"startFailed": "Start failed: {error}",
1435+
"activateFailed": "Activation failed: {error}",
1436+
"activateSuccess": "Compat route armed. Old links should resolve now.",
1437+
"disableFailed": "Failed: {error}"
13671438
}
13681439
}

src/locales/ru.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,11 +665,13 @@
665665
"tabSecurity": "Безопасность",
666666
"tabBackup": "Бэкапы",
667667
"tabMaintenance": "Обслуживание",
668+
"tabMigration": "Миграция",
668669
"tabSystemDesc": "Производительность, кэш, лимиты и SSH-пул.",
669670
"tabSubscriptionDesc": "Внешний вид, кнопки и поведение клиентских приложений.",
670671
"tabSecurityDesc": "Администратор, 2FA, API-ключи, MCP и вебхуки.",
671672
"tabBackupDesc": "Локальные и S3 бэкапы, расписание, восстановление.",
672673
"tabMaintenanceDesc": "Обслуживание, кэш, сброс счётчиков и опасные операции.",
674+
"tabMigrationDesc": "Бесшовный импорт пользователей из Marzban — старые подписочные ссылки продолжают работать.",
673675
"subBrandingTitle": "Брендинг страницы",
674676
"subProfileLinksTitle": "Профиль Hiddify",
675677
"sslNotConfigured": "SSL не настроен",
@@ -1364,5 +1366,74 @@
13641366
"copyCredentials": "Скопировать ссылку",
13651367
"retrySetup": "Повторить настройку",
13661368
"continueManual": "Перейти в панель"
1369+
},
1370+
"migration": {
1371+
"armed": "Совместимость со старыми ссылками ВКЛЮЧЕНА.",
1372+
"armedDetail": "Путь: {path} · завершено {date} · импортировано {imported}, пропущено {skipped}, ошибок {errors}",
1373+
"off": "Совместимость со старыми ссылками ВЫКЛЮЧЕНА. Пройдите мастер ниже, чтобы импортировать пользователей и активировать маршрут.",
1374+
"disableBtn": "Отключить совместимость",
1375+
"disableConfirm": "Отключить маршрут совместимости с Marzban? Старые ссылки перестанут открываться до повторного включения.",
1376+
"step1": "1. Подключение",
1377+
"step2": "2. Параметры",
1378+
"step3": "3. Импорт",
1379+
"step4": "4. Завершение",
1380+
"connectTitle": "Подключение к исходной панели Marzban",
1381+
"connectDesc": "Укажите URL панели и учётные данные sudo-администратора (или владельца). Данные не сохраняются — они хранятся только в этой сессии браузера до завершения импорта.",
1382+
"marzbanUrl": "URL Marzban",
1383+
"marzbanUrlHint": "Указывайте с протоколом. Завершающие слэши срезаются автоматически.",
1384+
"adminUsername": "Логин администратора",
1385+
"adminPassword": "Пароль администратора",
1386+
"testBtn": "Проверить подключение",
1387+
"testing": "Проверка…",
1388+
"continueBtn": "Продолжить",
1389+
"backBtn": "← Назад",
1390+
"marzbanUsers": "Пользователей в Marzban",
1391+
"admins": "Администраторов",
1392+
"sudoAccess": "Доступ sudo",
1393+
"status": "Статус",
1394+
"sudo": "sudo",
1395+
"limited": "ограниченный",
1396+
"configureTitle": "Параметры миграции",
1397+
"configureDesc": "Выберите, в какую группу серверов Celerity попадут пользователи каждого администратора Marzban, задайте путь подписки и вставьте JWT-секрет Marzban — он нужен для проверки старых ссылок.",
1398+
"pathLabel": "Путь подписки (XRAY_SUBSCRIPTION_PATH)",
1399+
"pathHint": "Должен совпадать с XRAY_SUBSCRIPTION_PATH исходной панели. По умолчанию sub. Зарезервировано: api, panel, health, sse, docs, public, static.",
1400+
"acceptSalt": "Принимать случайный URL-salt (только если в Marzban был задан XRAY_SUBSCRIPTION_URL_PREFIX с *).",
1401+
"jwtSecret": "JWT-секрет Marzban",
1402+
"jwtSecretPlaceholder": "64-символьная hex-строка из `SELECT secret_key FROM jwt;`",
1403+
"jwtSecretHint": "Команда ниже запускается внутри контейнера Marzban и читает секрет через родной Python/SQLAlchemy Marzban. Поэтому она работает с SQLite, MySQL и любой другой БД, настроенной в панели. В Celerity секрет хранится в AES-шифровании.",
1404+
"jwtSecretCommandTitle": "Команда для получения JWT-секрета",
1405+
"jwtSecretCommandDesc": "Выполните на сервере, где запущен Marzban. Команда автоматически найдёт контейнер Marzban по образу или имени.",
1406+
"copyCommand": "Скопировать команду",
1407+
"copied": "Скопировано",
1408+
"mappingTitle": "Сопоставление: администратор → группа серверов",
1409+
"mappingDesc": "Пользователи каждого администратора Marzban попадут в выбранную группу серверов Celerity. В списке только активные группы.",
1410+
"thAdmin": "Администратор Marzban",
1411+
"thGroup": "Группа серверов Celerity",
1412+
"skipUsers": "— пропустить пользователей этого администратора —",
1413+
"fallbackGroup": "Группа по умолчанию (для пользователей без сопоставленного администратора)",
1414+
"fallbackNone": "— нет —",
1415+
"onlyActive": "Импортировать только active / on_hold пользователей (рекомендуется).",
1416+
"importVless": "Переиспользовать UUID VLESS из Marzban (Xray-идентичность остаётся стабильной для тех, у кого уже был VLESS).",
1417+
"startBtn": "Запустить импорт",
1418+
"starting": "Запуск…",
1419+
"runTitle": "Идёт импорт",
1420+
"runDesc": "Не закрывайте страницу до завершения импорта — задача живёт в памяти панели, перезапуск прервёт прогресс.",
1421+
"processed": "обработано",
1422+
"total": "всего",
1423+
"imported": "импортировано",
1424+
"skipped": "пропущено",
1425+
"errors": "ошибок",
1426+
"conflicts": "конфликтов",
1427+
"finalizeTitle": "Активация совместимости",
1428+
"finalizeDesc": "Пользователи уже в Celerity. Последний шаг включает маршрут /{path}/{token}, чтобы старые подписочные ссылки продолжали работать.",
1429+
"conflictsTitle": "Конфликты (требуют ручной проверки)",
1430+
"thReason": "Причина",
1431+
"activateBtn": "Активировать маршрут",
1432+
"activating": "Активация…",
1433+
"editOptions": "← Изменить параметры",
1434+
"startFailed": "Не удалось запустить: {error}",
1435+
"activateFailed": "Не удалось активировать: {error}",
1436+
"activateSuccess": "Маршрут совместимости включён. Старые ссылки уже должны открываться.",
1437+
"disableFailed": "Ошибка: {error}"
13671438
}
13681439
}

src/models/settingsModel.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,31 @@ const settingsSchema = new mongoose.Schema({
143143
remote: { type: String, default: 'tls://1.1.1.1' },
144144
},
145145
},
146-
146+
147+
// Marzban legacy-link compatibility. When `enabled` is true the compat
148+
// middleware accepts incoming requests at /{path}/{token} (or /{salt}/{path}/{token}
149+
// when urlSalt is set), verifies the HMAC against jwtSecretEncrypted, and
150+
// delegates to the regular subscription pipeline. Stays inert until the
151+
// migration wizard finalizes — `enabled:false` is the safe default.
152+
migration: {
153+
marzban: {
154+
enabled: { type: Boolean, default: false },
155+
path: { type: String, default: 'sub' },
156+
jwtSecretEncrypted: { type: String, default: '' },
157+
// True if the source Marzban panel used XRAY_SUBSCRIPTION_URL_PREFIX
158+
// with a `*` placeholder — published URLs then look like
159+
// `https://host/<salt>/sub/<token>`. The salt segment is random per
160+
// user/link, so the compat regex only checks its shape, not value.
161+
acceptUrlSalt: { type: Boolean, default: false },
162+
completedAt: { type: Date, default: null },
163+
stats: {
164+
imported: { type: Number, default: 0 },
165+
skipped: { type: Number, default: 0 },
166+
errors: { type: Number, default: 0 },
167+
},
168+
},
169+
},
170+
147171
}, { timestamps: true });
148172

149173
settingsSchema.statics.get = async function() {

0 commit comments

Comments
 (0)