diff --git a/server/index.ts b/server/index.ts index e5c34f6..71bbe5e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -72,20 +72,35 @@ app.post('/api/auth/regenerate-token', requireAuth, authRegenerateToken) // ── API publique v1 (auth par token) ────────────────────────── app.use('/api', publicApiRouter) -// ── Routes (admin uniquement) ────────────────────────────────── -app.use('/api', requireAdmin, usersRouter) -app.use('/api', requireAdmin, jellyfinRouter) -app.use('/api', requireAdmin, settingsRouter) -app.use('/api', requireAdmin, torrentClientsRouter) -app.use('/api', requireAdmin, downloadsRouter) -app.use('/api', requireAdmin, organizeRouter) -app.use('/api', requireAdmin, importRouter) -app.use('/api', requireAdmin, systemRouter) -app.use('/api', requireAdmin, nfoUpdatesRouter) -app.use('/api', requireAdmin, plexRouter) -app.use('/api', requireAdmin, rssSyncRouter) +// ── Garde admin : protège les préfixes réservés aux admins ──── +// NOTE : app.use('/api', requireAdmin, router) s'applique à TOUTES +// les requêtes /api/* avant même que le router vérifie ses routes, +// ce qui bloquerait les utilisateurs standard. On utilise donc un +// middleware ciblé sur les chemins réellement admin-only. +const ADMIN_PREFIXES = [ + '/users', '/jellyfin', '/settings', '/torrent-clients', + '/downloads', '/download', '/organize', '/import', + '/system', '/nfo-updates', '/plex', '/rss-sync', +] +app.use('/api', (req, res, next) => { + if (ADMIN_PREFIXES.some(p => req.path === p || req.path.startsWith(p + '/'))) { + return requireAdmin(req, res, next) + } + next() +}) -// ── Routes accessibles à tous les utilisateurs connectés ─────── +// ── Routers (admin + utilisateurs connectés) ─────────────────── +app.use('/api', usersRouter) +app.use('/api', jellyfinRouter) +app.use('/api', settingsRouter) +app.use('/api', torrentClientsRouter) +app.use('/api', downloadsRouter) +app.use('/api', organizeRouter) +app.use('/api', importRouter) +app.use('/api', systemRouter) +app.use('/api', nfoUpdatesRouter) +app.use('/api', plexRouter) +app.use('/api', rssSyncRouter) app.use('/api', requireAuth, seriesRouter) app.use('/api', requireAuth, requestsRouter) // Invites : mix public + admin (le router gère ses propres middlewares) diff --git a/src/views/settings/AdvancedView.vue b/src/views/settings/AdvancedView.vue index 2af9e5f..81a3ab4 100644 --- a/src/views/settings/AdvancedView.vue +++ b/src/views/settings/AdvancedView.vue @@ -26,6 +26,7 @@ import SettingsToggle from '@/components/settings/SettingsToggle.vue' const devMode = ref(false) const saved = ref(false) let saveTimer: ReturnType | null = null +let loaded = false onMounted(async () => { const res = await fetch('/api/settings', { credentials: 'include' }) @@ -33,9 +34,11 @@ onMounted(async () => { const s = await res.json() devMode.value = s.devMode ?? false } + loaded = true }) watch(devMode, async (val) => { + if (!loaded) return await fetch('/api/settings', { method : 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/src/views/settings/DownloadClientView.vue b/src/views/settings/DownloadClientView.vue index 42d17ef..eae2cff 100644 --- a/src/views/settings/DownloadClientView.vue +++ b/src/views/settings/DownloadClientView.vue @@ -255,6 +255,10 @@ const clientMatrix = [ id: 'real-debrid', label: 'Real-Debrid', version: 'API v1', auth: 'Clé API', fileSelect: true, fileProgress: true, torrentUpload: 'proxy', categories: false, deleteFiles: true, }, + { + id: 'alldebrid', label: 'AllDebrid', version: 'API v4', + auth: 'Clé API', fileSelect: true, fileProgress: true, torrentUpload: 'proxy', categories: false, deleteFiles: true, + }, ] const modal = ref({ diff --git a/src/views/settings/JellyfinView.vue b/src/views/settings/JellyfinView.vue index 59bc3a3..dd6a988 100644 --- a/src/views/settings/JellyfinView.vue +++ b/src/views/settings/JellyfinView.vue @@ -71,38 +71,6 @@ - -
-
-

Token API personnel

-

- Utilisez ce token pour vous authentifier sur l'API publique FanKarr - (ex : configuration manuelle du plugin Jellyfin). -

-
- -
-
- - {{ showToken ? myToken : '••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••' }} - - - -
- -

- Endpoint auth plugin : - POST {{ origin }}/api/v1/auth/jellyfin -

-
-
-

Référence API v1

@@ -147,10 +115,7 @@ const syncing = ref(false) const syncResult = ref<{ created: number; skipped: number; users: string[] } | null>(null) const syncError = ref(null) -const myToken = ref('') -const showToken = ref(false) -const tokenCopied = ref(false) -const origin = window.location.origin +const origin = window.location.origin const apiEndpoints = [ { method: 'POST', path: '/api/v1/auth/jellyfin', desc: 'Échanger un token Jellyfin contre un token FanKarr' }, @@ -173,15 +138,7 @@ async function loadSettings() { } } -async function loadMyToken() { - const res = await fetch('/api/auth/me', { credentials: 'include' }) - if (res.ok) { - const d = await res.json() - myToken.value = d.apiToken ?? '' - } -} - -onMounted(() => { loadSettings(); loadMyToken() }) +onMounted(() => { loadSettings() }) async function save() { saving.value = true @@ -227,14 +184,5 @@ async function copyToken() { setTimeout(() => { tokenCopied.value = false }, 2000) } -async function regenerateToken() { - if (!confirm('Regénérer le token ? L\'ancien token sera immédiatement invalidé.')) return - const res = await fetch('/api/auth/regenerate-token', { - method: 'POST', credentials: 'include', - }) - if (res.ok) { - const d = await res.json() - myToken.value = d.apiToken - } -} +