Files
dashcaddy/dashcaddy-api/routes/arr/credentials.js
Krystie b172a21b63 Migrate 25 route files to throw-based error handling
Converted routes:
- All auth routes (totp.js, keys.js, sso-gate.js)
- Recipe deployment routes (deploy.js, manage.js, index.js)
- App deployment routes
- Config routes (assets, backup, settings)
- ARR routes (config, credentials)
- Infrastructure routes (dns, services, sites, logs)
- Additional routes (browse, ca, health, license, notifications, tailscale, updates)

Changes:
- Replaced ctx.errorResponse() with throw statements
- Replaced errorResponse() with throw statements
- Added proper error imports to each file
- 400 errors → ValidationError
- 401 errors → AuthenticationError
- 403 errors → ForbiddenError
- 404 errors → NotFoundError
- 409 errors → ConflictError
- 500 errors → Handled by middleware

Result: 25 files migrated, ~150 error responses standardized
2026-03-29 18:53:03 -07:00

137 lines
4.6 KiB
JavaScript

const express = require('express');
const { validateURL, validateToken } = require('../../input-validator');
const { ValidationError } = require('../errors');
module.exports = function(ctx, helpers) {
const router = express.Router();
// Store arr service credentials
router.post('/arr/credentials', ctx.asyncHandler(async (req, res) => {
const { service, apiKey, url, seedboxBaseUrl, qualityProfileId, qualityProfileName } = req.body;
if (!service || !apiKey) {
throw new ValidationError('Service name and API key required');
}
const validServices = ['radarr', 'sonarr', 'prowlarr', 'lidarr', 'plex'];
if (!validServices.includes(service)) {
return ctx.errorResponse(res, 400, `Invalid service. Must be one of: ${validServices.join(', ')}`);
}
// Validate API key format
try {
validateToken(apiKey);
} catch (e) {
throw new ValidationError('Invalid API key format');
}
// Validate URL if provided
if (url) {
try { validateURL(url); } catch (e) {
throw new ValidationError('Invalid URL format');
}
}
// Determine credential key
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
// Build metadata
const metadata = {
service,
source: url ? 'external' : 'local',
url: url || null,
storedAt: new Date().toISOString()
};
// Test connection if URL is known
let connectionTest = null;
let resolvedUrl = url;
if (!resolvedUrl) {
// Try to resolve URL from services.json
try {
const services = await ctx.servicesStateManager.read();
const svc = Array.isArray(services) ? services : services.services || [];
const found = svc.find(s => s.id === service && s.isExternal);
if (found?.externalUrl) resolvedUrl = found.externalUrl;
} catch (e) { /* ignore */ }
}
if (resolvedUrl) {
connectionTest = await helpers.testServiceConnection(service, resolvedUrl, apiKey);
if (connectionTest.success) {
metadata.lastVerified = new Date().toISOString();
metadata.version = connectionTest.version;
metadata.url = resolvedUrl;
}
}
// Store quality profile preference if provided
if (qualityProfileId) {
metadata.qualityProfileId = qualityProfileId;
metadata.qualityProfileName = qualityProfileName || null;
}
// Store the credential
const stored = await ctx.credentialManager.store(credKey, apiKey, metadata);
if (!stored) {
return ctx.errorResponse(res, 500, 'Failed to store credential');
}
// Optionally store seedbox base URL
if (seedboxBaseUrl) {
try { validateURL(seedboxBaseUrl); } catch (e) {
throw new ValidationError('Invalid seedbox base URL');
}
await ctx.credentialManager.store('arr.seedbox.baseurl', seedboxBaseUrl, {
storedAt: new Date().toISOString()
});
}
ctx.log.info('arr', 'Stored API key', { service, verified: connectionTest?.success || false });
res.json({
success: true,
message: `${service} API key stored`,
connectionTest,
url: resolvedUrl
});
}, 'arr-credentials-store'));
// List stored arr credentials (keys only, not values)
router.get('/arr/credentials', ctx.asyncHandler(async (req, res) => {
const services = ['radarr', 'sonarr', 'prowlarr', 'lidarr', 'plex'];
const credentials = {};
for (const service of services) {
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
const hasKey = !!(await ctx.credentialManager.retrieve(credKey));
const metadata = await ctx.credentialManager.getMetadata(credKey);
credentials[service] = {
hasKey,
url: metadata?.url || null,
lastVerified: metadata?.lastVerified || null,
version: metadata?.version || null,
source: metadata?.source || null
};
}
// Get seedbox base URL
const seedboxBaseUrl = await ctx.credentialManager.retrieve('arr.seedbox.baseurl');
res.json({ success: true, credentials, seedboxBaseUrl: seedboxBaseUrl || null });
}, 'arr-credentials-list'));
// Delete stored arr credentials
router.delete('/arr/credentials/:service', ctx.asyncHandler(async (req, res) => {
const { service } = req.params;
const credKey = service === 'plex' ? 'arr.plex.token' : `arr.${service}.apikey`;
await ctx.credentialManager.delete(credKey);
ctx.log.info('arr', 'Deleted credentials', { service });
res.json({ success: true, message: `${service} credentials removed` });
}, 'arr-credentials-delete'));
return router;
};