feat: support d'un chemin de base (BASE_URL) pour reverse proxy
FanKarr supposait jusqu'ici être servi à la racine du domaine. La variable
d'environnement BASE_URL (ex. /fankarr) permet de l'héberger dans un
sous-dossier, sans reconstruire l'image : le préfixe est appliqué au démarrage.
- server/config.ts : BASE_URL normalisée et validée ('' = racine)
- server/index.ts : toutes les routes montées sous BASE_URL via un Router,
index.html servi avec <base href> et window.__BASE_URL__, redirection de /
- server/auth.ts : cookie de session limité au chemin de base
- src/base-url.ts : préfixage des appels fetch('/api/…') en un point unique,
helpers withBase / absoluteUrl
- src/router : createWebHistory(BASE_URL)
- vite.config.ts : base './' (assets relatifs, résolus par <base href>)
Comportement inchangé lorsque BASE_URL n'est pas défini.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e8dc76e94a
commit
be97aa0dea
12 files changed
+163
-35
No files matched your search
+54
-28
@@ -17,7 +17,7 @@ import realDebridDriver from './torrent-clients/real-debrid.js'
|
||||
import delugeDriver from './torrent-clients/deluge.js'
|
||||
import { autoOrganizeAll, scanMediaPath, syncFilenameChanges, migrateOrganizedEpisodeIds, dedupeOrganizedEpisodes } from './organize.js'
|
||||
import { logger } from './logger.js'
|
||||
import { DATA_DIR, BASE_DIR } from './config.js'
|
||||
import { DATA_DIR, BASE_DIR, BASE_URL } from './config.js'
|
||||
import { readAvailable, readInfohashMap } from './lib/github-cache.js'
|
||||
import { loadCatalog } from './lib/serie-helpers.js'
|
||||
import { ORGANIZED_PATH, readOrganized, writeOrganized } from './lib/organized-store.js'
|
||||
@@ -56,6 +56,9 @@ registerDriver(delugeDriver)
|
||||
const app = express()
|
||||
const PORT = Number(process.env.PORT) || 9898
|
||||
|
||||
// Toutes les routes de l'application sont montées sous BASE_URL ('' = racine)
|
||||
const site = express.Router()
|
||||
|
||||
app.use(express.json())
|
||||
app.use(cookieParser())
|
||||
|
||||
@@ -68,7 +71,8 @@ app.use(cors({
|
||||
|
||||
const PUBLIC_PATH = path.join(BASE_DIR, 'public')
|
||||
if (fs.existsSync(PUBLIC_PATH)) {
|
||||
app.use(express.static(PUBLIC_PATH))
|
||||
// index: false — index.html passe par le fallback SPA, qui y injecte BASE_URL
|
||||
site.use(express.static(PUBLIC_PATH, { index: false }))
|
||||
}
|
||||
|
||||
// ── Migration vers le multi-utilisateur ───────────────────────
|
||||
@@ -76,17 +80,17 @@ migrateIfNeeded()
|
||||
migrateOnboarding()
|
||||
|
||||
// ── Authentification ───────────────────────────────────────────
|
||||
app.get ('/api/auth/status', authStatus)
|
||||
app.post('/api/auth/setup', authSetup)
|
||||
app.post('/api/auth/login', authLogin)
|
||||
app.post('/api/auth/logout', authLogout)
|
||||
app.get ('/api/auth/me', requireAuth, authMe)
|
||||
app.post('/api/auth/change-password', requireAuth, authChangePassword)
|
||||
app.post('/api/auth/regenerate-token', requireAuth, authRegenerateToken)
|
||||
app.post('/api/auth/tour-seen', requireAuth, authTourSeen)
|
||||
site.get ('/api/auth/status', authStatus)
|
||||
site.post('/api/auth/setup', authSetup)
|
||||
site.post('/api/auth/login', authLogin)
|
||||
site.post('/api/auth/logout', authLogout)
|
||||
site.get ('/api/auth/me', requireAuth, authMe)
|
||||
site.post('/api/auth/change-password', requireAuth, authChangePassword)
|
||||
site.post('/api/auth/regenerate-token', requireAuth, authRegenerateToken)
|
||||
site.post('/api/auth/tour-seen', requireAuth, authTourSeen)
|
||||
|
||||
// ── API publique v1 (authentification par jeton) ──────────────
|
||||
app.use('/api', publicApiRouter)
|
||||
site.use('/api', publicApiRouter)
|
||||
|
||||
// ── Garde admin par préfixe ───────────────────────────────────
|
||||
// requireAdmin sur app.use('/api') bloquerait tous les utilisateurs
|
||||
@@ -98,7 +102,7 @@ const ADMIN_PREFIXES = [
|
||||
'/rename-episode', '/rename-all', '/purge-nfo',
|
||||
'/logs', '/browse', '/browse-files', '/update', '/scan', '/debug',
|
||||
]
|
||||
app.use('/api', (req, res, next) => {
|
||||
site.use('/api', (req, res, next) => {
|
||||
if (ADMIN_PREFIXES.some(p => req.path === p || req.path.startsWith(p + '/'))) {
|
||||
return requireAdmin(req, res, next)
|
||||
}
|
||||
@@ -106,31 +110,53 @@ app.use('/api', (req, res, next) => {
|
||||
})
|
||||
|
||||
// ── Routes (admins et 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', invitesRouter)
|
||||
app.use('/api', requireAuth, seriesRouter)
|
||||
app.use('/api', requireAuth, requestsRouter)
|
||||
site.use('/api', usersRouter)
|
||||
site.use('/api', jellyfinRouter)
|
||||
site.use('/api', settingsRouter)
|
||||
site.use('/api', torrentClientsRouter)
|
||||
site.use('/api', downloadsRouter)
|
||||
site.use('/api', organizeRouter)
|
||||
site.use('/api', importRouter)
|
||||
site.use('/api', systemRouter)
|
||||
site.use('/api', nfoUpdatesRouter)
|
||||
site.use('/api', plexRouter)
|
||||
site.use('/api', rssSyncRouter)
|
||||
site.use('/api', invitesRouter)
|
||||
site.use('/api', requireAuth, seriesRouter)
|
||||
site.use('/api', requireAuth, requestsRouter)
|
||||
|
||||
// ── Application web (toutes les autres routes) ─────────────────
|
||||
// index.html est servi avec <base href> et window.__BASE_URL__ : le client
|
||||
// (routeur et appels /api) suit ainsi le préfixe du reverse proxy sans rebuild.
|
||||
let indexHtmlCache: string | null = null
|
||||
function renderIndexHtml(): string {
|
||||
if (indexHtmlCache !== null) return indexHtmlCache
|
||||
const raw = fs.readFileSync(path.join(PUBLIC_PATH, 'index.html'), 'utf-8')
|
||||
const injected =
|
||||
`<base href="${BASE_URL}/">\n` +
|
||||
` <script>window.__BASE_URL__ = ${JSON.stringify(BASE_URL)}</script>`
|
||||
indexHtmlCache = raw.replace('<head>', `<head>\n ${injected}`)
|
||||
return indexHtmlCache
|
||||
}
|
||||
|
||||
if (fs.existsSync(PUBLIC_PATH)) {
|
||||
app.get('*path', (_req, res) => { res.sendFile(path.join(PUBLIC_PATH, 'index.html')) })
|
||||
site.get('*path', (_req, res) => {
|
||||
res.type('html').send(renderIndexHtml())
|
||||
})
|
||||
}
|
||||
|
||||
// ── Montage sous le chemin de base ─────────────────────────────
|
||||
app.use(BASE_URL || '/', site)
|
||||
if (BASE_URL) {
|
||||
// Accès à la racine : redirection vers l'application
|
||||
app.get('/', (_req, res) => { res.redirect(`${BASE_URL}/`) })
|
||||
}
|
||||
|
||||
// ── Démarrage ──────────────────────────────────────────────────
|
||||
const server = http.createServer({ maxHeaderSize: 32768 }, app)
|
||||
|
||||
server.listen(PORT, async () => {
|
||||
const url = `http://localhost:${PORT}`
|
||||
const url = `http://localhost:${PORT}${BASE_URL}`
|
||||
console.log(`\n FanKarr \x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\\n`)
|
||||
logger.info('api', `Serveur démarré sur ${url}`)
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user