feat: 增强设置功能,支持当前识别的管理 IP 和白名单更新逻辑
This commit is contained in:
@@ -73,6 +73,21 @@ describe('settings and auth events', () => {
|
||||
expect(events[0].message || '').toContain('签到 Cron');
|
||||
});
|
||||
|
||||
it('returns current recognized admin IP in runtime settings response', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/settings/runtime',
|
||||
remoteAddress: '10.0.0.8',
|
||||
headers: {
|
||||
'x-forwarded-for': '203.0.113.5, 10.0.0.8',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = response.json() as { currentAdminIp?: string };
|
||||
expect(body.currentAdminIp).toBe('203.0.113.5');
|
||||
});
|
||||
|
||||
it('rejects proxy token that does not start with sk-', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
@@ -114,6 +129,43 @@ describe('settings and auth events', () => {
|
||||
expect(runtime.routingFallbackUnitCost).toBe(0.25);
|
||||
});
|
||||
|
||||
it('rejects allowlist update that does not include current request IP', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/runtime',
|
||||
remoteAddress: '198.51.100.10',
|
||||
payload: {
|
||||
adminIpAllowlist: ['198.51.100.11'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = response.json() as { message?: string };
|
||||
expect(body.message).toContain('白名单');
|
||||
expect(body.message).toContain('198.51.100.10');
|
||||
|
||||
const saved = db.select().from(schema.settings).where(eq(schema.settings.key, 'admin_ip_allowlist')).get();
|
||||
expect(saved).toBeFalsy();
|
||||
});
|
||||
|
||||
it('allows allowlist update when current request IP is included', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/runtime',
|
||||
remoteAddress: '198.51.100.10',
|
||||
payload: {
|
||||
adminIpAllowlist: ['198.51.100.10', '198.51.100.11'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = response.json() as { adminIpAllowlist?: string[] };
|
||||
expect(body.adminIpAllowlist).toEqual(['198.51.100.10', '198.51.100.11']);
|
||||
|
||||
const saved = db.select().from(schema.settings).where(eq(schema.settings.key, 'admin_ip_allowlist')).get();
|
||||
expect(saved?.value).toBe(JSON.stringify(['198.51.100.10', '198.51.100.11']));
|
||||
});
|
||||
|
||||
it('appends event when admin auth token changes', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { updateBalanceRefreshCron, updateCheckinCron } from '../../services/chec
|
||||
import { sendNotification } from '../../services/notifyService.js';
|
||||
import { exportBackup, importBackup, type BackupExportType } from '../../services/backupService.js';
|
||||
import { startBackgroundTask } from '../../services/backgroundTaskService.js';
|
||||
import { extractClientIp, isIpAllowed } from '../../middleware/auth.js';
|
||||
|
||||
type RoutingWeights = typeof config.routingWeights;
|
||||
|
||||
@@ -215,7 +216,7 @@ function applyImportedSettingToRuntime(key: string, value: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
function getRuntimeSettingsResponse() {
|
||||
function getRuntimeSettingsResponse(currentAdminIp = '') {
|
||||
return {
|
||||
checkinCron: config.checkinCron,
|
||||
balanceRefreshCron: config.balanceRefreshCron,
|
||||
@@ -237,18 +238,21 @@ function getRuntimeSettingsResponse() {
|
||||
smtpTo: config.smtpTo,
|
||||
notifyCooldownSec: config.notifyCooldownSec,
|
||||
adminIpAllowlist: config.adminIpAllowlist,
|
||||
currentAdminIp,
|
||||
proxyTokenMasked: maskSecret(config.proxyToken),
|
||||
};
|
||||
}
|
||||
|
||||
export async function settingsRoutes(app: FastifyInstance) {
|
||||
app.get('/api/settings/runtime', async () => {
|
||||
return getRuntimeSettingsResponse();
|
||||
app.get('/api/settings/runtime', async (request) => {
|
||||
const currentAdminIp = extractClientIp(request.ip, request.headers['x-forwarded-for']);
|
||||
return getRuntimeSettingsResponse(currentAdminIp);
|
||||
});
|
||||
|
||||
app.put<{ Body: RuntimeSettingsBody }>('/api/settings/runtime', async (request, reply) => {
|
||||
const body = request.body || {};
|
||||
const changedLabels: string[] = [];
|
||||
const currentRequestIp = extractClientIp(request.ip, request.headers['x-forwarded-for']);
|
||||
|
||||
if (body.checkinCron !== undefined) {
|
||||
if (!cron.validate(body.checkinCron)) {
|
||||
@@ -418,6 +422,12 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
|
||||
if (body.adminIpAllowlist !== undefined) {
|
||||
const nextAllowlist = toStringList(body.adminIpAllowlist);
|
||||
if (nextAllowlist.length > 0 && !isIpAllowed(currentRequestIp, nextAllowlist)) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: `保存失败:当前请求 IP(${currentRequestIp || 'unknown'})不在新白名单中。请至少保留当前 IP,避免把自己锁出后台。`,
|
||||
});
|
||||
}
|
||||
if (JSON.stringify(nextAllowlist) !== JSON.stringify(config.adminIpAllowlist)) {
|
||||
changedLabels.push('管理端 IP 白名单');
|
||||
}
|
||||
@@ -470,7 +480,7 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
return {
|
||||
success: true,
|
||||
message: '运行时设置已更新',
|
||||
...getRuntimeSettingsResponse(),
|
||||
...getRuntimeSettingsResponse(currentRequestIp),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user