feat(webhooks): add notification management API + one-click Sofarr webhook setup (Phase 3)
All checks were successful
All checks were successful
This commit is contained in:
@@ -29,6 +29,12 @@ COOKIE_SECRET=your-cookie-secret-here
|
||||
# Generate with: openssl rand -hex 32
|
||||
SOFARR_WEBHOOK_SECRET=your-webhook-secret-here
|
||||
|
||||
# Public base URL of Sofarr (for webhook configuration)
|
||||
# Required for the one-click webhook setup endpoints
|
||||
# Sonarr/Radarr need this URL to know where to send webhook events
|
||||
# Example: https://sofarr.example.com or https://192.168.1.100:3001
|
||||
SOFARR_BASE_URL=https://your-sofarr-url
|
||||
|
||||
# =============================================================================
|
||||
# TLS / HTTPS
|
||||
# =============================================================================
|
||||
|
||||
@@ -4,6 +4,7 @@ const axios = require('axios');
|
||||
const router = express.Router();
|
||||
const requireAuth = require('../middleware/requireAuth');
|
||||
const sanitizeError = require('../utils/sanitizeError');
|
||||
const { getWebhookSecret, getSofarrBaseUrl } = require('../utils/config');
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
@@ -56,4 +57,150 @@ router.get('/movies', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Notification proxy routes (Phase 3)
|
||||
// GET /api/radarr/notifications - list all notifications
|
||||
router.get('/notifications', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.RADARR_URL}/api/v3/notification`, {
|
||||
headers: { 'X-Api-Key': process.env.RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr notifications', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/radarr/notifications/:id - get specific notification
|
||||
router.get('/notifications/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.RADARR_URL}/api/v3/notification/${req.params.id}`, {
|
||||
headers: { 'X-Api-Key': process.env.RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/radarr/notifications - create notification
|
||||
router.post('/notifications', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.post(`${process.env.RADARR_URL}/api/v3/notification`, req.body, {
|
||||
headers: { 'X-Api-Key': process.env.RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create Radarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/radarr/notifications/:id - update notification
|
||||
router.put('/notifications/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.put(`${process.env.RADARR_URL}/api/v3/notification/${req.params.id}`, req.body, {
|
||||
headers: { 'X-Api-Key': process.env.RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update Radarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/radarr/notifications/:id - delete notification
|
||||
router.delete('/notifications/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.delete(`${process.env.RADARR_URL}/api/v3/notification/${req.params.id}`, {
|
||||
headers: { 'X-Api-Key': process.env.RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete Radarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/radarr/notifications/test - test notification
|
||||
router.post('/notifications/test', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.post(`${process.env.RADARR_URL}/api/v3/notification/test`, req.body, {
|
||||
headers: { 'X-Api-Key': process.env.RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to test Radarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/radarr/notifications/schema - get notification schema
|
||||
router.get('/notifications/schema', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.RADARR_URL}/api/v3/notification/schema`, {
|
||||
headers: { 'X-Api-Key': process.env.RADARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Radarr notification schema', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/radarr/notifications/sofarr-webhook - one-click Sofarr webhook setup
|
||||
router.post('/notifications/sofarr-webhook', async (req, res) => {
|
||||
try {
|
||||
const sofarrBaseUrl = getSofarrBaseUrl();
|
||||
const webhookSecret = getWebhookSecret();
|
||||
|
||||
if (!sofarrBaseUrl) {
|
||||
return res.status(400).json({ error: 'SOFARR_BASE_URL not configured' });
|
||||
}
|
||||
if (!webhookSecret) {
|
||||
return res.status(400).json({ error: 'SOFARR_WEBHOOK_SECRET not configured' });
|
||||
}
|
||||
|
||||
const webhookUrl = `${sofarrBaseUrl}/api/webhook/radarr`;
|
||||
|
||||
// Check if Sofarr webhook already exists
|
||||
const listResponse = await axios.get(`${process.env.RADARR_URL}/api/v3/notification`, {
|
||||
headers: { 'X-Api-Key': process.env.RADARR_API_KEY }
|
||||
});
|
||||
const existingNotification = listResponse.data.find(n => n.name === 'Sofarr');
|
||||
|
||||
const notificationPayload = {
|
||||
name: 'Sofarr',
|
||||
implementation: 'Webhook',
|
||||
configContract: 'WebhookSettings',
|
||||
fields: [
|
||||
{ name: 'url', value: webhookUrl },
|
||||
{ name: 'method', value: 'POST' },
|
||||
{ name: 'headers', value: [{ key: 'X-Sofarr-Webhook-Secret', value: webhookSecret }] }
|
||||
],
|
||||
onGrab: true,
|
||||
onDownload: true,
|
||||
onImport: true,
|
||||
onUpgrade: true,
|
||||
onRename: false,
|
||||
onHealthIssue: false,
|
||||
onApplicationUpdate: false
|
||||
};
|
||||
|
||||
if (existingNotification) {
|
||||
// Update existing notification
|
||||
const response = await axios.put(
|
||||
`${process.env.RADARR_URL}/api/v3/notification/${existingNotification.id}`,
|
||||
{ ...notificationPayload, id: existingNotification.id },
|
||||
{ headers: { 'X-Api-Key': process.env.RADARR_API_KEY } }
|
||||
);
|
||||
res.json(response.data);
|
||||
} else {
|
||||
// Create new notification
|
||||
const response = await axios.post(
|
||||
`${process.env.RADARR_URL}/api/v3/notification`,
|
||||
notificationPayload,
|
||||
{ headers: { 'X-Api-Key': process.env.RADARR_API_KEY } }
|
||||
);
|
||||
res.json(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to configure Sofarr webhook', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -4,6 +4,7 @@ const axios = require('axios');
|
||||
const router = express.Router();
|
||||
const requireAuth = require('../middleware/requireAuth');
|
||||
const sanitizeError = require('../utils/sanitizeError');
|
||||
const { getWebhookSecret, getSofarrBaseUrl } = require('../utils/config');
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
@@ -56,4 +57,150 @@ router.get('/series', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Notification proxy routes (Phase 3)
|
||||
// GET /api/sonarr/notifications - list all notifications
|
||||
router.get('/notifications', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.SONARR_URL}/api/v3/notification`, {
|
||||
headers: { 'X-Api-Key': process.env.SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr notifications', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/sonarr/notifications/:id - get specific notification
|
||||
router.get('/notifications/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.SONARR_URL}/api/v3/notification/${req.params.id}`, {
|
||||
headers: { 'X-Api-Key': process.env.SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/sonarr/notifications - create notification
|
||||
router.post('/notifications', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.post(`${process.env.SONARR_URL}/api/v3/notification`, req.body, {
|
||||
headers: { 'X-Api-Key': process.env.SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to create Sonarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/sonarr/notifications/:id - update notification
|
||||
router.put('/notifications/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.put(`${process.env.SONARR_URL}/api/v3/notification/${req.params.id}`, req.body, {
|
||||
headers: { 'X-Api-Key': process.env.SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to update Sonarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/sonarr/notifications/:id - delete notification
|
||||
router.delete('/notifications/:id', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.delete(`${process.env.SONARR_URL}/api/v3/notification/${req.params.id}`, {
|
||||
headers: { 'X-Api-Key': process.env.SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to delete Sonarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/sonarr/notifications/test - test notification
|
||||
router.post('/notifications/test', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.post(`${process.env.SONARR_URL}/api/v3/notification/test`, req.body, {
|
||||
headers: { 'X-Api-Key': process.env.SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to test Sonarr notification', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/sonarr/notifications/schema - get notification schema
|
||||
router.get('/notifications/schema', async (req, res) => {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.SONARR_URL}/api/v3/notification/schema`, {
|
||||
headers: { 'X-Api-Key': process.env.SONARR_API_KEY }
|
||||
});
|
||||
res.json(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to fetch Sonarr notification schema', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/sonarr/notifications/sofarr-webhook - one-click Sofarr webhook setup
|
||||
router.post('/notifications/sofarr-webhook', async (req, res) => {
|
||||
try {
|
||||
const sofarrBaseUrl = getSofarrBaseUrl();
|
||||
const webhookSecret = getWebhookSecret();
|
||||
|
||||
if (!sofarrBaseUrl) {
|
||||
return res.status(400).json({ error: 'SOFARR_BASE_URL not configured' });
|
||||
}
|
||||
if (!webhookSecret) {
|
||||
return res.status(400).json({ error: 'SOFARR_WEBHOOK_SECRET not configured' });
|
||||
}
|
||||
|
||||
const webhookUrl = `${sofarrBaseUrl}/api/webhook/sonarr`;
|
||||
|
||||
// Check if Sofarr webhook already exists
|
||||
const listResponse = await axios.get(`${process.env.SONARR_URL}/api/v3/notification`, {
|
||||
headers: { 'X-Api-Key': process.env.SONARR_API_KEY }
|
||||
});
|
||||
const existingNotification = listResponse.data.find(n => n.name === 'Sofarr');
|
||||
|
||||
const notificationPayload = {
|
||||
name: 'Sofarr',
|
||||
implementation: 'Webhook',
|
||||
configContract: 'WebhookSettings',
|
||||
fields: [
|
||||
{ name: 'url', value: webhookUrl },
|
||||
{ name: 'method', value: 'POST' },
|
||||
{ name: 'headers', value: [{ key: 'X-Sofarr-Webhook-Secret', value: webhookSecret }] }
|
||||
],
|
||||
onGrab: true,
|
||||
onDownload: true,
|
||||
onImport: true,
|
||||
onUpgrade: true,
|
||||
onRename: false,
|
||||
onHealthIssue: false,
|
||||
onApplicationUpdate: false
|
||||
};
|
||||
|
||||
if (existingNotification) {
|
||||
// Update existing notification
|
||||
const response = await axios.put(
|
||||
`${process.env.SONARR_URL}/api/v3/notification/${existingNotification.id}`,
|
||||
{ ...notificationPayload, id: existingNotification.id },
|
||||
{ headers: { 'X-Api-Key': process.env.SONARR_API_KEY } }
|
||||
);
|
||||
res.json(response.data);
|
||||
} else {
|
||||
// Create new notification
|
||||
const response = await axios.post(
|
||||
`${process.env.SONARR_URL}/api/v3/notification`,
|
||||
notificationPayload,
|
||||
{ headers: { 'X-Api-Key': process.env.SONARR_API_KEY } }
|
||||
);
|
||||
res.json(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Failed to configure Sofarr webhook', details: sanitizeError(error) });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -118,6 +118,10 @@ function getWebhookSecret() {
|
||||
return process.env.SOFARR_WEBHOOK_SECRET || '';
|
||||
}
|
||||
|
||||
function getSofarrBaseUrl() {
|
||||
return process.env.SOFARR_BASE_URL || '';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSABnzbdInstances,
|
||||
getSonarrInstances,
|
||||
@@ -126,6 +130,7 @@ module.exports = {
|
||||
getTransmissionInstances,
|
||||
getRtorrentInstances,
|
||||
getWebhookSecret,
|
||||
getSofarrBaseUrl,
|
||||
parseInstances,
|
||||
validateInstanceUrl
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user