feat(users): gestion des comptes et système d'invitations avec page d'administration feat(requests): demandes de séries avec fusion des saisons, vue admin et widget dashboard feat(jellyfin): synchronisation des comptes, SSO token exchange et API publique v1
166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
/**
|
|
* Système de demandes utilisateurs
|
|
*/
|
|
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import crypto from 'crypto'
|
|
import { DATA_DIR } from './config.js'
|
|
import { logger } from './logger.js'
|
|
import { findById } from './users.js'
|
|
|
|
export type RequestStatus = 'pending' | 'approved' | 'rejected' | 'completed'
|
|
|
|
export interface Requester {
|
|
userId : string
|
|
username : string
|
|
seasons : number[] // numéros de saison ; [] = toutes les saisons dispo
|
|
requestedAt: string
|
|
}
|
|
|
|
export interface SerieRequest {
|
|
id : string
|
|
serieId : number
|
|
serieName : string
|
|
requesters : Requester[]
|
|
status : RequestStatus
|
|
rejectionMessage?: string
|
|
createdAt : string
|
|
updatedAt : string
|
|
}
|
|
|
|
const REQUESTS_PATH = path.join(DATA_DIR, 'requests.json')
|
|
|
|
// ── Helpers fichier ───────────────────────────────────────────
|
|
|
|
export function readRequests(): SerieRequest[] {
|
|
try {
|
|
if (!fs.existsSync(REQUESTS_PATH)) return []
|
|
return JSON.parse(fs.readFileSync(REQUESTS_PATH, 'utf-8')) as SerieRequest[]
|
|
} catch { return [] }
|
|
}
|
|
|
|
function writeRequests(requests: SerieRequest[]): void {
|
|
fs.mkdirSync(path.dirname(REQUESTS_PATH), { recursive: true })
|
|
fs.writeFileSync(REQUESTS_PATH, JSON.stringify(requests, null, 2))
|
|
}
|
|
|
|
/** Union des saisons demandées sur tous les requesters ([] = toutes) */
|
|
export function mergedSeasons(req: SerieRequest): number[] {
|
|
if (req.requesters.some(r => r.seasons.length === 0)) return []
|
|
const all = new Set<number>()
|
|
req.requesters.forEach(r => r.seasons.forEach(s => all.add(s)))
|
|
return [...all].sort((a, b) => a - b)
|
|
}
|
|
|
|
// ── CRUD ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Créer ou fusionner une demande.
|
|
* - Si une demande active (non rejected/completed) existe pour la série → fusionne les saisons de cet user
|
|
* - Sinon → crée une nouvelle demande
|
|
*/
|
|
export function upsertRequest(
|
|
userId : string,
|
|
serieId : number,
|
|
serieName: string,
|
|
seasons : number[], // [] = toutes les saisons
|
|
): SerieRequest {
|
|
const user = findById(userId)
|
|
if (!user) throw new Error('Utilisateur introuvable')
|
|
|
|
const requests = readRequests()
|
|
|
|
// Chercher une demande active (pas rejected ni completed) pour cette série
|
|
const existing = requests.find(r =>
|
|
r.serieId === serieId &&
|
|
r.status !== 'rejected' &&
|
|
r.status !== 'completed'
|
|
)
|
|
|
|
const now = new Date().toISOString()
|
|
|
|
if (existing) {
|
|
// Mettre à jour ou ajouter ce requester dans la demande existante
|
|
const idx = existing.requesters.findIndex(r => r.userId === userId)
|
|
if (idx >= 0) {
|
|
// Mettre à jour les saisons (union)
|
|
if (seasons.length === 0 || existing.requesters[idx].seasons.length === 0) {
|
|
existing.requesters[idx].seasons = [] // l'une ou l'autre est "toutes" → toutes
|
|
} else {
|
|
const merged = new Set([...existing.requesters[idx].seasons, ...seasons])
|
|
existing.requesters[idx].seasons = [...merged].sort((a, b) => a - b)
|
|
}
|
|
existing.requesters[idx].requestedAt = now
|
|
} else {
|
|
existing.requesters.push({ userId, username: user.username, seasons, requestedAt: now })
|
|
}
|
|
existing.updatedAt = now
|
|
writeRequests(requests)
|
|
logger.info('requests', `Demande mise à jour pour "${serieName}" par "${user.username}"`)
|
|
return existing
|
|
}
|
|
|
|
// Nouvelle demande
|
|
const request: SerieRequest = {
|
|
id : crypto.randomUUID(),
|
|
serieId,
|
|
serieName,
|
|
requesters: [{ userId, username: user.username, seasons, requestedAt: now }],
|
|
status : 'pending',
|
|
createdAt : now,
|
|
updatedAt : now,
|
|
}
|
|
writeRequests([...requests, request])
|
|
logger.info('requests', `Nouvelle demande pour "${serieName}" par "${user.username}"`)
|
|
return request
|
|
}
|
|
|
|
export function approveRequest(id: string): SerieRequest {
|
|
const requests = readRequests()
|
|
const req = requests.find(r => r.id === id)
|
|
if (!req) throw new Error('Demande introuvable')
|
|
req.status = 'approved'
|
|
req.rejectionMessage = undefined
|
|
req.updatedAt = new Date().toISOString()
|
|
writeRequests(requests)
|
|
logger.info('requests', `Demande "${req.serieName}" approuvée`)
|
|
return req
|
|
}
|
|
|
|
export function rejectRequest(id: string, message?: string): SerieRequest {
|
|
const requests = readRequests()
|
|
const req = requests.find(r => r.id === id)
|
|
if (!req) throw new Error('Demande introuvable')
|
|
req.status = 'rejected'
|
|
req.rejectionMessage = message
|
|
req.updatedAt = new Date().toISOString()
|
|
writeRequests(requests)
|
|
logger.info('requests', `Demande "${req.serieName}" refusée`)
|
|
return req
|
|
}
|
|
|
|
export function completeRequest(id: string): SerieRequest {
|
|
const requests = readRequests()
|
|
const req = requests.find(r => r.id === id)
|
|
if (!req) throw new Error('Demande introuvable')
|
|
req.status = 'completed'
|
|
req.updatedAt = new Date().toISOString()
|
|
writeRequests(requests)
|
|
return req
|
|
}
|
|
|
|
export function deleteRequest(id: string): void {
|
|
const requests = readRequests()
|
|
if (!requests.find(r => r.id === id)) throw new Error('Demande introuvable')
|
|
writeRequests(requests.filter(r => r.id !== id))
|
|
}
|
|
|
|
export function getRequestsForUser(userId: string): SerieRequest[] {
|
|
return readRequests().filter(r => r.requesters.some(req => req.userId === userId))
|
|
}
|
|
|
|
export function getPendingCount(): number {
|
|
return readRequests().filter(r => r.status === 'pending').length
|
|
}
|