- Add specific error handling for GitHub API rate limit (403) errors - Create RateLimitError with proper error name for identification - Store last sync error and error time in settings for UI display - Add error status display in autosync settings modal - Show user-friendly error messages with GitHub token suggestion - Clear error status on successful sync - Update both GitHubJsonService and GitHubService with rate limit error handling This provides better user feedback when GitHub API rate limits are exceeded and guides users to set up a GitHub token for higher rate limits.
397 lines
13 KiB
TypeScript
397 lines
13 KiB
TypeScript
import type { NextRequest } from 'next/server';
|
|
import { NextResponse } from 'next/server';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { isValidCron } from 'cron-validator';
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const settings = await request.json();
|
|
|
|
if (!settings || typeof settings !== 'object') {
|
|
return NextResponse.json(
|
|
{ error: 'Settings object is required' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Handle test notification request
|
|
if (settings.testNotification) {
|
|
return await handleTestNotification();
|
|
}
|
|
|
|
// Handle manual sync trigger
|
|
if (settings.triggerManualSync) {
|
|
return await handleManualSync();
|
|
}
|
|
|
|
// Validate required fields for settings save
|
|
const requiredFields = [
|
|
'autoSyncEnabled',
|
|
'syncIntervalType',
|
|
'autoDownloadNew',
|
|
'autoUpdateExisting',
|
|
'notificationEnabled'
|
|
];
|
|
|
|
for (const field of requiredFields) {
|
|
if (!(field in settings)) {
|
|
return NextResponse.json(
|
|
{ error: `Missing required field: ${field}` },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate sync interval type
|
|
if (!['predefined', 'custom'].includes(settings.syncIntervalType)) {
|
|
return NextResponse.json(
|
|
{ error: 'syncIntervalType must be "predefined" or "custom"' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Validate predefined interval
|
|
if (settings.syncIntervalType === 'predefined') {
|
|
const validIntervals = ['15min', '30min', '1hour', '6hours', '12hours', '24hours'];
|
|
if (!validIntervals.includes(settings.syncIntervalPredefined)) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid predefined interval' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate custom cron expression
|
|
if (settings.syncIntervalType === 'custom') {
|
|
if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string' || settings.syncIntervalCron.trim() === '') {
|
|
// Fallback to predefined if custom is selected but no cron expression
|
|
settings.syncIntervalType = 'predefined';
|
|
settings.syncIntervalPredefined = settings.syncIntervalPredefined || '1hour';
|
|
settings.syncIntervalCron = '';
|
|
} else if (!isValidCron(settings.syncIntervalCron, { seconds: false })) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid cron expression' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Validate Apprise URLs if notifications are enabled
|
|
if (settings.notificationEnabled && settings.appriseUrls) {
|
|
try {
|
|
// Handle both array and JSON string formats
|
|
let urls;
|
|
if (Array.isArray(settings.appriseUrls)) {
|
|
urls = settings.appriseUrls;
|
|
} else if (typeof settings.appriseUrls === 'string') {
|
|
urls = JSON.parse(settings.appriseUrls);
|
|
} else {
|
|
return NextResponse.json(
|
|
{ error: 'Apprise URLs must be an array or JSON string' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (!Array.isArray(urls)) {
|
|
return NextResponse.json(
|
|
{ error: 'Apprise URLs must be an array' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Basic URL validation
|
|
for (const url of urls) {
|
|
if (typeof url !== 'string' || url.trim() === '') {
|
|
return NextResponse.json(
|
|
{ error: 'All Apprise URLs must be non-empty strings' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
} catch (parseError) {
|
|
return NextResponse.json(
|
|
{ error: 'Invalid JSON format for Apprise URLs' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Path to the .env file
|
|
const envPath = path.join(process.cwd(), '.env');
|
|
|
|
// Read existing .env file
|
|
let envContent = '';
|
|
if (fs.existsSync(envPath)) {
|
|
envContent = fs.readFileSync(envPath, 'utf8');
|
|
}
|
|
|
|
// Auto-sync settings to add/update
|
|
const autoSyncSettings = {
|
|
'AUTO_SYNC_ENABLED': settings.autoSyncEnabled ? 'true' : 'false',
|
|
'SYNC_INTERVAL_TYPE': settings.syncIntervalType,
|
|
'SYNC_INTERVAL_PREDEFINED': settings.syncIntervalPredefined || '',
|
|
'SYNC_INTERVAL_CRON': settings.syncIntervalCron || '',
|
|
'AUTO_DOWNLOAD_NEW': settings.autoDownloadNew ? 'true' : 'false',
|
|
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting ? 'true' : 'false',
|
|
'NOTIFICATION_ENABLED': settings.notificationEnabled ? 'true' : 'false',
|
|
'APPRISE_URLS': Array.isArray(settings.appriseUrls) ? JSON.stringify(settings.appriseUrls) : (settings.appriseUrls || '[]'),
|
|
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
|
|
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
|
|
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
|
|
};
|
|
|
|
// Update or add each setting
|
|
for (const [key, value] of Object.entries(autoSyncSettings)) {
|
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
const settingLine = `${key}="${value}"`;
|
|
|
|
if (regex.test(envContent)) {
|
|
// Replace existing setting
|
|
envContent = envContent.replace(regex, settingLine);
|
|
} else {
|
|
// Add new setting
|
|
envContent += (envContent.endsWith('\n') ? '' : '\n') + `${settingLine}\n`;
|
|
}
|
|
}
|
|
|
|
// Write back to .env file
|
|
fs.writeFileSync(envPath, envContent);
|
|
|
|
// Reschedule auto-sync service with new settings
|
|
try {
|
|
const { getAutoSyncService, setAutoSyncService } = await import('../../../../server/lib/autoSyncInit.js');
|
|
let autoSyncService = getAutoSyncService();
|
|
|
|
// If no global instance exists, create one
|
|
if (!autoSyncService) {
|
|
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
|
autoSyncService = new AutoSyncService();
|
|
setAutoSyncService(autoSyncService);
|
|
}
|
|
|
|
// Update the global service instance with new settings
|
|
autoSyncService.saveSettings(settings);
|
|
|
|
if (settings.autoSyncEnabled) {
|
|
autoSyncService.scheduleAutoSync();
|
|
} else {
|
|
autoSyncService.stopAutoSync();
|
|
// Ensure the service is completely stopped and won't restart
|
|
autoSyncService.isRunning = false;
|
|
// Also stop the global service instance if it exists
|
|
const { stopAutoSync: stopGlobalAutoSync } = await import('../../../../server/lib/autoSyncInit.js');
|
|
stopGlobalAutoSync();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error rescheduling auto-sync service:', error);
|
|
// Don't fail the request if rescheduling fails
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: 'Auto-sync settings saved successfully'
|
|
});
|
|
} catch (error) {
|
|
console.error('Error saving auto-sync settings:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to save auto-sync settings' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function GET() {
|
|
try {
|
|
// Path to the .env file
|
|
const envPath = path.join(process.cwd(), '.env');
|
|
|
|
if (!fs.existsSync(envPath)) {
|
|
return NextResponse.json({
|
|
settings: {
|
|
autoSyncEnabled: false,
|
|
syncIntervalType: 'predefined',
|
|
syncIntervalPredefined: '1hour',
|
|
syncIntervalCron: '',
|
|
autoDownloadNew: false,
|
|
autoUpdateExisting: false,
|
|
notificationEnabled: false,
|
|
appriseUrls: [],
|
|
lastAutoSync: '',
|
|
lastAutoSyncError: null,
|
|
lastAutoSyncErrorTime: null
|
|
}
|
|
});
|
|
}
|
|
|
|
// Read .env file and extract auto-sync settings
|
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
|
|
const settings = {
|
|
autoSyncEnabled: getEnvValue(envContent, 'AUTO_SYNC_ENABLED') === 'true',
|
|
syncIntervalType: getEnvValue(envContent, 'SYNC_INTERVAL_TYPE') || 'predefined',
|
|
syncIntervalPredefined: getEnvValue(envContent, 'SYNC_INTERVAL_PREDEFINED') || '1hour',
|
|
syncIntervalCron: getEnvValue(envContent, 'SYNC_INTERVAL_CRON') || '',
|
|
autoDownloadNew: getEnvValue(envContent, 'AUTO_DOWNLOAD_NEW') === 'true',
|
|
autoUpdateExisting: getEnvValue(envContent, 'AUTO_UPDATE_EXISTING') === 'true',
|
|
notificationEnabled: getEnvValue(envContent, 'NOTIFICATION_ENABLED') === 'true',
|
|
appriseUrls: (() => {
|
|
try {
|
|
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') || '[]';
|
|
return JSON.parse(urlsValue);
|
|
} catch {
|
|
return [];
|
|
}
|
|
})(),
|
|
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') || '',
|
|
lastAutoSyncError: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR') || null,
|
|
lastAutoSyncErrorTime: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR_TIME') || null
|
|
};
|
|
|
|
return NextResponse.json({ settings });
|
|
} catch (error) {
|
|
console.error('Error reading auto-sync settings:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to read auto-sync settings' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Helper function to handle test notification
|
|
async function handleTestNotification() {
|
|
try {
|
|
// Load current settings
|
|
const envPath = path.join(process.cwd(), '.env');
|
|
|
|
if (!fs.existsSync(envPath)) {
|
|
return NextResponse.json(
|
|
{ error: 'No auto-sync settings found' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
const notificationEnabled = getEnvValue(envContent, 'NOTIFICATION_ENABLED') === 'true';
|
|
const appriseUrls = (() => {
|
|
try {
|
|
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') || '[]';
|
|
return JSON.parse(urlsValue);
|
|
} catch {
|
|
return [];
|
|
}
|
|
})();
|
|
|
|
if (!notificationEnabled) {
|
|
return NextResponse.json(
|
|
{ error: 'Notifications are not enabled' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (!appriseUrls || appriseUrls.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'No Apprise URLs configured' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Send test notification using the auto-sync service
|
|
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
|
const autoSyncService = new AutoSyncService();
|
|
const result = await autoSyncService.testNotification();
|
|
|
|
if (result.success) {
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: 'Test notification sent successfully'
|
|
});
|
|
} else {
|
|
return NextResponse.json(
|
|
{ error: result.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error sending test notification:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to send test notification' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Helper function to handle manual sync trigger
|
|
async function handleManualSync() {
|
|
try {
|
|
// Load current settings
|
|
const envPath = path.join(process.cwd(), '.env');
|
|
|
|
if (!fs.existsSync(envPath)) {
|
|
return NextResponse.json(
|
|
{ error: 'No auto-sync settings found' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
const envContent = fs.readFileSync(envPath, 'utf8');
|
|
const autoSyncEnabled = getEnvValue(envContent, 'AUTO_SYNC_ENABLED') === 'true';
|
|
|
|
if (!autoSyncEnabled) {
|
|
return NextResponse.json(
|
|
{ error: 'Auto-sync is not enabled' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Trigger manual sync using the auto-sync service
|
|
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
|
|
const autoSyncService = new AutoSyncService();
|
|
const result = await autoSyncService.executeAutoSync() as any;
|
|
|
|
if (result && result.success) {
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: 'Manual sync completed successfully',
|
|
result
|
|
});
|
|
} else {
|
|
return NextResponse.json(
|
|
{ error: result.message },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error triggering manual sync:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to trigger manual sync' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
// Helper function to extract value from .env content
|
|
function getEnvValue(envContent: string, key: string): string {
|
|
// Try to match the pattern with quotes around the value (handles nested quotes)
|
|
const regex = new RegExp(`^${key}="(.+)"$`, 'm');
|
|
let match = regex.exec(envContent);
|
|
|
|
if (match && match[1]) {
|
|
let value = match[1];
|
|
// Remove extra quotes that might be around JSON values
|
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// Try to match without quotes (fallback)
|
|
const regexNoQuotes = new RegExp(`^${key}=([^\\s]*)$`, 'm');
|
|
match = regexNoQuotes.exec(envContent);
|
|
if (match && match[1]) {
|
|
return match[1];
|
|
}
|
|
|
|
return '';
|
|
}
|