Compare commits

..

13 Commits

Author SHA1 Message Date
7f361ce0a2 refactor: delete unused GET /api/komga/config route
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 2s
2026-02-28 11:13:45 +01:00
eec51b7ef8 refactor: convert Komga test connection to Server Action
- Add testKomgaConnection to config.ts
- Update KomgaSettings to use Server Action
- Remove api/komga/test route
2026-02-28 11:09:48 +01:00
b40f59bec6 refactor: convert admin user management to Server Actions
- Add src/app/actions/admin.ts with updateUserRoles, deleteUser, resetUserPassword
- Update EditUserDialog, DeleteUserDialog, ResetPasswordDialog to use Server Actions
- Remove admin users API routes (PATCH/DELETE/PUT)
2026-02-28 11:06:42 +01:00
7134c069d7 refactor: convert auth register to Server Action
- Add src/app/actions/auth.ts with registerUser
- Update RegisterForm to use Server Action
- Remove api/auth/register route
2026-02-28 11:01:13 +01:00
b815202529 refactor: convert password change to Server Action
- Add src/app/actions/password.ts with changePassword
- Update ChangePasswordForm to use Server Action
- Remove api/user/password route (entire file)
2026-02-28 10:59:00 +01:00
0548215096 refactor: convert Komga config to Server Action
- Add src/app/actions/config.ts with saveKomgaConfig
- Update KomgaSettings to use Server Action
- Remove POST from api/komga/config route (keep GET)
2026-02-28 10:56:52 +01:00
6180f9abb1 refactor: convert library scan to Server Action
- Add src/app/actions/library.ts with scanLibrary
- Update ScanButton to use Server Action
- Remove POST from api/komga/libraries/[libraryId]/scan route
2026-02-28 10:53:41 +01:00
d56b0fd7ae refactor: convert preferences to Server Action
- Add src/app/actions/preferences.ts with updatePreferences
- Update PreferencesContext to use Server Action
- Remove PUT from api/preferences route (keep GET)
2026-02-28 10:50:32 +01:00
7308c0aa63 refactor: convert favorites to Server Actions
- Add src/app/actions/favorites.ts with addToFavorites and removeFromFavorites
- Update SeriesHeader to use Server Actions instead of fetch
- Keep API route GET only (POST/DELETE removed)
2026-02-28 10:46:03 +01:00
7e3fb22d3a docs: add server actions conversion plan 2026-02-28 10:39:28 +01:00
546f3769c2 refactor: remove unused read-progress API route 2026-02-28 10:36:58 +01:00
03cb46f81b refactor: use Server Actions for read progress updates
- Create src/app/actions/read-progress.ts with updateReadProgress and deleteReadProgress
- Update mark-as-read-button and mark-as-unread-button to use Server Actions
- Update usePageNavigation hook to use Server Action
- Use revalidateTag with 'min' profile for cache invalidation
2026-02-28 10:34:26 +01:00
ecce0a9738 fix: invalidate home cache when updating read progress
- Add cache tags support to BaseApiService
- Tag home data with 'home-data' tag in HomeService
- Use revalidateTag('home-data', 'max') after read progress updates
- With 'max' profile: serve stale while fetching fresh in background
2026-02-28 10:16:12 +01:00
34 changed files with 666 additions and 802 deletions

63
docs/api-get-cleanup.md Normal file
View File

@@ -0,0 +1,63 @@
# Plan - Suppression des routes API GET restantes
## État actuel
Routes GET encore présentes mais peu/n'utilisées :
| Route | Utilisation actuelle | Action |
|-------|---------------------|--------|
| `GET /api/komga/config` | ❌ Non utilisée | 🔴 Supprimer |
| `GET /api/komga/favorites` | Sidebar, SeriesHeader (client) | 🟡 Optimiser |
| `GET /api/preferences` | PreferencesContext (client) | 🟡 Optimiser |
| `GET /api/komga/books/[bookId]` | ClientBookPage, DownloadManager | 🟡 Supprimer (données déjà en props) |
| `GET /api/user/profile` | ? | 🔍 Vérifier |
## Actions proposées
### 1. Supprimer `GET /api/komga/config`
La config est déjà appelée directement dans `settings/page.tsx` via `ConfigDBService.getConfig()`.
**Action** : Supprimer la route API.
---
### 2. Optimiser les préférences
Les préférences sont déjà passées depuis `layout.tsx` via `PreferencesService.getPreferences()`.
Le `PreferencesContext` refetch en client - c'est redondant.
**Action** : Le contexte utilise déjà les `initialPreferences`. Le fetch client n'est nécessaire que si on n'a pas les données initiales.
---
### 3. Supprimer `GET /api/komga/books/[bookId]`
Regardons ce que fait `ClientBookPage` :
```tsx
// Server Component (page.tsx) fetch les données
const data = await BookService.getBook(bookId);
// Passe à ClientBookPage
<ClientBookPage bookId={bookId} initialData={{ ...data, nextBook }} />
// ClientClientBookPage refetch en client si pas de initialData
useEffect(() => {
if (!initialData) fetchBookData(); // Only if SSR failed
}, [bookId, initialData]);
```
**Action** : Supprimer le fetch client - les données sont déjà en props.
---
### 4. Garder pour l'instant
Ces routes nécessitent plus de refactoring :
- `GET /api/komga/favorites` - Utilisé dans des composants clients (Sidebar)
- `GET /api/admin/users` - AdminContent
- `GET /api/admin/stats` - AdminContent
Ces cas pourraient être résolus en passant les données depuis des Server Components parents.

149
docs/server-actions-plan.md Normal file
View File

@@ -0,0 +1,149 @@
# Plan de conversion API Routes → Server Actions
## État des lieux
### ✅ Converti
| Ancienne Route | Server Action | Status |
|----------------|---------------|--------|
| `PATCH /api/komga/books/[bookId]/read-progress` | `updateReadProgress()` | ✅ Done |
| `DELETE /api/komga/books/[bookId]/read-progress` | `deleteReadProgress()` | ✅ Done |
| `POST /api/komga/favorites` | `addToFavorites()` | ✅ Done |
| `DELETE /api/komga/favorites` | `removeFromFavorites()` | ✅ Done |
| `PUT /api/preferences` | `updatePreferences()` | ✅ Done |
| `POST /api/komga/libraries/[libraryId]/scan` | `scanLibrary()` | ✅ Done |
| `POST /api/komga/config` | `saveKomgaConfig()` | ✅ Done |
| `POST /api/komga/test` | `testKomgaConnection()` | ✅ Done |
| `PUT /api/user/password` | `changePassword()` | ✅ Done |
| `POST /api/auth/register` | `registerUser()` | ✅ Done |
| `PATCH /api/admin/users/[userId]` | `updateUserRoles()` | ✅ Done |
| `DELETE /api/admin/users/[userId]` | `deleteUser()` | ✅ Done |
| `PUT /api/admin/users/[userId]/password` | `resetUserPassword()` | ✅ Done |
---
## À convertir (priorité haute)
### 1. Scan de bibliothèque
**Route actuelle** : `api/komga/libraries/[libraryId]/scan/route.ts`
```typescript
// Action à créer : src/app/actions/library.ts
export async function scanLibrary(libraryId: string)
```
**Appelants à migrer** :
- `components/library/ScanButton.tsx` (POST fetch)
---
## À convertir (priorité moyenne)
### 2. Configuration Komga
**Route actuelle** : `api/komga/config/route.ts`
```typescript
// Action à créer : src/app/actions/config.ts
export async function saveKomgaConfig(config: KomgaConfigData)
export async function getKomgaConfig()
```
---
### 3. Mot de passe utilisateur
**Route actuelle** : `api/user/password/route.ts`
```typescript
// Action à créer : src/app/actions/password.ts
export async function changePassword(currentPassword: string, newPassword: string)
```
---
### 4. Inscription
**Route actuelle** : `api/auth/register/route.ts`
```typescript
// Action à créer : src/app/actions/auth.ts
export async function registerUser(email: string, password: string)
```
---
## À convertir (priorité basse - admin)
### 5. Gestion des utilisateurs (admin)
**Routes** :
- `api/admin/users/[userId]/route.ts` (PATCH, DELETE)
- `api/admin/users/[userId]/password/route.ts` (PUT)
```typescript
// Actions à créer : src/app/actions/admin.ts
export async function updateUserRoles(userId: string, roles: string[])
export async function deleteUser(userId: string)
export async function resetUserPassword(userId: string, newPassword: string)
```
---
## À garder en API Routes
Ces routes ne doivent PAS être converties :
| Route | Raison |
|-------|--------|
| `api/komga/home` | GET - called from Server Components |
| `api/komga/books/[bookId]` | GET - fetch données livre |
| `api/komga/series/*` | GET - fetch séries |
| `api/komga/libraries/*` | GET - fetch bibliothèques |
| `api/komga/random-book` | GET - fetch aléatoire |
| `api/komga/images/*` | GET - servir images (streaming) |
| `api/auth/[...nextauth]/*` | NextAuth handler externe |
| `api/admin/users` | GET - fetch liste users |
| `api/admin/stats` | GET - fetch stats |
| `api/user/profile` | GET - fetch profile |
---
## Pattern à suivre
```typescript
// src/app/actions/[feature].ts
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
import { Service } from "@/lib/services/service";
export async function actionName(params): Promise<{ success: boolean; message: string }> {
try {
await Service.doSomething(params);
// Invalider le cache si nécessaire
revalidateTag("cache-tag", "min");
revalidatePath("/");
return { success: true, message: "Succès" };
} catch (error) {
return { success: false, message: error.message };
}
}
```
```typescript
// src/components/feature/Component.tsx
"use client";
import { actionName } from "@/app/actions/feature";
const handleAction = async () => {
const result = await actionName(params);
if (!result.success) {
// handle error
}
};
```

72
src/app/actions/admin.ts Normal file
View File

@@ -0,0 +1,72 @@
"use server";
import { AdminService } from "@/lib/services/admin.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
import { AuthServerService } from "@/lib/services/auth-server.service";
/**
* Met à jour les rôles d'un utilisateur
*/
export async function updateUserRoles(
userId: string,
roles: string[]
): Promise<{ success: boolean; message: string }> {
try {
if (roles.length === 0) {
return { success: false, message: "L'utilisateur doit avoir au moins un rôle" };
}
await AdminService.updateUserRoles(userId, roles);
return { success: true, message: "Rôles mis à jour" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la mise à jour des rôles" };
}
}
/**
* Supprime un utilisateur
*/
export async function deleteUser(
userId: string
): Promise<{ success: boolean; message: string }> {
try {
await AdminService.deleteUser(userId);
return { success: true, message: "Utilisateur supprimé" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la suppression" };
}
}
/**
* Réinitialise le mot de passe d'un utilisateur
*/
export async function resetUserPassword(
userId: string,
newPassword: string
): Promise<{ success: boolean; message: string }> {
try {
if (!AuthServerService.isPasswordStrong(newPassword)) {
return {
success: false,
message: "Le mot de passe doit contenir au moins 8 caractères, une majuscule et un chiffre",
};
}
await AdminService.resetUserPassword(userId, newPassword);
return { success: true, message: "Mot de passe réinitialisé" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la réinitialisation du mot de passe" };
}
}

23
src/app/actions/auth.ts Normal file
View File

@@ -0,0 +1,23 @@
"use server";
import { AuthServerService } from "@/lib/services/auth-server.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
/**
* Inscrit un nouvel utilisateur
*/
export async function registerUser(
email: string,
password: string
): Promise<{ success: boolean; message: string }> {
try {
await AuthServerService.registerUser(email, password);
return { success: true, message: "Inscription réussie" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de l'inscription" };
}
}

74
src/app/actions/config.ts Normal file
View File

@@ -0,0 +1,74 @@
"use server";
import { revalidatePath } from "next/cache";
import { ConfigDBService } from "@/lib/services/config-db.service";
import { TestService } from "@/lib/services/test.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
import type { KomgaConfig, KomgaConfigData, KomgaLibrary } from "@/types/komga";
interface SaveConfigInput {
url: string;
username: string;
password?: string;
authHeader?: string;
}
/**
* Teste la connexion à Komga
*/
export async function testKomgaConnection(
serverUrl: string,
username: string,
password: string
): Promise<{ success: boolean; message: string }> {
try {
const authHeader = Buffer.from(`${username}:${password}`).toString("base64");
const { libraries }: { libraries: KomgaLibrary[] } = await TestService.testConnection({
serverUrl,
authHeader,
});
return {
success: true,
message: `Connexion réussie ! ${libraries.length} bibliothèque${libraries.length > 1 ? "s" : ""} trouvée${libraries.length > 1 ? "s" : ""}`,
};
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la connexion" };
}
}
/**
* Sauvegarde la configuration Komga
*/
export async function saveKomgaConfig(
config: SaveConfigInput
): Promise<{ success: boolean; message: string; data?: KomgaConfig }> {
try {
const configData: KomgaConfigData = {
url: config.url,
username: config.username,
password: config.password,
authHeader: config.authHeader || "",
};
const mongoConfig = await ConfigDBService.saveConfig(configData);
// Invalider le cache
revalidatePath("/settings");
return {
success: true,
message: "Configuration sauvegardée",
data: mongoConfig,
};
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la sauvegarde" };
}
}

View File

@@ -0,0 +1,39 @@
"use server";
import { FavoriteService } from "@/lib/services/favorite.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
/**
* Ajoute une série aux favoris
*/
export async function addToFavorites(
seriesId: string
): Promise<{ success: boolean; message: string }> {
try {
await FavoriteService.addToFavorites(seriesId);
return { success: true, message: "Série ajoutée aux favoris" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de l'ajout aux favoris" };
}
}
/**
* Retire une série des favoris
*/
export async function removeFromFavorites(
seriesId: string
): Promise<{ success: boolean; message: string }> {
try {
await FavoriteService.removeFromFavorites(seriesId);
return { success: true, message: "Série retirée des favoris" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la suppression des favoris" };
}
}

View File

@@ -0,0 +1,28 @@
"use server";
import { revalidatePath } from "next/cache";
import { LibraryService } from "@/lib/services/library.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
/**
* Lance un scan de bibliothèque
*/
export async function scanLibrary(
libraryId: string
): Promise<{ success: boolean; message: string }> {
try {
await LibraryService.scanLibrary(libraryId, false);
// Invalider le cache de la bibliothèque
revalidatePath(`/libraries/${libraryId}`);
revalidatePath("/libraries");
return { success: true, message: "Scan lancé" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors du scan" };
}
}

View File

@@ -0,0 +1,33 @@
"use server";
import { UserService } from "@/lib/services/user.service";
import { AuthServerService } from "@/lib/services/auth-server.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
/**
* Change le mot de passe de l'utilisateur
*/
export async function changePassword(
currentPassword: string,
newPassword: string
): Promise<{ success: boolean; message: string }> {
try {
// Vérifier que le nouveau mot de passe est fort
if (!AuthServerService.isPasswordStrong(newPassword)) {
return {
success: false,
message: "Le nouveau mot de passe doit contenir au moins 8 caractères, une majuscule et un chiffre",
};
}
await UserService.changePassword(currentPassword, newPassword);
return { success: true, message: "Mot de passe modifié avec succès" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors du changement de mot de passe" };
}
}

View File

@@ -0,0 +1,30 @@
"use server";
import { revalidatePath } from "next/cache";
import { PreferencesService } from "@/lib/services/preferences.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
import type { UserPreferences } from "@/types/preferences";
/**
* Met à jour les préférences utilisateur
*/
export async function updatePreferences(
newPreferences: Partial<UserPreferences>
): Promise<{ success: boolean; message: string; data?: UserPreferences }> {
try {
const updatedPreferences = await PreferencesService.updatePreferences(newPreferences);
// Invalider les pages qui utilisent les préférences
revalidatePath("/");
revalidatePath("/libraries");
revalidatePath("/series");
return { success: true, message: "Préférences mises à jour", data: updatedPreferences };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la mise à jour des préférences" };
}
}

View File

@@ -0,0 +1,53 @@
"use server";
import { revalidateTag } from "next/cache";
import { BookService } from "@/lib/services/book.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
const HOME_CACHE_TAG = "home-data";
/**
* Met à jour la progression de lecture d'un livre
* Note: ne pas utiliser "use server" avec redirect - on gère manuellement
*/
export async function updateReadProgress(
bookId: string,
page: number,
completed: boolean = false
): Promise<{ success: boolean; message: string }> {
try {
await BookService.updateReadProgress(bookId, page, completed);
// Invalider le cache de la home (sans refresh auto)
revalidateTag(HOME_CACHE_TAG, "min");
return { success: true, message: "Progression mise à jour" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la mise à jour" };
}
}
/**
* Supprime la progression de lecture d'un livre
*/
export async function deleteReadProgress(
bookId: string
): Promise<{ success: boolean; message: string }> {
try {
await BookService.deleteReadProgress(bookId);
// Invalider le cache de la home (sans refresh auto)
revalidateTag(HOME_CACHE_TAG, "min");
return { success: true, message: "Progression supprimée" };
} catch (error) {
if (error instanceof AppError) {
return { success: false, message: error.message };
}
return { success: false, message: "Erreur lors de la suppression" };
}
}

View File

@@ -1,59 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { AdminService } from "@/lib/services/admin.service";
import { AppError } from "@/utils/errors";
import { AuthServerService } from "@/lib/services/auth-server.service";
import logger from "@/lib/logger";
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ userId: string }> }
) {
try {
const { userId } = await params;
const body = await request.json();
const { newPassword } = body;
if (!newPassword) {
return NextResponse.json({ error: "Nouveau mot de passe manquant" }, { status: 400 });
}
// Vérifier que le mot de passe est fort
if (!AuthServerService.isPasswordStrong(newPassword)) {
return NextResponse.json(
{
error: "Le mot de passe doit contenir au moins 8 caractères, une majuscule et un chiffre",
},
{ status: 400 }
);
}
await AdminService.resetUserPassword(userId, newPassword);
return NextResponse.json({ success: true });
} catch (error) {
logger.error({ err: error }, "Erreur lors de la réinitialisation du mot de passe:");
if (error instanceof AppError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{
status:
error.code === "AUTH_FORBIDDEN"
? 403
: error.code === "AUTH_UNAUTHENTICATED"
? 401
: error.code === "AUTH_USER_NOT_FOUND"
? 404
: error.code === "ADMIN_CANNOT_RESET_OWN_PASSWORD"
? 400
: 500,
}
);
}
return NextResponse.json(
{ error: "Erreur lors de la réinitialisation du mot de passe" },
{ status: 500 }
);
}
}

View File

@@ -1,83 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { AdminService } from "@/lib/services/admin.service";
import { AppError } from "@/utils/errors";
import logger from "@/lib/logger";
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ userId: string }> }
) {
try {
const { userId } = await params;
const body = await request.json();
const { roles } = body;
if (!roles || !Array.isArray(roles)) {
return NextResponse.json({ error: "Rôles invalides" }, { status: 400 });
}
await AdminService.updateUserRoles(userId, roles);
return NextResponse.json({ success: true });
} catch (error) {
logger.error({ err: error }, "Erreur lors de la mise à jour de l'utilisateur:");
if (error instanceof AppError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{
status:
error.code === "AUTH_FORBIDDEN"
? 403
: error.code === "AUTH_UNAUTHENTICATED"
? 401
: error.code === "AUTH_USER_NOT_FOUND"
? 404
: 500,
}
);
}
return NextResponse.json(
{ error: "Erreur lors de la mise à jour de l'utilisateur" },
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ userId: string }> }
) {
try {
const { userId } = await params;
await AdminService.deleteUser(userId);
return NextResponse.json({ success: true });
} catch (error) {
logger.error({ err: error }, "Erreur lors de la suppression de l'utilisateur:");
if (error instanceof AppError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{
status:
error.code === "AUTH_FORBIDDEN"
? 403
: error.code === "AUTH_UNAUTHENTICATED"
? 401
: error.code === "AUTH_USER_NOT_FOUND"
? 404
: error.code === "ADMIN_CANNOT_DELETE_SELF"
? 400
: 500,
}
);
}
return NextResponse.json(
{ error: "Erreur lors de la suppression de l'utilisateur" },
{ status: 500 }
);
}
}

View File

@@ -1,55 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { AuthServerService } from "@/lib/services/auth-server.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { ERROR_MESSAGES } from "@/constants/errorMessages";
import { AppError } from "@/utils/errors";
import logger from "@/lib/logger";
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
if (!email || !password) {
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.INVALID_USER_DATA,
name: "Invalid user data",
message: ERROR_MESSAGES[ERROR_CODES.AUTH.INVALID_USER_DATA],
} as AppError,
},
{ status: 400 }
);
}
const userData = await AuthServerService.registerUser(email, password);
return NextResponse.json({ success: true, user: userData });
} catch (error) {
logger.error({ err: error }, "Registration error:");
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
name: error.name,
message: error.message,
} as AppError,
},
{ status: 400 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.REGISTRATION_FAILED,
name: "Registration failed",
message: ERROR_MESSAGES[ERROR_CODES.AUTH.REGISTRATION_FAILED],
} as AppError,
},
{ status: 500 }
);
}
}

View File

@@ -1,126 +0,0 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { BookService } from "@/lib/services/book.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { getErrorMessage } from "@/utils/errors";
import { AppError } from "@/utils/errors";
import logger from "@/lib/logger";
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ bookId: string }> }
) {
try {
const bookId: string = (await params).bookId;
// Handle empty or invalid body (can happen when request is aborted during navigation)
let body: { page?: unknown; completed?: boolean };
try {
const text = await request.text();
if (!text) {
return NextResponse.json(
{
error: {
code: ERROR_CODES.BOOK.PROGRESS_UPDATE_ERROR,
name: "Progress update error",
message: "Empty request body",
},
},
{ status: 400 }
);
}
body = JSON.parse(text);
} catch {
return NextResponse.json(
{
error: {
code: ERROR_CODES.BOOK.PROGRESS_UPDATE_ERROR,
name: "Progress update error",
message: "Invalid JSON body",
},
},
{ status: 400 }
);
}
const { page, completed } = body;
if (typeof page !== "number") {
return NextResponse.json(
{
error: {
code: ERROR_CODES.BOOK.PROGRESS_UPDATE_ERROR,
name: "Progress update error",
message: getErrorMessage(ERROR_CODES.BOOK.PROGRESS_UPDATE_ERROR),
},
},
{ status: 400 }
);
}
await BookService.updateReadProgress(bookId, page, completed);
return NextResponse.json({ message: "📖 Progression mise à jour avec succès" });
} catch (error) {
logger.error({ err: error }, "Erreur lors de la mise à jour de la progression:");
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
name: "Progress update error",
message: getErrorMessage(error.code),
},
},
{ status: 500 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.BOOK.PROGRESS_UPDATE_ERROR,
name: "Progress update error",
message: getErrorMessage(ERROR_CODES.BOOK.PROGRESS_UPDATE_ERROR),
},
},
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ bookId: string }> }
) {
try {
const bookId: string = (await params).bookId;
await BookService.deleteReadProgress(bookId);
return NextResponse.json({ message: "🗑️ Progression supprimée avec succès" });
} catch (error) {
logger.error({ err: error }, "Erreur lors de la suppression de la progression:");
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
name: "Progress delete error",
message: getErrorMessage(error.code),
},
},
{ status: 500 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.BOOK.PROGRESS_DELETE_ERROR,
name: "Progress delete error",
message: getErrorMessage(ERROR_CODES.BOOK.PROGRESS_DELETE_ERROR),
},
},
{ status: 500 }
);
}
}

View File

@@ -1,91 +0,0 @@
import { NextResponse } from "next/server";
import { ConfigDBService } from "@/lib/services/config-db.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import type { KomgaConfig, KomgaConfigData } from "@/types/komga";
import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
import logger from "@/lib/logger";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
try {
const data: KomgaConfigData = await request.json();
const mongoConfig: KomgaConfig = await ConfigDBService.saveConfig(data);
return NextResponse.json(
{ message: "⚙️ Configuration sauvegardée avec succès", mongoConfig },
{ status: 200 }
);
} catch (error) {
logger.error({ err: error }, "Erreur lors de la sauvegarde de la configuration:");
if (error instanceof Error && error.message === "Utilisateur non authentifié") {
return NextResponse.json(
{
error: {
code: ERROR_CODES.MIDDLEWARE.UNAUTHORIZED,
name: "Unauthorized",
message: getErrorMessage(ERROR_CODES.MIDDLEWARE.UNAUTHORIZED),
},
},
{ status: 401 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.CONFIG.SAVE_ERROR,
name: "Config save error",
message: getErrorMessage(ERROR_CODES.CONFIG.SAVE_ERROR),
},
},
{ status: 500 }
);
}
}
export async function GET() {
try {
const mongoConfig: KomgaConfig | null = await ConfigDBService.getConfig();
return NextResponse.json(mongoConfig, { status: 200 });
} catch (error) {
logger.error({ err: error }, "Erreur lors de la récupération de la configuration:");
if (error instanceof Error) {
if (error.message === "Utilisateur non authentifié") {
return NextResponse.json(
{
error: {
code: ERROR_CODES.MIDDLEWARE.UNAUTHORIZED,
name: "Unauthorized",
message: getErrorMessage(ERROR_CODES.MIDDLEWARE.UNAUTHORIZED),
},
},
{ status: 401 }
);
}
if (error.message === "Configuration non trouvée") {
return NextResponse.json(
{
error: {
code: ERROR_CODES.KOMGA.MISSING_CONFIG,
name: "Missing config",
message: getErrorMessage(ERROR_CODES.KOMGA.MISSING_CONFIG),
},
},
{ status: 404 }
);
}
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.CONFIG.FETCH_ERROR,
name: "Config fetch error",
message: getErrorMessage(ERROR_CODES.CONFIG.FETCH_ERROR),
},
},
{ status: 500 }
);
}
}

View File

@@ -4,9 +4,9 @@ import { SeriesService } from "@/lib/services/series.service";
import { ERROR_CODES } from "@/constants/errorCodes"; import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors"; import { AppError } from "@/utils/errors";
import { getErrorMessage } from "@/utils/errors"; import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
// GET reste utilisé par Sidebar et SeriesHeader pour récupérer la liste des favoris
export async function GET() { export async function GET() {
try { try {
const favoriteIds: string[] = await FavoriteService.getAllFavoriteIds(); const favoriteIds: string[] = await FavoriteService.getAllFavoriteIds();
@@ -61,67 +61,3 @@ export async function GET() {
); );
} }
} }
export async function POST(request: NextRequest) {
try {
const { seriesId }: { seriesId: string } = await request.json();
await FavoriteService.addToFavorites(seriesId);
return NextResponse.json({ message: "⭐️ Série ajoutée aux favoris" });
} catch (error) {
logger.error({ err: error }, "Erreur lors de l'ajout du favori:");
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
name: "Favorite add error",
message: getErrorMessage(error.code),
},
},
{ status: 500 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.FAVORITE.ADD_ERROR,
name: "Favorite add error",
message: getErrorMessage(ERROR_CODES.FAVORITE.ADD_ERROR),
},
},
{ status: 500 }
);
}
}
export async function DELETE(request: NextRequest) {
try {
const { seriesId }: { seriesId: string } = await request.json();
await FavoriteService.removeFromFavorites(seriesId);
return NextResponse.json({ message: "💔 Série retirée des favoris" });
} catch (error) {
logger.error({ err: error }, "Erreur lors de la suppression du favori:");
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
name: "Favorite delete error",
message: getErrorMessage(error.code),
},
},
{ status: 500 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.FAVORITE.DELETE_ERROR,
name: "Favorite delete error",
message: getErrorMessage(ERROR_CODES.FAVORITE.DELETE_ERROR),
},
},
{ status: 500 }
);
}
}

View File

@@ -1,45 +0,0 @@
import { NextResponse } from "next/server";
import { LibraryService } from "@/lib/services/library.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
import logger from "@/lib/logger";
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ libraryId: string }> }
) {
try {
const libraryId: string = (await params).libraryId;
// Scan library with deep=false
await LibraryService.scanLibrary(libraryId, false);
return NextResponse.json({ success: true });
} catch (error) {
logger.error({ err: error }, "API Library Scan - Erreur");
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
name: "Library scan error",
message: getErrorMessage(error.code),
},
},
{ status: 500 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.LIBRARY.SCAN_ERROR,
name: "Library scan error",
message: getErrorMessage(ERROR_CODES.LIBRARY.SCAN_ERROR),
},
},
{ status: 500 }
);
}
}

View File

@@ -1,37 +0,0 @@
import { NextResponse } from "next/server";
import { TestService } from "@/lib/services/test.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { getErrorMessage } from "@/utils/errors";
import type { KomgaLibrary } from "@/types/komga";
import type { NextRequest } from "next/server";
import logger from "@/lib/logger";
export async function POST(request: NextRequest) {
try {
const { serverUrl, username, password } = await request.json();
const authHeader = Buffer.from(`${username}:${password}`).toString("base64");
const { libraries }: { libraries: KomgaLibrary[] } = await TestService.testConnection({
serverUrl,
authHeader,
});
return NextResponse.json({
message: `✅ Connexion réussie ! ${libraries.length} bibliothèque${
libraries.length > 1 ? "s" : ""
} trouvée${libraries.length > 1 ? "s" : ""}`,
});
} catch (error) {
logger.error({ err: error }, "Erreur lors du test de connexion:");
return NextResponse.json(
{
error: {
code: ERROR_CODES.KOMGA.CONNECTION_ERROR,
name: "Connection error",
message: getErrorMessage(ERROR_CODES.KOMGA.CONNECTION_ERROR),
},
},
{ status: 400 }
);
}
}

View File

@@ -7,6 +7,7 @@ import type { UserPreferences } from "@/types/preferences";
import { getErrorMessage } from "@/utils/errors"; import { getErrorMessage } from "@/utils/errors";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
// GET reste utilisé par PreferencesContext pour récupérer les préférences
export async function GET() { export async function GET() {
try { try {
const preferences: UserPreferences = await PreferencesService.getPreferences(); const preferences: UserPreferences = await PreferencesService.getPreferences();
@@ -37,36 +38,3 @@ export async function GET() {
); );
} }
} }
export async function PUT(request: NextRequest) {
try {
const preferences: UserPreferences = await request.json();
const updatedPreferences: UserPreferences =
await PreferencesService.updatePreferences(preferences);
return NextResponse.json(updatedPreferences);
} catch (error) {
logger.error({ err: error }, "Erreur lors de la mise à jour des préférences:");
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
name: "Preferences update error",
code: error.code,
message: getErrorMessage(error.code),
},
},
{ status: 500 }
);
}
return NextResponse.json(
{
error: {
name: "Preferences update error",
code: ERROR_CODES.PREFERENCES.UPDATE_ERROR,
message: getErrorMessage(ERROR_CODES.PREFERENCES.UPDATE_ERROR),
},
},
{ status: 500 }
);
}
}

View File

@@ -1,52 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { UserService } from "@/lib/services/user.service";
import { AppError } from "@/utils/errors";
import { AuthServerService } from "@/lib/services/auth-server.service";
import logger from "@/lib/logger";
export async function PUT(request: NextRequest) {
try {
const body = await request.json();
const { currentPassword, newPassword } = body;
if (!currentPassword || !newPassword) {
return NextResponse.json({ error: "Mots de passe manquants" }, { status: 400 });
}
// Vérifier que le nouveau mot de passe est fort
if (!AuthServerService.isPasswordStrong(newPassword)) {
return NextResponse.json(
{
error:
"Le nouveau mot de passe doit contenir au moins 8 caractères, une majuscule et un chiffre",
},
{ status: 400 }
);
}
await UserService.changePassword(currentPassword, newPassword);
return NextResponse.json({ success: true });
} catch (error) {
logger.error({ err: error }, "Erreur lors du changement de mot de passe:");
if (error instanceof AppError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{
status:
error.code === "AUTH_INVALID_PASSWORD"
? 400
: error.code === "AUTH_UNAUTHENTICATED"
? 401
: 500,
}
);
}
return NextResponse.json(
{ error: "Erreur lors du changement de mot de passe" },
{ status: 500 }
);
}
}

View File

@@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { Lock } from "lucide-react"; import { Lock } from "lucide-react";
import { changePassword } from "@/app/actions/password";
export function ChangePasswordForm() { export function ChangePasswordForm() {
const [currentPassword, setCurrentPassword] = useState(""); const [currentPassword, setCurrentPassword] = useState("");
@@ -39,15 +40,10 @@ export function ChangePasswordForm() {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch("/api/user/password", { const result = await changePassword(currentPassword, newPassword);
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ currentPassword, newPassword }),
});
if (!response.ok) { if (!result.success) {
const data = await response.json(); throw new Error(result.message);
throw new Error(data.error || "Erreur lors du changement de mot de passe");
} }
toast({ toast({

View File

@@ -13,6 +13,7 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import type { AdminUserData } from "@/lib/services/admin.service"; import type { AdminUserData } from "@/lib/services/admin.service";
import { deleteUser } from "@/app/actions/admin";
interface DeleteUserDialogProps { interface DeleteUserDialogProps {
user: AdminUserData; user: AdminUserData;
@@ -29,13 +30,10 @@ export function DeleteUserDialog({ user, open, onOpenChange, onSuccess }: Delete
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch(`/api/admin/users/${user.id}`, { const result = await deleteUser(user.id);
method: "DELETE",
});
if (!response.ok) { if (!result.success) {
const data = await response.json(); throw new Error(result.message);
throw new Error(data.error || "Erreur lors de la suppression");
} }
toast({ toast({

View File

@@ -14,6 +14,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import type { AdminUserData } from "@/lib/services/admin.service"; import type { AdminUserData } from "@/lib/services/admin.service";
import { updateUserRoles } from "@/app/actions/admin";
interface EditUserDialogProps { interface EditUserDialogProps {
user: AdminUserData; user: AdminUserData;
@@ -51,15 +52,10 @@ export function EditUserDialog({ user, open, onOpenChange, onSuccess }: EditUser
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch(`/api/admin/users/${user.id}`, { const result = await updateUserRoles(user.id, selectedRoles);
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ roles: selectedRoles }),
});
if (!response.ok) { if (!result.success) {
const data = await response.json(); throw new Error(result.message);
throw new Error(data.error || "Erreur lors de la mise à jour");
} }
toast({ toast({

View File

@@ -15,6 +15,7 @@ import { Label } from "@/components/ui/label";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { Lock } from "lucide-react"; import { Lock } from "lucide-react";
import type { AdminUserData } from "@/lib/services/admin.service"; import type { AdminUserData } from "@/lib/services/admin.service";
import { resetUserPassword } from "@/app/actions/admin";
interface ResetPasswordDialogProps { interface ResetPasswordDialogProps {
user: AdminUserData; user: AdminUserData;
@@ -65,15 +66,10 @@ export function ResetPasswordDialog({
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch(`/api/admin/users/${user.id}/password`, { const result = await resetUserPassword(user.id, newPassword);
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ newPassword }),
});
if (!response.ok) { if (!result.success) {
const data = await response.json(); throw new Error(result.message);
throw new Error(data.error || "Erreur lors de la réinitialisation");
} }
toast({ toast({

View File

@@ -9,6 +9,7 @@ import type { AppErrorType } from "@/types/global";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { registerUser } from "@/app/actions/auth";
interface RegisterFormProps { interface RegisterFormProps {
from?: string; from?: string;
@@ -41,24 +42,15 @@ export function RegisterForm({ from: _from }: RegisterFormProps) {
} }
try { try {
// Étape 1: Inscription via l'API // Étape 1: Inscription via Server Action
const response = await fetch("/api/auth/register", { const result = await registerUser(email, password);
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) { if (!result.success) {
const data = await response.json(); setError({
setError( code: "AUTH_REGISTRATION_FAILED",
data.error || { name: "Registration failed",
code: "AUTH_REGISTRATION_FAILED", message: result.message,
name: "Registration failed", });
message: "Erreur lors de l'inscription",
}
);
return; return;
} }

View File

@@ -8,6 +8,7 @@ import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
import { scanLibrary } from "@/app/actions/library";
interface ScanButtonProps { interface ScanButtonProps {
libraryId: string; libraryId: string;
@@ -22,12 +23,10 @@ export function ScanButton({ libraryId }: ScanButtonProps) {
const handleScan = async () => { const handleScan = async () => {
setIsScanning(true); setIsScanning(true);
try { try {
const response = await fetch(`/api/komga/libraries/${libraryId}/scan`, { const result = await scanLibrary(libraryId);
method: "POST",
});
if (!response.ok) { if (!result.success) {
throw new Error("Failed to scan library"); throw new Error(result.message);
} }
toast({ toast({
@@ -35,14 +34,9 @@ export function ScanButton({ libraryId }: ScanButtonProps) {
description: t("library.scan.success.description"), description: t("library.scan.success.description"),
}); });
// Attendre 5 secondes pour que le scan se termine, puis invalider le cache et rafraîchir // Attendre 5 secondes pour que le scan se termine, puis rafraîchir
setTimeout(async () => { setTimeout(async () => {
try { try {
// Invalider le cache
await fetch(`/api/komga/libraries/${libraryId}/series`, {
method: "DELETE",
});
// Rafraîchir la page pour voir les changements // Rafraîchir la page pour voir les changements
router.refresh(); router.refresh();
@@ -52,7 +46,7 @@ export function ScanButton({ libraryId }: ScanButtonProps) {
description: t("library.scan.complete.description"), description: t("library.scan.complete.description"),
}); });
} catch (error) { } catch (error) {
logger.error({ err: error }, "Error invalidating cache after scan:"); logger.error({ err: error }, "Error refreshing after scan:");
toast({ toast({
variant: "destructive", variant: "destructive",
title: t("library.scan.error.title"), title: t("library.scan.error.title"),

View File

@@ -3,6 +3,7 @@ import { useRouter } from "next/navigation";
import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service"; import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.service";
import type { KomgaBook } from "@/types/komga"; import type { KomgaBook } from "@/types/komga";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
import { updateReadProgress } from "@/app/actions/read-progress";
interface UsePageNavigationProps { interface UsePageNavigationProps {
book: KomgaBook; book: KomgaBook;
@@ -48,11 +49,7 @@ export function usePageNavigation({
try { try {
ClientOfflineBookService.setCurrentPage(bookRef.current, page); ClientOfflineBookService.setCurrentPage(bookRef.current, page);
const completed = page === pagesLengthRef.current; const completed = page === pagesLengthRef.current;
await fetch(`/api/komga/books/${bookRef.current.id}/read-progress`, { await updateReadProgress(bookRef.current.id, page, completed);
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ page, completed }),
});
} catch (error) { } catch (error) {
logger.error({ err: error }, "Sync error:"); logger.error({ err: error }, "Sync error:");
} }

View File

@@ -13,6 +13,7 @@ import { SeriesCover } from "@/components/ui/series-cover";
import { StatusBadge } from "@/components/ui/status-badge"; import { StatusBadge } from "@/components/ui/status-badge";
import { IconButton } from "@/components/ui/icon-button"; import { IconButton } from "@/components/ui/icon-button";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
import { addToFavorites, removeFromFavorites } from "@/app/actions/favorites";
interface SeriesHeaderProps { interface SeriesHeaderProps {
series: KomgaSeries; series: KomgaSeries;
@@ -51,15 +52,10 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
const handleToggleFavorite = async () => { const handleToggleFavorite = async () => {
try { try {
const response = await fetch(`/api/komga/favorites`, { const action = isFavorite ? removeFromFavorites : addToFavorites;
method: isFavorite ? "DELETE" : "POST", const result = await action(series.id);
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ seriesId: series.id }),
});
if (response.ok) { if (result.success) {
setIsFavorite(!isFavorite); setIsFavorite(!isFavorite);
// Dispatcher l'événement avec le seriesId pour mise à jour optimiste de la sidebar // Dispatcher l'événement avec le seriesId pour mise à jour optimiste de la sidebar
const event = new CustomEvent("favoritesChanged", { const event = new CustomEvent("favoritesChanged", {
@@ -70,10 +66,6 @@ export const SeriesHeader = ({ series, refreshSeries }: SeriesHeaderProps) => {
title: t(isFavorite ? "series.header.favorite.remove" : "series.header.favorite.add"), title: t(isFavorite ? "series.header.favorite.remove" : "series.header.favorite.add"),
description: series.metadata.title, description: series.metadata.title,
}); });
} else if (response.status === 500) {
throw new AppError(ERROR_CODES.FAVORITE.SERVER_ERROR);
} else if (response.status === 404) {
throw new AppError(ERROR_CODES.FAVORITE.UPDATE_ERROR);
} else { } else {
throw new AppError( throw new AppError(
isFavorite ? ERROR_CODES.FAVORITE.DELETE_ERROR : ERROR_CODES.FAVORITE.ADD_ERROR isFavorite ? ERROR_CODES.FAVORITE.DELETE_ERROR : ERROR_CODES.FAVORITE.ADD_ERROR

View File

@@ -7,6 +7,7 @@ import { Network, Loader2 } from "lucide-react";
import type { KomgaConfig } from "@/types/komga"; import type { KomgaConfig } from "@/types/komga";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
import { saveKomgaConfig, testKomgaConnection } from "@/app/actions/config";
interface KomgaSettingsProps { interface KomgaSettingsProps {
initialConfig: KomgaConfig | null; initialConfig: KomgaConfig | null;
@@ -40,21 +41,10 @@ export function KomgaSettings({ initialConfig }: KomgaSettingsProps) {
const password = formData.get("password") as string; const password = formData.get("password") as string;
try { try {
const response = await fetch("/api/komga/test", { const result = await testKomgaConnection(serverUrl.trim(), username, password || config.password);
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
serverUrl: serverUrl.trim(),
username,
password: password || config.password,
}),
});
if (!response.ok) { if (!result.success) {
const data = await response.json(); throw new Error(result.message);
throw new Error(data.error || t("settings.komga.error.message"));
} }
toast({ toast({
@@ -90,31 +80,22 @@ export function KomgaSettings({ initialConfig }: KomgaSettingsProps) {
}; };
try { try {
const response = await fetch("/api/komga/config", { const result = await saveKomgaConfig({
method: "POST", url: newConfig.serverUrl,
headers: { username: newConfig.username,
"Content-Type": "application/json", password: newConfig.password,
},
body: JSON.stringify({
url: newConfig.serverUrl,
username: newConfig.username,
password: newConfig.password,
}),
}); });
if (!response.ok) { if (!result.success) {
const data = await response.json(); throw new Error(result.message);
throw new Error(data.error || t("settings.komga.error.message"));
} }
const savedConfig = await response.json();
setConfig(newConfig); setConfig(newConfig);
setLocalInitialConfig({ setLocalInitialConfig({
url: newConfig.serverUrl, url: newConfig.serverUrl,
username: newConfig.username, username: newConfig.username,
userId: savedConfig.userId, userId: result.data?.userId || 0,
authHeader: savedConfig.authHeader, authHeader: result.data?.authHeader || "",
}); });
setIsEditingConfig(false); setIsEditingConfig(false);

View File

@@ -7,6 +7,7 @@ import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.serv
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
import { updateReadProgress } from "@/app/actions/read-progress";
interface MarkAsReadButtonProps { interface MarkAsReadButtonProps {
bookId: string; bookId: string;
@@ -32,16 +33,11 @@ export function MarkAsReadButton({
setIsLoading(true); setIsLoading(true);
try { try {
ClientOfflineBookService.removeCurrentPageById(bookId); ClientOfflineBookService.removeCurrentPageById(bookId);
const response = await fetch(`/api/komga/books/${bookId}/read-progress`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ page: pagesCount, completed: true }),
});
if (!response.ok) { const result = await updateReadProgress(bookId, pagesCount, true);
throw new Error(t("books.actions.markAsRead.error.update"));
if (!result.success) {
throw new Error(result.message);
} }
toast({ toast({

View File

@@ -7,6 +7,7 @@ import { ClientOfflineBookService } from "@/lib/services/client-offlinebook.serv
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
import { deleteReadProgress } from "@/app/actions/read-progress";
interface MarkAsUnreadButtonProps { interface MarkAsUnreadButtonProps {
bookId: string; bookId: string;
@@ -23,12 +24,10 @@ export function MarkAsUnreadButton({ bookId, onSuccess, className }: MarkAsUnrea
e.stopPropagation(); // Empêcher la propagation au parent e.stopPropagation(); // Empêcher la propagation au parent
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch(`/api/komga/books/${bookId}/read-progress`, { const result = await deleteReadProgress(bookId);
method: "DELETE",
});
if (!response.ok) { if (!result.success) {
throw new Error(t("books.actions.markAsUnread.error.update")); throw new Error(result.message);
} }
// On supprime la page courante du localStorage seulement après que l'API a répondu // On supprime la page courante du localStorage seulement après que l'API a répondu

View File

@@ -7,10 +7,11 @@ import { AppError } from "../utils/errors";
import type { UserPreferences } from "@/types/preferences"; import type { UserPreferences } from "@/types/preferences";
import { defaultPreferences } from "@/types/preferences"; import { defaultPreferences } from "@/types/preferences";
import logger from "@/lib/logger"; import logger from "@/lib/logger";
import { updatePreferences as updatePreferencesAction } from "@/app/actions/preferences";
interface PreferencesContextType { interface PreferencesContextType {
preferences: UserPreferences; preferences: UserPreferences;
updatePreferences: (newPreferences: Partial<UserPreferences>) => Promise<void>; updatePreferences: (newPreferences: Partial<UserPreferences>) => Promise<UserPreferences | undefined>;
isLoading: boolean; isLoading: boolean;
} }
@@ -84,28 +85,22 @@ export function PreferencesProvider({
} }
}, [status, fetchPreferences, hasValidInitialPreferences]); }, [status, fetchPreferences, hasValidInitialPreferences]);
const updatePreferences = useCallback(async (newPreferences: Partial<UserPreferences>) => { const updatePreferences = useCallback(async (newPreferences: Partial<UserPreferences>): Promise<UserPreferences | undefined> => {
try { try {
const response = await fetch("/api/preferences", { const result = await updatePreferencesAction(newPreferences);
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newPreferences),
});
if (!response.ok) { if (!result.success) {
throw new AppError(ERROR_CODES.PREFERENCES.UPDATE_ERROR); throw new AppError(ERROR_CODES.PREFERENCES.UPDATE_ERROR);
} }
const updatedPreferences = await response.json(); if (result.data) {
setPreferences((prev) => ({
...prev,
...result.data,
}));
}
setPreferences((prev) => ({ return result.data;
...prev,
...updatedPreferences,
}));
return updatedPreferences;
} catch (error) { } catch (error) {
logger.error({ err: error }, "Erreur lors de la mise à jour des préférences"); logger.error({ err: error }, "Erreur lors de la mise à jour des préférences");
throw error; throw error;

View File

@@ -10,6 +10,8 @@ interface KomgaRequestInit extends RequestInit {
noJson?: boolean; noJson?: boolean;
/** Next.js cache duration in seconds. Use false to disable cache, number for TTL */ /** Next.js cache duration in seconds. Use false to disable cache, number for TTL */
revalidate?: number | false; revalidate?: number | false;
/** Cache tags for targeted invalidation */
tags?: string[];
} }
interface KomgaUrlBuilder { interface KomgaUrlBuilder {
@@ -137,10 +139,12 @@ export abstract class BaseApiService {
connectTimeout: timeoutMs, connectTimeout: timeoutMs,
bodyTimeout: timeoutMs, bodyTimeout: timeoutMs,
headersTimeout: timeoutMs, headersTimeout: timeoutMs,
// Next.js cache // Next.js cache with tags support
next: options.revalidate !== undefined next: options.tags
? { revalidate: options.revalidate } ? { tags: options.tags }
: undefined, : options.revalidate !== undefined
? { revalidate: options.revalidate }
: undefined,
}); });
} catch (fetchError: any) { } catch (fetchError: any) {
// Gestion spécifique des erreurs DNS // Gestion spécifique des erreurs DNS
@@ -158,10 +162,12 @@ export abstract class BaseApiService {
// Force IPv4 si IPv6 pose problème // Force IPv4 si IPv6 pose problème
// @ts-ignore // @ts-ignore
family: 4, family: 4,
// Next.js cache // Next.js cache with tags support
next: options.revalidate !== undefined next: options.tags
? { revalidate: options.revalidate } ? { tags: options.tags }
: undefined, : options.revalidate !== undefined
? { revalidate: options.revalidate }
: undefined,
}); });
} else if (fetchError?.cause?.code === "UND_ERR_CONNECT_TIMEOUT") { } else if (fetchError?.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
// Retry automatique sur timeout de connexion (cold start) // Retry automatique sur timeout de connexion (cold start)
@@ -175,10 +181,12 @@ export abstract class BaseApiService {
connectTimeout: timeoutMs, connectTimeout: timeoutMs,
bodyTimeout: timeoutMs, bodyTimeout: timeoutMs,
headersTimeout: timeoutMs, headersTimeout: timeoutMs,
// Next.js cache // Next.js cache with tags support
next: options.revalidate !== undefined next: options.tags
? { revalidate: options.revalidate } ? { tags: options.tags }
: undefined, : options.revalidate !== undefined
? { revalidate: options.revalidate }
: undefined,
}); });
} else { } else {
throw fetchError; throw fetchError;

View File

@@ -7,8 +7,12 @@ import { AppError } from "../../utils/errors";
export type { HomeData }; export type { HomeData };
// Cache tag pour invalidation ciblée
const HOME_CACHE_TAG = "home-data";
export class HomeService extends BaseApiService { export class HomeService extends BaseApiService {
private static readonly CACHE_TTL = 120; // 2 minutes private static readonly CACHE_TTL = 120; // 2 minutes fallback
private static readonly CACHE_TAG = HOME_CACHE_TAG;
static async getHomeData(): Promise<HomeData> { static async getHomeData(): Promise<HomeData> {
try { try {
@@ -25,7 +29,7 @@ export class HomeService extends BaseApiService {
}, },
}, },
{}, {},
{ revalidate: this.CACHE_TTL } { revalidate: this.CACHE_TTL, tags: [this.CACHE_TAG] }
), ),
this.fetchFromApi<LibraryResponse<KomgaBook>>( this.fetchFromApi<LibraryResponse<KomgaBook>>(
{ {
@@ -39,7 +43,7 @@ export class HomeService extends BaseApiService {
}, },
}, },
{}, {},
{ revalidate: this.CACHE_TTL } { revalidate: this.CACHE_TTL, tags: [this.CACHE_TAG] }
), ),
this.fetchFromApi<LibraryResponse<KomgaBook>>( this.fetchFromApi<LibraryResponse<KomgaBook>>(
{ {
@@ -51,7 +55,7 @@ export class HomeService extends BaseApiService {
}, },
}, },
{}, {},
{ revalidate: this.CACHE_TTL } { revalidate: this.CACHE_TTL, tags: [this.CACHE_TAG] }
), ),
this.fetchFromApi<LibraryResponse<KomgaBook>>( this.fetchFromApi<LibraryResponse<KomgaBook>>(
{ {
@@ -63,7 +67,7 @@ export class HomeService extends BaseApiService {
}, },
}, },
{}, {},
{ revalidate: this.CACHE_TTL } { revalidate: this.CACHE_TTL, tags: [this.CACHE_TAG] }
), ),
this.fetchFromApi<LibraryResponse<KomgaSeries>>( this.fetchFromApi<LibraryResponse<KomgaSeries>>(
{ {
@@ -75,7 +79,7 @@ export class HomeService extends BaseApiService {
}, },
}, },
{}, {},
{ revalidate: this.CACHE_TTL } { revalidate: this.CACHE_TTL, tags: [this.CACHE_TAG] }
), ),
]); ]);