- Port par défaut 3001 → 9898 - Volume /app/data → /config (entrypoint, docker-compose, README) - Normalisation titres Kaï/Kai dans index.ts (catalogue + déduplication + serie detail) - Fix pagination GitLab NFO (per_page=100 → loop multi-pages) - Fix _is_bonus_filename avec word boundaries (\b) — évite faux positifs "national"→"ona" - Fix dossier Bonus/Specials exclus : récupère flottants (7,5/19,5) et S00Exx → saison 0 - _EP_FLOAT_PATTERN supporte virgules (7,5) en plus des points (7.5) - Ajout strict_season dans find_episode/resolve_ep — évite collision ep1 S1 vs ep1 S2 (JoJo Parties) - Ajout SEASON_EP_OFFSET pour Hokuto No Ken (saison 02 torrent → +16 offset API) - Fallback non-strict si strict_season échoue (Hokuto ep 5-16 S1) - Fix collect_covered_episodes : fallback torrent.serie_id si absent dans resolved_episodes - Fix s6_verify : get_all_specials + compteur S00 séparé, déduplication Kaï/Kai, format OK/TOTAL - Ajout MANUAL_TORRENTS dans s4 (MHA Film 4 You're Next URL Nyaa) - Injection torrents locaux manquants dans pipeline (Reborn! Kaï via fichier local) - Fix s5 : résolution directe torrents manuels season_number=0 - Fix s2 : pack_saga_slash et pack_partie_slash → pack_integrale, lookahead (?<!/) sur episode_num_tiret_apres - Fix s5 skip condition : re-résoudre si resolved_count < files_count (Fairy Tail) - README : note hardlinks volumes multiples, port, chemins config
114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import fs from 'fs'
|
|
import path from 'path'
|
|
import bcrypt from 'bcryptjs'
|
|
import jwt from 'jsonwebtoken'
|
|
import type { Request, Response, NextFunction } from 'express'
|
|
import { JWT_SECRET } from './secret.js'
|
|
|
|
const DATA_PATH = path.join(process.cwd(), 'config', 'auth.json')
|
|
const SALT_ROUNDS = 10
|
|
|
|
interface AuthData {
|
|
username: string
|
|
passwordHash: string
|
|
}
|
|
|
|
// ── Helpers fichier ───────────────────────────────────────────
|
|
function readAuth(): AuthData | null {
|
|
try {
|
|
if (!fs.existsSync(DATA_PATH)) return null
|
|
return JSON.parse(fs.readFileSync(DATA_PATH, 'utf-8'))
|
|
} catch { return null }
|
|
}
|
|
|
|
function writeAuth(data: AuthData): void {
|
|
fs.mkdirSync(path.dirname(DATA_PATH), { recursive: true })
|
|
fs.writeFileSync(DATA_PATH, JSON.stringify(data, null, 2))
|
|
}
|
|
|
|
// ── Routes ────────────────────────────────────────────────────
|
|
|
|
// GET /api/auth/status
|
|
export function authStatus(req: Request, res: Response): void {
|
|
const existing = readAuth()
|
|
const token = req.cookies?.fankarr_token
|
|
|
|
let loggedIn = false
|
|
if (token) {
|
|
try {
|
|
jwt.verify(token, JWT_SECRET)
|
|
loggedIn = true
|
|
} catch {}
|
|
}
|
|
|
|
res.json({
|
|
setup: !!existing,
|
|
loggedIn
|
|
})
|
|
}
|
|
|
|
// POST /api/auth/setup
|
|
export function authSetup(req: Request, res: Response): void {
|
|
if (readAuth()) {
|
|
res.status(400).json({ error: 'Un compte existe déjà' })
|
|
return
|
|
}
|
|
|
|
const { username, password } = req.body
|
|
if (!username || !password) {
|
|
res.status(400).json({ error: 'Username et password requis' })
|
|
return
|
|
}
|
|
|
|
const passwordHash = bcrypt.hashSync(password, SALT_ROUNDS)
|
|
writeAuth({ username, passwordHash })
|
|
|
|
const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1d' })
|
|
res.cookie('fankarr_token', token, { httpOnly: true, sameSite: 'lax' })
|
|
res.json({ success: true })
|
|
}
|
|
|
|
// POST /api/auth/login
|
|
export function authLogin(req: Request, res: Response): void {
|
|
const auth = readAuth()
|
|
if (!auth) {
|
|
res.status(400).json({ error: 'Aucun compte configuré' })
|
|
return
|
|
}
|
|
|
|
const { username, password } = req.body
|
|
if (!username || !password) {
|
|
res.status(400).json({ error: 'Username et password requis' })
|
|
return
|
|
}
|
|
|
|
if (username !== auth.username || !bcrypt.compareSync(password, auth.passwordHash)) {
|
|
res.status(401).json({ error: 'Identifiants incorrects' })
|
|
return
|
|
}
|
|
|
|
const token = jwt.sign({ username }, JWT_SECRET, { expiresIn: '1d' })
|
|
res.cookie('fankarr_token', token, { httpOnly: true, sameSite: 'lax' })
|
|
res.json({ success: true })
|
|
}
|
|
|
|
// POST /api/auth/logout
|
|
export function authLogout(_req: Request, res: Response): void {
|
|
res.clearCookie('fankarr_token')
|
|
res.json({ success: true })
|
|
}
|
|
|
|
// Middleware protection des routes
|
|
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
|
|
const token = req.cookies?.fankarr_token
|
|
if (!token) {
|
|
res.status(401).json({ error: 'Non authentifié' })
|
|
return
|
|
}
|
|
try {
|
|
jwt.verify(token, JWT_SECRET)
|
|
next()
|
|
} catch {
|
|
res.status(401).json({ error: 'Token invalide' })
|
|
}
|
|
} |