Notifications corrections
This commit is contained in:
parent
0beb7eb815
commit
5397536572
438
ANALYSE_PAGES_S3.md
Normal file
438
ANALYSE_PAGES_S3.md
Normal file
@ -0,0 +1,438 @@
|
||||
# Analyse Complète : Page "Pages" et Intégration S3
|
||||
|
||||
## 📋 Vue d'ensemble
|
||||
|
||||
La page `/pages` est une application de gestion de notes et contacts (carnet) qui utilise un stockage S3-compatible (MinIO) pour persister les données utilisateur.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Structure des Routes
|
||||
|
||||
### Route Frontend
|
||||
- **URL** : `/pages`
|
||||
- **Fichier** : `app/pages/page.tsx`
|
||||
- **Type** : Client Component (Next.js App Router)
|
||||
- **Authentification** : Requise (redirection vers `/signin` si non authentifié)
|
||||
|
||||
### Routes API
|
||||
|
||||
#### 1. Routes Principales (`/api/storage/*`)
|
||||
- **`GET /api/storage/status`** : Liste les dossiers disponibles pour l'utilisateur
|
||||
- **`POST /api/storage/init`** : Initialise la structure de dossiers pour un nouvel utilisateur
|
||||
- **`POST /api/storage/init/folder`** : Crée un dossier spécifique
|
||||
- **`GET /api/storage/files?folder={folder}`** : Liste les fichiers d'un dossier
|
||||
- **`GET /api/storage/files/content?path={path}`** : Récupère le contenu d'un fichier
|
||||
- **`POST /api/storage/files`** : Crée un nouveau fichier
|
||||
- **`PUT /api/storage/files`** : Met à jour un fichier existant
|
||||
- **`DELETE /api/storage/files?id={id}`** : Supprime un fichier
|
||||
|
||||
#### 2. Routes de Compatibilité (`/api/nextcloud/*`)
|
||||
- **Adapter Pattern** : Les routes `/api/nextcloud/*` redirigent vers `/api/storage/*`
|
||||
- **Raison** : Compatibilité avec l'ancien code qui utilisait NextCloud
|
||||
- **Fichiers** :
|
||||
- `app/api/nextcloud/status/route.ts` → Retourne les dossiers standards
|
||||
- `app/api/nextcloud/files/route.ts` → Redirige vers `/api/storage/files`
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Composants Principaux
|
||||
|
||||
### 1. **Page Principale** (`app/pages/page.tsx`)
|
||||
|
||||
**Responsabilités** :
|
||||
- Gestion de l'état global (notes, contacts, dossiers sélectionnés)
|
||||
- Orchestration des trois panneaux (Navigation, Liste, Éditeur)
|
||||
- Gestion du cache multi-niveaux
|
||||
- Gestion responsive (mobile/tablette/desktop)
|
||||
|
||||
**États Principaux** :
|
||||
```typescript
|
||||
- selectedFolder: string (Notes, Diary, Health, Contacts)
|
||||
- selectedNote: Note | null
|
||||
- selectedContact: Contact | null
|
||||
- notes: Note[]
|
||||
- contacts: Contact[]
|
||||
- nextcloudFolders: string[]
|
||||
```
|
||||
|
||||
**Cache** :
|
||||
- **In-memory** : `notesCache`, `noteContentCache`, `foldersCache`
|
||||
- **localStorage** : Cache persistant avec expiration (5-15 minutes)
|
||||
|
||||
### 2. **Navigation** (`components/carnet/navigation.tsx`)
|
||||
|
||||
**Fonctionnalités** :
|
||||
- Affichage des dossiers (Notes, Diary, Health, Contacts)
|
||||
- Recherche de dossiers
|
||||
- Expansion du dossier Contacts pour afficher les fichiers VCF
|
||||
- Icônes contextuelles par type de dossier
|
||||
|
||||
**Dossiers Standards** :
|
||||
- `Notes` → Bloc-notes
|
||||
- `Diary` → Journal
|
||||
- `Health` → Carnet de santé
|
||||
- `Contacts` → Carnet d'adresses
|
||||
|
||||
### 3. **NotesView** (`components/carnet/notes-view.tsx`)
|
||||
|
||||
**Fonctionnalités** :
|
||||
- Liste des notes avec recherche
|
||||
- Tri par date de modification
|
||||
- Affichage formaté des dates
|
||||
- Actions : Créer, Supprimer, Sélectionner
|
||||
- Formatage spécial pour Diary/Health (extraction de dates)
|
||||
|
||||
### 4. **ContactsView** (`components/carnet/contacts-view.tsx`)
|
||||
|
||||
**Fonctionnalités** :
|
||||
- Liste des contacts avec recherche
|
||||
- Filtrage par nom, email, organisation
|
||||
- Support des fichiers VCF multiples
|
||||
- Création de nouveaux contacts
|
||||
|
||||
### 5. **Editor** (`components/carnet/editor.tsx`)
|
||||
|
||||
**Fonctionnalités** :
|
||||
- Édition de notes en Markdown
|
||||
- Sauvegarde automatique (debounce 1 seconde)
|
||||
- Cache du contenu pour performance
|
||||
- Gestion des erreurs et états de chargement
|
||||
|
||||
**Sauvegarde** :
|
||||
- Auto-save après 1 seconde d'inactivité
|
||||
- Utilise `PUT /api/storage/files` pour les mises à jour
|
||||
- Invalide le cache après sauvegarde
|
||||
|
||||
### 6. **ContactDetails** (`components/carnet/contact-details.tsx`)
|
||||
|
||||
**Fonctionnalités** :
|
||||
- Affichage détaillé d'un contact
|
||||
- Édition inline des champs
|
||||
- Support VCard (vCard 3.0)
|
||||
- Sauvegarde dans fichiers VCF
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Configuration S3
|
||||
|
||||
### Fichier de Configuration (`lib/s3.ts`)
|
||||
|
||||
```typescript
|
||||
S3_CONFIG = {
|
||||
endpoint: 'https://dome-api.slm-lab.net',
|
||||
region: 'us-east-1',
|
||||
bucket: process.env.S3_BUCKET || 'pages',
|
||||
accessKey: '4aBT4CMb7JIMMyUtp4Pl',
|
||||
secretKey: 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg'
|
||||
}
|
||||
```
|
||||
|
||||
**Type de Stockage** : MinIO (S3-compatible)
|
||||
**Client** : AWS SDK v3 (`@aws-sdk/client-s3`)
|
||||
|
||||
### Structure de Stockage
|
||||
|
||||
```
|
||||
bucket: pages/
|
||||
└── user-{userId}/
|
||||
├── notes/
|
||||
│ ├── note1.md
|
||||
│ └── note2.md
|
||||
├── diary/
|
||||
│ └── 2024-01-15-entry.md
|
||||
├── health/
|
||||
│ └── health-record.md
|
||||
└── contacts/
|
||||
├── Allemanique.vcf
|
||||
└── contacts.vcf
|
||||
```
|
||||
|
||||
**Format des Clés S3** :
|
||||
- Notes : `user-{userId}/{folder}/{title}.md`
|
||||
- Contacts : `user-{userId}/contacts/{filename}.vcf`
|
||||
|
||||
### Fonctions S3 Principales
|
||||
|
||||
#### `putObject(key, content, contentType)`
|
||||
- Upload un fichier vers S3
|
||||
- Convertit automatiquement les strings en Buffer UTF-8
|
||||
- Retourne la clé du fichier créé
|
||||
|
||||
#### `getObjectContent(key)`
|
||||
- Récupère le contenu d'un fichier
|
||||
- Stream le contenu et le convertit en string UTF-8
|
||||
- Retourne `null` si le fichier n'existe pas
|
||||
|
||||
#### `deleteObject(key)`
|
||||
- Supprime un fichier de S3
|
||||
- Utilise `DeleteObjectCommand`
|
||||
|
||||
#### `listUserObjects(userId, folder)`
|
||||
- Liste les objets d'un dossier utilisateur
|
||||
- Filtre les placeholders et dossiers vides
|
||||
- Retourne métadonnées (nom, taille, date de modification)
|
||||
|
||||
#### `createUserFolderStructure(userId)`
|
||||
- Crée la structure de dossiers standard
|
||||
- Crée des fichiers `.placeholder` pour chaque dossier
|
||||
- Dossiers créés : `notes`, `diary`, `health`, `contacts`
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Flux de Données
|
||||
|
||||
### 1. Initialisation
|
||||
|
||||
```
|
||||
User Login
|
||||
↓
|
||||
POST /api/storage/init
|
||||
↓
|
||||
createUserFolderStructure(userId)
|
||||
↓
|
||||
Création des dossiers dans S3
|
||||
↓
|
||||
GET /api/storage/status
|
||||
↓
|
||||
Affichage des dossiers dans Navigation
|
||||
```
|
||||
|
||||
### 2. Chargement des Notes
|
||||
|
||||
```
|
||||
User sélectionne un dossier
|
||||
↓
|
||||
Check cache (in-memory → localStorage)
|
||||
↓
|
||||
Si cache valide → Utiliser cache
|
||||
↓
|
||||
Sinon → GET /api/storage/files?folder={folder}
|
||||
↓
|
||||
listUserObjects(userId, folder)
|
||||
↓
|
||||
Mise à jour du cache + Affichage
|
||||
```
|
||||
|
||||
### 3. Édition d'une Note
|
||||
|
||||
```
|
||||
User sélectionne une note
|
||||
↓
|
||||
Check cache du contenu
|
||||
↓
|
||||
Si cache valide → Afficher
|
||||
↓
|
||||
Sinon → GET /api/storage/files/content?path={id}
|
||||
↓
|
||||
getObjectContent(key)
|
||||
↓
|
||||
Affichage dans Editor
|
||||
```
|
||||
|
||||
### 4. Sauvegarde d'une Note
|
||||
|
||||
```
|
||||
User modifie le contenu
|
||||
↓
|
||||
Debounce 1 seconde
|
||||
↓
|
||||
PUT /api/storage/files
|
||||
↓
|
||||
putObject(key, content, 'text/markdown')
|
||||
↓
|
||||
Invalidation du cache
|
||||
↓
|
||||
Rafraîchissement de la liste
|
||||
```
|
||||
|
||||
### 5. Gestion des Contacts
|
||||
|
||||
```
|
||||
User sélectionne "Contacts"
|
||||
↓
|
||||
GET /api/storage/files?folder=contacts
|
||||
↓
|
||||
Filtrage des fichiers .vcf
|
||||
↓
|
||||
Pour chaque VCF → GET /api/storage/files/content
|
||||
↓
|
||||
Parsing VCard avec vcard-parser
|
||||
↓
|
||||
Affichage dans ContactsView
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Architecture UI
|
||||
|
||||
### Layout Responsive
|
||||
|
||||
**Desktop (> 1024px)** :
|
||||
- 3 panneaux : Navigation | Liste | Éditeur
|
||||
- Panneaux redimensionnables avec `PanelResizer`
|
||||
|
||||
**Tablette (768px - 1024px)** :
|
||||
- 2 panneaux : Navigation | Liste/Éditeur
|
||||
- Navigation toujours visible
|
||||
|
||||
**Mobile (< 768px)** :
|
||||
- 1 panneau à la fois
|
||||
- Bouton toggle pour navigation
|
||||
- Navigation overlay
|
||||
|
||||
### Système de Cache
|
||||
|
||||
**Niveaux de Cache** :
|
||||
1. **In-memory** : `useRef` pour performance immédiate
|
||||
2. **localStorage** : Persistance entre sessions
|
||||
3. **S3** : Source de vérité
|
||||
|
||||
**Expiration** :
|
||||
- Liste des notes : 5 minutes
|
||||
- Contenu des notes : 15 minutes
|
||||
- Liste des dossiers : 2 minutes
|
||||
|
||||
**Invalidation** :
|
||||
- Après sauvegarde d'une note
|
||||
- Après création/suppression
|
||||
- Après événement `note-saved` (CustomEvent)
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Sécurité
|
||||
|
||||
### Authentification
|
||||
- **NextAuth** : Vérification de session sur toutes les routes API
|
||||
- **Isolation utilisateur** : Tous les chemins S3 incluent `user-{userId}/`
|
||||
- **Validation** : Vérification que l'utilisateur ne peut accéder qu'à ses propres fichiers
|
||||
|
||||
### Exemple de Validation
|
||||
```typescript
|
||||
// Dans /api/storage/files/content/route.ts
|
||||
if (!key.startsWith(`user-${userId}/`)) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Formats de Fichiers
|
||||
|
||||
### Notes
|
||||
- **Format** : Markdown (`.md`)
|
||||
- **MIME Type** : `text/markdown`
|
||||
- **Structure** : Texte libre avec support Markdown
|
||||
|
||||
### Contacts
|
||||
- **Format** : vCard 3.0 (`.vcf`)
|
||||
- **MIME Type** : `text/vcard`
|
||||
- **Bibliothèque** : `vcard-parser` pour parsing/formatting
|
||||
- **Structure** : Multiple vCards dans un fichier
|
||||
|
||||
**Exemple VCard** :
|
||||
```
|
||||
BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
UID:{id}
|
||||
FN:{fullName}
|
||||
EMAIL;TYPE=INTERNET:{email}
|
||||
TEL;TYPE=CELL:{phone}
|
||||
ORG:{organization}
|
||||
ADR:{address}
|
||||
NOTE:{notes}
|
||||
END:VCARD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Points d'Attention
|
||||
|
||||
### 1. **Compatibilité NextCloud/S3**
|
||||
- Le code utilise parfois `/api/nextcloud/*` et parfois `/api/storage/*`
|
||||
- Les routes NextCloud sont des adapters qui redirigent vers Storage
|
||||
- **Recommandation** : Migrer progressivement vers `/api/storage/*` uniquement
|
||||
|
||||
### 2. **Gestion de la Casse**
|
||||
- Les noms de dossiers sont normalisés en lowercase pour S3
|
||||
- Mais l'affichage utilise la casse originale (Notes, Diary, etc.)
|
||||
- **Risque** : Incohérences si un dossier est créé avec une casse différente
|
||||
|
||||
### 3. **Cache Multi-Niveaux**
|
||||
- Complexité de synchronisation entre caches
|
||||
- **Risque** : Données obsolètes si invalidation incomplète
|
||||
|
||||
### 4. **Credentials S3 en Dur**
|
||||
- Les clés d'accès S3 sont hardcodées dans `lib/s3.ts`
|
||||
- **Recommandation** : Utiliser des variables d'environnement
|
||||
|
||||
### 5. **Gestion des Erreurs**
|
||||
- Certaines erreurs sont loggées mais pas toujours remontées à l'utilisateur
|
||||
- **Recommandation** : Améliorer le feedback utilisateur
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Améliorations Suggérées
|
||||
|
||||
### 1. **Sécurité**
|
||||
- [ ] Déplacer les credentials S3 vers variables d'environnement
|
||||
- [ ] Implémenter des rate limits sur les API
|
||||
- [ ] Ajouter une validation plus stricte des chemins
|
||||
|
||||
### 2. **Performance**
|
||||
- [ ] Implémenter un système de pagination pour les grandes listes
|
||||
- [ ] Optimiser les requêtes S3 avec des préfixes plus spécifiques
|
||||
- [ ] Ajouter un système de compression pour les gros fichiers
|
||||
|
||||
### 3. **UX**
|
||||
- [ ] Ajouter des indicateurs de synchronisation
|
||||
- [ ] Implémenter un système de conflits pour les éditions simultanées
|
||||
- [ ] Améliorer les messages d'erreur utilisateur
|
||||
|
||||
### 4. **Architecture**
|
||||
- [ ] Unifier les routes API (supprimer les adapters NextCloud)
|
||||
- [ ] Créer un service layer pour abstraire S3
|
||||
- [ ] Implémenter des tests unitaires pour les fonctions S3
|
||||
|
||||
---
|
||||
|
||||
## 📊 Métriques et Monitoring
|
||||
|
||||
### Endpoints à Monitorer
|
||||
- Temps de réponse des requêtes S3
|
||||
- Taux d'erreur des opérations CRUD
|
||||
- Utilisation du cache (hit rate)
|
||||
- Taille des fichiers uploadés
|
||||
|
||||
### Logs Importants
|
||||
- Erreurs d'authentification
|
||||
- Échecs de connexion S3
|
||||
- Échecs de parsing VCard
|
||||
- Incohérences de cache
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Dépendances Clés
|
||||
|
||||
```json
|
||||
{
|
||||
"@aws-sdk/client-s3": "^3.802.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.802.0",
|
||||
"vcard-parser": "^x.x.x",
|
||||
"next-auth": "^x.x.x"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Références
|
||||
|
||||
- **Fichier S3 Config** : `lib/s3.ts`
|
||||
- **Page Principale** : `app/pages/page.tsx`
|
||||
- **Routes API Storage** : `app/api/storage/**/*.ts`
|
||||
- **Routes API NextCloud** : `app/api/nextcloud/**/*.ts`
|
||||
- **Composants** : `components/carnet/*.tsx`
|
||||
|
||||
---
|
||||
|
||||
*Document généré le : $(date)*
|
||||
*Dernière mise à jour : Analyse complète du système Pages/S3*
|
||||
@ -3,6 +3,33 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from "@/app/api/auth/options";
|
||||
import { getObjectContent } from '@/lib/s3';
|
||||
|
||||
// Error types for better error handling
|
||||
enum StorageError {
|
||||
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||
S3_ERROR = 'S3_ERROR',
|
||||
INTERNAL_ERROR = 'INTERNAL_ERROR'
|
||||
}
|
||||
|
||||
// Helper function to create error response
|
||||
function createErrorResponse(
|
||||
error: StorageError,
|
||||
message: string,
|
||||
status: number,
|
||||
details?: any
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error,
|
||||
message: message,
|
||||
...(details && { details })
|
||||
},
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to check authentication
|
||||
async function checkAuth(request: Request) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@ -21,7 +48,11 @@ export async function GET(request: Request) {
|
||||
try {
|
||||
const { authorized, userId } = await checkAuth(request);
|
||||
if (!authorized || !userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
return createErrorResponse(
|
||||
StorageError.UNAUTHORIZED,
|
||||
'Authentication required',
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
@ -29,7 +60,12 @@ export async function GET(request: Request) {
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!path && !id) {
|
||||
return NextResponse.json({ error: 'Path or ID parameter is required' }, { status: 400 });
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Path or ID parameter is required',
|
||||
400,
|
||||
{ missing: 'path or id' }
|
||||
);
|
||||
}
|
||||
|
||||
// Determine the key to use
|
||||
@ -42,7 +78,12 @@ export async function GET(request: Request) {
|
||||
// Ensure the user can only access their own files
|
||||
if (!key.startsWith(`user-${userId}/`)) {
|
||||
console.error('Unauthorized file access attempt:', { userId, fileId: id });
|
||||
return NextResponse.json({ error: 'Unauthorized access to file' }, { status: 403 });
|
||||
return createErrorResponse(
|
||||
StorageError.FORBIDDEN,
|
||||
'Unauthorized access to file',
|
||||
403,
|
||||
{ fileId: id }
|
||||
);
|
||||
}
|
||||
} else if (path) {
|
||||
// If a path is provided, ensure it contains the user's ID
|
||||
@ -65,28 +106,79 @@ export async function GET(request: Request) {
|
||||
console.log('Converted NextCloud path to S3 key:', { path, key });
|
||||
} else {
|
||||
console.error('Unauthorized file access attempt:', { userId, filePath: path });
|
||||
return NextResponse.json({ error: 'Unauthorized access to file' }, { status: 403 });
|
||||
return createErrorResponse(
|
||||
StorageError.FORBIDDEN,
|
||||
'Unauthorized access to file',
|
||||
403,
|
||||
{ filePath: path }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If it already contains user ID, use the path directly
|
||||
key = path;
|
||||
}
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 });
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Invalid parameters',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Validate key format (prevent path traversal)
|
||||
if (key.includes('..') || key.includes('//')) {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Invalid file path',
|
||||
400,
|
||||
{ key }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Fetching file content from S3:', { key });
|
||||
|
||||
// Get the file content
|
||||
const content = await getObjectContent(key);
|
||||
|
||||
if (!content) {
|
||||
return NextResponse.json({ error: 'File not found' }, { status: 404 });
|
||||
try {
|
||||
// Get the file content
|
||||
const content = await getObjectContent(key);
|
||||
|
||||
if (!content) {
|
||||
return createErrorResponse(
|
||||
StorageError.NOT_FOUND,
|
||||
'File not found',
|
||||
404,
|
||||
{ key }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ content });
|
||||
} catch (s3Error) {
|
||||
console.error('S3 error fetching file content:', s3Error);
|
||||
// Check if file doesn't exist
|
||||
if (s3Error instanceof Error && s3Error.message.includes('NoSuchKey')) {
|
||||
return createErrorResponse(
|
||||
StorageError.NOT_FOUND,
|
||||
'File not found',
|
||||
404,
|
||||
{ key }
|
||||
);
|
||||
}
|
||||
return createErrorResponse(
|
||||
StorageError.S3_ERROR,
|
||||
'Failed to fetch file content from storage',
|
||||
503,
|
||||
{
|
||||
error: s3Error instanceof Error ? s3Error.message : String(s3Error),
|
||||
key
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ content });
|
||||
} catch (error) {
|
||||
console.error('Error fetching file content:', error);
|
||||
return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
||||
return createErrorResponse(
|
||||
StorageError.INTERNAL_ERROR,
|
||||
'An unexpected error occurred',
|
||||
500,
|
||||
{ error: error instanceof Error ? error.message : String(error) }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,33 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from "@/app/api/auth/options";
|
||||
import { listUserObjects, putObject, deleteObject } from '@/lib/s3';
|
||||
|
||||
// Error types for better error handling
|
||||
enum StorageError {
|
||||
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||
S3_ERROR = 'S3_ERROR',
|
||||
INTERNAL_ERROR = 'INTERNAL_ERROR'
|
||||
}
|
||||
|
||||
// Helper function to create error response
|
||||
function createErrorResponse(
|
||||
error: StorageError,
|
||||
message: string,
|
||||
status: number,
|
||||
details?: any
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error,
|
||||
message: message,
|
||||
...(details && { details })
|
||||
},
|
||||
{ status }
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to check authentication
|
||||
async function checkAuth(request: Request) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@ -22,14 +49,33 @@ export async function GET(request: Request) {
|
||||
try {
|
||||
const { authorized, userId } = await checkAuth(request);
|
||||
if (!authorized || !userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
return createErrorResponse(
|
||||
StorageError.UNAUTHORIZED,
|
||||
'Authentication required',
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const folderParam = searchParams.get('folder');
|
||||
|
||||
if (!folderParam) {
|
||||
return NextResponse.json({ error: 'Folder parameter is required' }, { status: 400 });
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Folder parameter is required',
|
||||
400,
|
||||
{ missing: 'folder' }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate folder name (prevent path traversal)
|
||||
if (folderParam.includes('..') || folderParam.includes('/')) {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Invalid folder name',
|
||||
400,
|
||||
{ folder: folderParam }
|
||||
);
|
||||
}
|
||||
|
||||
// Try both lowercase and original case to maintain compatibility
|
||||
@ -38,19 +84,37 @@ export async function GET(request: Request) {
|
||||
|
||||
console.log(`Listing files for user ${userId} in folder: ${folderParam} (normalized: ${normalizedFolder})`);
|
||||
|
||||
// First try with the exact folder name as provided
|
||||
let files = await listUserObjects(userId, folderParam);
|
||||
|
||||
// If no files found with original case, try with lowercase
|
||||
if (files.length === 0 && folderParam !== normalizedFolder) {
|
||||
console.log(`No files found with original case, trying lowercase: ${normalizedFolder}`);
|
||||
files = await listUserObjects(userId, normalizedFolder);
|
||||
try {
|
||||
// First try with the exact folder name as provided
|
||||
let files = await listUserObjects(userId, folderParam);
|
||||
|
||||
// If no files found with original case, try with lowercase
|
||||
if (files.length === 0 && folderParam !== normalizedFolder) {
|
||||
console.log(`No files found with original case, trying lowercase: ${normalizedFolder}`);
|
||||
files = await listUserObjects(userId, normalizedFolder);
|
||||
}
|
||||
|
||||
return NextResponse.json(files);
|
||||
} catch (s3Error) {
|
||||
console.error('S3 error listing files:', s3Error);
|
||||
return createErrorResponse(
|
||||
StorageError.S3_ERROR,
|
||||
'Failed to list files from storage',
|
||||
503,
|
||||
{
|
||||
error: s3Error instanceof Error ? s3Error.message : String(s3Error),
|
||||
folder: folderParam
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(files);
|
||||
} catch (error) {
|
||||
console.error('Error listing files:', error);
|
||||
return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
||||
return createErrorResponse(
|
||||
StorageError.INTERNAL_ERROR,
|
||||
'An unexpected error occurred',
|
||||
500,
|
||||
{ error: error instanceof Error ? error.message : String(error) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,32 +123,104 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
const { authorized, userId } = await checkAuth(request);
|
||||
if (!authorized || !userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
return createErrorResponse(
|
||||
StorageError.UNAUTHORIZED,
|
||||
'Authentication required',
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch (parseError) {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Invalid JSON in request body',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const { title, content, folder } = body;
|
||||
|
||||
if (!title || !content || !folder) {
|
||||
return NextResponse.json({ error: 'Missing required fields', received: { title: !!title, content: !!content, folder: !!folder } }, { status: 400 });
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Missing required fields',
|
||||
400,
|
||||
{
|
||||
received: {
|
||||
title: !!title,
|
||||
content: !!content,
|
||||
folder: !!folder
|
||||
},
|
||||
required: ['title', 'content', 'folder']
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
if (typeof title !== 'string' || title.trim().length === 0) {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Title must be a non-empty string',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof content !== 'string') {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Content must be a string',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Validate folder name (prevent path traversal)
|
||||
if (folder.includes('..') || folder.includes('/')) {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Invalid folder name',
|
||||
400,
|
||||
{ folder }
|
||||
);
|
||||
}
|
||||
|
||||
// Normalize folder name
|
||||
const normalizedFolder = folder.toLowerCase();
|
||||
|
||||
// Sanitize title (remove dangerous characters)
|
||||
const sanitizedTitle = title.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
|
||||
// Create the full key (path) for the S3 object
|
||||
// Remove 'pages/' prefix since it's already the bucket name
|
||||
const key = `user-${userId}/${normalizedFolder}/${title}${title.endsWith('.md') ? '' : '.md'}`;
|
||||
const key = `user-${userId}/${normalizedFolder}/${sanitizedTitle}${sanitizedTitle.endsWith('.md') ? '' : '.md'}`;
|
||||
|
||||
console.log('Creating file in S3:', { key, contentLength: content.length });
|
||||
|
||||
// Save the file to S3
|
||||
const file = await putObject(key, content);
|
||||
|
||||
return NextResponse.json(file);
|
||||
try {
|
||||
// Save the file to S3
|
||||
const file = await putObject(key, content);
|
||||
return NextResponse.json(file);
|
||||
} catch (s3Error) {
|
||||
console.error('S3 error creating file:', s3Error);
|
||||
return createErrorResponse(
|
||||
StorageError.S3_ERROR,
|
||||
'Failed to create file in storage',
|
||||
503,
|
||||
{
|
||||
error: s3Error instanceof Error ? s3Error.message : String(s3Error),
|
||||
key
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating file:', error);
|
||||
return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
||||
return createErrorResponse(
|
||||
StorageError.INTERNAL_ERROR,
|
||||
'An unexpected error occurred',
|
||||
500,
|
||||
{ error: error instanceof Error ? error.message : String(error) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,10 +229,24 @@ export async function PUT(request: Request) {
|
||||
try {
|
||||
const { authorized, userId } = await checkAuth(request);
|
||||
if (!authorized || !userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
return createErrorResponse(
|
||||
StorageError.UNAUTHORIZED,
|
||||
'Authentication required',
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch (parseError) {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Invalid JSON in request body',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const { id, title, content, folder, mime } = body;
|
||||
|
||||
// Check if this is using the direct id (key) or needs to construct one
|
||||
@ -105,28 +255,75 @@ export async function PUT(request: Request) {
|
||||
if (id) {
|
||||
// Ensure the user can only access their own files
|
||||
if (!id.includes(`user-${userId}/`)) {
|
||||
return NextResponse.json({ error: 'Unauthorized access to file' }, { status: 403 });
|
||||
return createErrorResponse(
|
||||
StorageError.FORBIDDEN,
|
||||
'Unauthorized access to file',
|
||||
403,
|
||||
{ fileId: id }
|
||||
);
|
||||
}
|
||||
key = id;
|
||||
} else {
|
||||
// If id is not provided, construct it from folder and title
|
||||
if (!title || !folder) {
|
||||
return NextResponse.json({ error: 'Missing required fields', received: { title: !!title, folder: !!folder } }, { status: 400 });
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Missing required fields: either id or (title and folder) must be provided',
|
||||
400,
|
||||
{ received: { title: !!title, folder: !!folder } }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate folder name
|
||||
if (folder.includes('..') || folder.includes('/')) {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Invalid folder name',
|
||||
400,
|
||||
{ folder }
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedFolder = folder.toLowerCase();
|
||||
// Remove 'pages/' prefix since it's already the bucket name
|
||||
key = `user-${userId}/${normalizedFolder}/${title}${title.endsWith('.md') ? '' : '.md'}`;
|
||||
const sanitizedTitle = title.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
key = `user-${userId}/${normalizedFolder}/${sanitizedTitle}${sanitizedTitle.endsWith('.md') ? '' : '.md'}`;
|
||||
}
|
||||
|
||||
// Validate content
|
||||
if (content === undefined || content === null) {
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Content is required',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Updating file in S3:', { key, contentLength: content?.length });
|
||||
|
||||
// Update the file
|
||||
const file = await putObject(key, content, mime);
|
||||
|
||||
return NextResponse.json(file);
|
||||
try {
|
||||
// Update the file
|
||||
const file = await putObject(key, content, mime);
|
||||
return NextResponse.json(file);
|
||||
} catch (s3Error) {
|
||||
console.error('S3 error updating file:', s3Error);
|
||||
return createErrorResponse(
|
||||
StorageError.S3_ERROR,
|
||||
'Failed to update file in storage',
|
||||
503,
|
||||
{
|
||||
error: s3Error instanceof Error ? s3Error.message : String(s3Error),
|
||||
key
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating file:', error);
|
||||
return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
||||
return createErrorResponse(
|
||||
StorageError.INTERNAL_ERROR,
|
||||
'An unexpected error occurred',
|
||||
500,
|
||||
{ error: error instanceof Error ? error.message : String(error) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,29 +332,69 @@ export async function DELETE(request: Request) {
|
||||
try {
|
||||
const { authorized, userId } = await checkAuth(request);
|
||||
if (!authorized || !userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
return createErrorResponse(
|
||||
StorageError.UNAUTHORIZED,
|
||||
'Authentication required',
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'Missing file id' }, { status: 400 });
|
||||
return createErrorResponse(
|
||||
StorageError.VALIDATION_ERROR,
|
||||
'Missing file id parameter',
|
||||
400,
|
||||
{ missing: 'id' }
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure the user can only delete their own files
|
||||
if (!id.includes(`user-${userId}/`)) {
|
||||
return NextResponse.json({ error: 'Unauthorized access to file' }, { status: 403 });
|
||||
return createErrorResponse(
|
||||
StorageError.FORBIDDEN,
|
||||
'Unauthorized access to file',
|
||||
403,
|
||||
{ fileId: id }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Deleting file from S3:', { key: id });
|
||||
|
||||
// Delete the file
|
||||
await deleteObject(id);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
try {
|
||||
// Delete the file
|
||||
await deleteObject(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (s3Error) {
|
||||
console.error('S3 error deleting file:', s3Error);
|
||||
// Check if file doesn't exist (404)
|
||||
if (s3Error instanceof Error && s3Error.message.includes('NoSuchKey')) {
|
||||
return createErrorResponse(
|
||||
StorageError.NOT_FOUND,
|
||||
'File not found',
|
||||
404,
|
||||
{ fileId: id }
|
||||
);
|
||||
}
|
||||
return createErrorResponse(
|
||||
StorageError.S3_ERROR,
|
||||
'Failed to delete file from storage',
|
||||
503,
|
||||
{
|
||||
error: s3Error instanceof Error ? s3Error.message : String(s3Error),
|
||||
fileId: id
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
return NextResponse.json({ error: 'Internal server error', details: error instanceof Error ? error.message : String(error) }, { status: 500 });
|
||||
return createErrorResponse(
|
||||
StorageError.INTERNAL_ERROR,
|
||||
'An unexpected error occurred',
|
||||
500,
|
||||
{ error: error instanceof Error ? error.message : String(error) }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ import { X, Menu } from "lucide-react";
|
||||
import { ContactDetails } from '@/components/carnet/contact-details';
|
||||
import { parse as parseVCard, format as formatVCard } from 'vcard-parser';
|
||||
import { PaneLayout } from './pane-layout';
|
||||
import { notesCache, noteContentCache, foldersCache, invalidateFolderCache, invalidateNoteCache } from '@/lib/cache-utils';
|
||||
|
||||
interface Note {
|
||||
id: string;
|
||||
@ -62,60 +63,32 @@ export default function CarnetPage() {
|
||||
const isSmallScreen = useMediaQuery("(max-width: 768px)");
|
||||
const isMediumScreen = useMediaQuery("(max-width: 1024px)");
|
||||
|
||||
// Cache for Nextcloud folders
|
||||
const foldersCache = useRef<{ folders: string[]; timestamp: number } | null>(null);
|
||||
|
||||
// Cache for notes list (Panel 2)
|
||||
const notesCache = useRef<Record<string, { notes: Note[]; timestamp: number }>>({});
|
||||
|
||||
// Cache for note content (Panel 3)
|
||||
const noteContentCache = useRef<Record<string, { content: string; timestamp: number }>>({});
|
||||
|
||||
// Clear folder cache on component mount to ensure fresh data
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.removeItem('nextcloud_folders');
|
||||
console.log('Cleared folder cache');
|
||||
} catch (error) {
|
||||
console.error('Error clearing folder cache:', error);
|
||||
}
|
||||
}, []);
|
||||
// Cache is now managed by cache-utils.ts
|
||||
|
||||
useEffect(() => {
|
||||
const fetchNextcloudFolders = async () => {
|
||||
// First check localStorage cache
|
||||
const cachedData = localStorage.getItem('nextcloud_folders');
|
||||
if (cachedData) {
|
||||
const { folders, timestamp } = JSON.parse(cachedData);
|
||||
const cacheAge = Date.now() - timestamp;
|
||||
if (cacheAge < 5 * 60 * 1000) { // 5 minutes cache
|
||||
setNextcloudFolders(folders);
|
||||
return;
|
||||
}
|
||||
// Check cache first
|
||||
const cacheKey = session?.user?.id || 'default';
|
||||
const cachedFolders = foldersCache.get<string[]>(cacheKey);
|
||||
if (cachedFolders) {
|
||||
setNextcloudFolders(cachedFolders);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/nextcloud/status');
|
||||
const response = await fetch('/api/storage/status');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch Nextcloud folders');
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || 'Failed to fetch storage folders');
|
||||
}
|
||||
const data = await response.json();
|
||||
const folders = data.folders || [];
|
||||
|
||||
// Update both localStorage and memory cache
|
||||
localStorage.setItem('nextcloud_folders', JSON.stringify({
|
||||
folders,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
|
||||
foldersCache.current = {
|
||||
folders,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Update cache
|
||||
foldersCache.set(cacheKey, folders);
|
||||
setNextcloudFolders(folders);
|
||||
} catch (err) {
|
||||
console.error('Error fetching Nextcloud folders:', err);
|
||||
console.error('Error fetching storage folders:', err);
|
||||
setNextcloudFolders([]);
|
||||
}
|
||||
};
|
||||
@ -123,7 +96,7 @@ export default function CarnetPage() {
|
||||
if (status === "authenticated") {
|
||||
fetchNextcloudFolders();
|
||||
}
|
||||
}, [status]);
|
||||
}, [status, session?.user?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
@ -184,21 +157,14 @@ export default function CarnetPage() {
|
||||
useEffect(() => {
|
||||
const handleNoteSaved = (event: CustomEvent) => {
|
||||
const folder = event.detail?.folder;
|
||||
if (folder && selectedFolder.toLowerCase() === folder.toLowerCase()) {
|
||||
console.log('Note saved event received, refreshing notes list');
|
||||
// Clear cache and fetch fresh notes
|
||||
const cacheKey = `${session?.user?.id}-${folder}`;
|
||||
if (notesCache.current[cacheKey]) {
|
||||
delete notesCache.current[cacheKey];
|
||||
if (folder && session?.user?.id) {
|
||||
console.log('Note saved event received, invalidating cache for folder:', folder);
|
||||
// Invalidate cache for this folder
|
||||
invalidateFolderCache(session.user.id, folder);
|
||||
// Fetch notes if this is the current folder
|
||||
if (selectedFolder.toLowerCase() === folder.toLowerCase()) {
|
||||
fetchNotes();
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(`notes-cache-${cacheKey}`);
|
||||
} catch (error) {
|
||||
console.error('Error removing cache:', error);
|
||||
}
|
||||
// Fetch notes will be called by the existing useEffect when selectedFolder changes
|
||||
// But we can also call it directly here
|
||||
fetchNotes();
|
||||
}
|
||||
};
|
||||
|
||||
@ -309,6 +275,11 @@ export default function CarnetPage() {
|
||||
|
||||
// Fetch notes based on the selected folder
|
||||
const fetchNotes = async () => {
|
||||
if (!session?.user?.id) {
|
||||
setIsLoadingNotes(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoadingNotes(true);
|
||||
|
||||
@ -316,42 +287,18 @@ export default function CarnetPage() {
|
||||
const folderLowercase = selectedFolder.toLowerCase();
|
||||
console.log(`Fetching notes from folder: ${folderLowercase}`);
|
||||
|
||||
// Check in-memory cache first
|
||||
const cacheKey = `${session?.user?.id}-${folderLowercase}`;
|
||||
const cachedNotes = notesCache.current[cacheKey];
|
||||
const CACHE_EXPIRATION = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
// Check cache first
|
||||
const cacheKey = `${session.user.id}-${folderLowercase}`;
|
||||
const cachedNotes = notesCache.get<Note[]>(cacheKey);
|
||||
|
||||
if (cachedNotes && (Date.now() - cachedNotes.timestamp) < CACHE_EXPIRATION) {
|
||||
if (cachedNotes) {
|
||||
console.log(`Using cached notes for ${folderLowercase} folder`);
|
||||
setNotes(cachedNotes.notes);
|
||||
setNotes(cachedNotes);
|
||||
setIsLoadingNotes(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check localStorage cache if in-memory cache is not available
|
||||
try {
|
||||
const localStorageKey = `notes-cache-${cacheKey}`;
|
||||
const storedCache = localStorage.getItem(localStorageKey);
|
||||
|
||||
if (storedCache) {
|
||||
const { notes, timestamp } = JSON.parse(storedCache);
|
||||
|
||||
if ((Date.now() - timestamp) < CACHE_EXPIRATION) {
|
||||
console.log(`Using localStorage cached notes for ${folderLowercase} folder`);
|
||||
setNotes(notes);
|
||||
|
||||
// Update in-memory cache
|
||||
notesCache.current[cacheKey] = { notes, timestamp };
|
||||
|
||||
setIsLoadingNotes(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error accessing localStorage notes cache:', error);
|
||||
}
|
||||
|
||||
// Use direct storage API instead of adapter if cache is not available or expired
|
||||
// Fetch from API
|
||||
const response = await fetch(`/api/storage/files?folder=${folderLowercase}`);
|
||||
|
||||
if (response.ok) {
|
||||
@ -361,20 +308,11 @@ export default function CarnetPage() {
|
||||
// Update state
|
||||
setNotes(data);
|
||||
|
||||
// Update both caches
|
||||
const newTimestamp = Date.now();
|
||||
notesCache.current[cacheKey] = { notes: data, timestamp: newTimestamp };
|
||||
|
||||
try {
|
||||
localStorage.setItem(`notes-cache-${cacheKey}`, JSON.stringify({
|
||||
notes: data,
|
||||
timestamp: newTimestamp
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error saving notes to localStorage:', error);
|
||||
}
|
||||
// Update cache
|
||||
notesCache.set(cacheKey, data);
|
||||
} else {
|
||||
console.error('Error fetching notes:', await response.text());
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.error('Error fetching notes:', errorData.message || response.statusText);
|
||||
setNotes([]);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -415,42 +353,22 @@ export default function CarnetPage() {
|
||||
|
||||
if (response.ok) {
|
||||
// Invalidate the cache for this folder to ensure fresh data on next fetch
|
||||
const cacheKey = `${session?.user?.id}-${selectedFolder.toLowerCase()}`;
|
||||
|
||||
// Remove from in-memory cache
|
||||
if (notesCache.current[cacheKey]) {
|
||||
delete notesCache.current[cacheKey];
|
||||
}
|
||||
|
||||
// Remove from localStorage cache
|
||||
try {
|
||||
localStorage.removeItem(`notes-cache-${cacheKey}`);
|
||||
} catch (error) {
|
||||
console.error('Error removing notes from localStorage:', error);
|
||||
if (session?.user?.id) {
|
||||
invalidateFolderCache(session.user.id, selectedFolder);
|
||||
}
|
||||
|
||||
// Update the content cache for this note
|
||||
if (payload.id) {
|
||||
noteContentCache.current[payload.id] = {
|
||||
content: payload.content,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Update localStorage cache
|
||||
try {
|
||||
localStorage.setItem(`note-content-${payload.id}`, JSON.stringify({
|
||||
content: payload.content,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error saving note content to localStorage:', error);
|
||||
}
|
||||
noteContentCache.set(payload.id, payload.content);
|
||||
}
|
||||
|
||||
// Refresh the list of notes
|
||||
fetchNotes();
|
||||
} else {
|
||||
console.error('Error saving note:', await response.text());
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMessage = errorData.message || errorData.error || 'Failed to save note';
|
||||
console.error('Error saving note:', errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving note:', error);
|
||||
@ -485,7 +403,7 @@ export default function CarnetPage() {
|
||||
|
||||
const handleNoteSave = async (note: Note) => {
|
||||
try {
|
||||
const endpoint = note.id ? '/api/nextcloud/files' : '/api/nextcloud/files';
|
||||
const endpoint = '/api/storage/files';
|
||||
const method = note.id ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
@ -497,22 +415,24 @@ export default function CarnetPage() {
|
||||
id: note.id,
|
||||
title: note.title,
|
||||
content: note.content,
|
||||
folder: selectedFolder
|
||||
folder: selectedFolder.toLowerCase()
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save note');
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to save note: ${errorText}`);
|
||||
}
|
||||
|
||||
// After successful save, refresh the notes list
|
||||
const notesResponse = await fetch(`/api/nextcloud/files?folder=${selectedFolder}`);
|
||||
const notesResponse = await fetch(`/api/storage/files?folder=${selectedFolder.toLowerCase()}`);
|
||||
if (notesResponse.ok) {
|
||||
const updatedNotes = await notesResponse.json();
|
||||
setNotes(updatedNotes);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving note:', error);
|
||||
throw error; // Re-throw pour permettre la gestion d'erreur en amont
|
||||
}
|
||||
};
|
||||
|
||||
@ -570,23 +490,20 @@ export default function CarnetPage() {
|
||||
|
||||
const handleDeleteNote = async (note: Note) => {
|
||||
try {
|
||||
const response = await fetch(`/api/nextcloud/files`, {
|
||||
const response = await fetch(`/api/storage/files?id=${encodeURIComponent(note.id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: note.id,
|
||||
folder: selectedFolder
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete note');
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to delete note: ${errorText}`);
|
||||
}
|
||||
|
||||
// Refresh the notes list
|
||||
const notesResponse = await fetch(`/api/nextcloud/files?folder=${selectedFolder}`);
|
||||
const notesResponse = await fetch(`/api/storage/files?folder=${selectedFolder.toLowerCase()}`);
|
||||
if (notesResponse.ok) {
|
||||
const updatedNotes = await notesResponse.json();
|
||||
setNotes(updatedNotes);
|
||||
@ -598,6 +515,7 @@ export default function CarnetPage() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error);
|
||||
throw error; // Re-throw pour permettre la gestion d'erreur en amont
|
||||
}
|
||||
};
|
||||
|
||||
@ -607,17 +525,16 @@ export default function CarnetPage() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Always use Allemanique.vcf for new contacts
|
||||
const basePath = `/files/cube-${session.user.id}/Private/Contacts`;
|
||||
// Use S3 path structure: user-{userId}/contacts/{filename}.vcf
|
||||
const vcfFile = 'Allemanique.vcf';
|
||||
const path = `${basePath}/${vcfFile}`;
|
||||
const s3Key = `user-${session.user.id}/contacts/${vcfFile}`;
|
||||
|
||||
let vcfContent = '';
|
||||
let existingContacts: string[] = [];
|
||||
|
||||
try {
|
||||
// Try to get existing contacts from the VCF file
|
||||
const response = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(path)}`);
|
||||
const response = await fetch(`/api/storage/files/content?path=${encodeURIComponent(s3Key)}`);
|
||||
if (response.ok) {
|
||||
const { content } = await response.json();
|
||||
// Split the content into individual vCards
|
||||
@ -678,23 +595,24 @@ export default function CarnetPage() {
|
||||
// Join all vCards back together with proper spacing
|
||||
vcfContent = updatedVcards.join('\n\n');
|
||||
|
||||
// Save the updated VCF file
|
||||
const saveResponse = await fetch('/api/nextcloud/files', {
|
||||
// Save the updated VCF file using S3 storage API
|
||||
const saveResponse = await fetch('/api/storage/files', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: path,
|
||||
id: s3Key,
|
||||
title: vcfFile,
|
||||
content: vcfContent,
|
||||
folder: 'Contacts',
|
||||
folder: 'contacts',
|
||||
mime: 'text/vcard'
|
||||
}),
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error('Failed to save contact');
|
||||
const errorText = await saveResponse.text();
|
||||
throw new Error(`Failed to save contact: ${errorText}`);
|
||||
}
|
||||
|
||||
// Refresh the contacts list
|
||||
@ -706,6 +624,7 @@ export default function CarnetPage() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving contact:', error);
|
||||
throw error; // Re-throw pour permettre la gestion d'erreur en amont
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@ -716,18 +635,23 @@ export default function CarnetPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session?.user?.id) {
|
||||
console.error('No user session available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Determine the correct VCF file path
|
||||
const basePath = `/files/cube-${session?.user?.id}/Private/Contacts`;
|
||||
const vcfFile = contact.group ? `${contact.group}.vcf` : 'contacts.vcf';
|
||||
const path = `${basePath}/${vcfFile}`;
|
||||
// Use S3 path structure: user-{userId}/contacts/{filename}.vcf
|
||||
const vcfFile = contact.group ? `${contact.group}.vcf` : 'Allemanique.vcf';
|
||||
const s3Key = `user-${session.user.id}/contacts/${vcfFile}`;
|
||||
|
||||
// Get existing contacts from the VCF file
|
||||
const response = await fetch(`/api/nextcloud/files/content?path=${encodeURIComponent(path)}`);
|
||||
const response = await fetch(`/api/storage/files/content?path=${encodeURIComponent(s3Key)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch contacts');
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to fetch contacts: ${errorText}`);
|
||||
}
|
||||
|
||||
const { content } = await response.json();
|
||||
@ -742,23 +666,24 @@ export default function CarnetPage() {
|
||||
// Join the remaining vCards back together
|
||||
const vcfContent = updatedVcards.map((section: string) => 'BEGIN:VCARD' + section).join('\n');
|
||||
|
||||
// Save the updated VCF file
|
||||
const saveResponse = await fetch('/api/nextcloud/files', {
|
||||
// Save the updated VCF file using S3 storage API
|
||||
const saveResponse = await fetch('/api/storage/files', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: path,
|
||||
id: s3Key,
|
||||
title: vcfFile,
|
||||
content: vcfContent,
|
||||
folder: 'Contacts',
|
||||
folder: 'contacts',
|
||||
mime: 'text/vcard'
|
||||
}),
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error('Failed to delete contact');
|
||||
const errorText = await saveResponse.text();
|
||||
throw new Error(`Failed to delete contact: ${errorText}`);
|
||||
}
|
||||
|
||||
// Clear selected contact and refresh list
|
||||
@ -766,6 +691,7 @@ export default function CarnetPage() {
|
||||
await fetchContacts(selectedFolder);
|
||||
} catch (error) {
|
||||
console.error('Error deleting contact:', error);
|
||||
throw error; // Re-throw pour permettre la gestion d'erreur en amont
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Image, FileText, Link, List, Plus } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { noteContentCache } from '@/lib/cache-utils';
|
||||
|
||||
interface Note {
|
||||
id: string;
|
||||
@ -31,10 +32,6 @@ export const Editor: React.FC<EditorProps> = ({ note, onSave, currentFolder = 'N
|
||||
const saveTimeout = useRef<NodeJS.Timeout>();
|
||||
const router = useRouter();
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
// Content cache for notes
|
||||
const contentCache = useRef<Record<string, { content: string; timestamp: number }>>({});
|
||||
const CACHE_EXPIRATION = 15 * 60 * 1000; // 15 minutes in milliseconds
|
||||
|
||||
useEffect(() => {
|
||||
// Redirect to login if not authenticated
|
||||
@ -49,38 +46,15 @@ export const Editor: React.FC<EditorProps> = ({ note, onSave, currentFolder = 'N
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// First check in-memory cache
|
||||
const cachedContent = contentCache.current[note.id];
|
||||
if (cachedContent && (Date.now() - cachedContent.timestamp) < CACHE_EXPIRATION) {
|
||||
// Check cache first
|
||||
const cachedContent = noteContentCache.get<string>(note.id);
|
||||
if (cachedContent) {
|
||||
console.log(`Using cached content for note ${note.title}`);
|
||||
setContent(cachedContent.content || '');
|
||||
setContent(cachedContent);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then check localStorage cache
|
||||
try {
|
||||
const localStorageKey = `note-content-${note.id}`;
|
||||
const storedCache = localStorage.getItem(localStorageKey);
|
||||
|
||||
if (storedCache) {
|
||||
const { content, timestamp } = JSON.parse(storedCache);
|
||||
|
||||
if ((Date.now() - timestamp) < CACHE_EXPIRATION) {
|
||||
console.log(`Using localStorage cached content for note ${note.title}`);
|
||||
setContent(content || '');
|
||||
|
||||
// Update in-memory cache
|
||||
contentCache.current[note.id] = { content, timestamp };
|
||||
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error accessing localStorage content cache:', error);
|
||||
}
|
||||
|
||||
// If cache miss, fetch from API
|
||||
try {
|
||||
const response = await fetch(`/api/storage/files/content?path=${encodeURIComponent(note.id)}`);
|
||||
@ -90,27 +64,19 @@ export const Editor: React.FC<EditorProps> = ({ note, onSave, currentFolder = 'N
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to fetch note content: ${response.status} ${errorText}`);
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMessage = errorData.message || errorData.error || `Failed to fetch note content: ${response.status}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const data = await response.json();
|
||||
setContent(data.content || '');
|
||||
|
||||
// Update both caches
|
||||
const newTimestamp = Date.now();
|
||||
contentCache.current[note.id] = { content: data.content, timestamp: newTimestamp };
|
||||
|
||||
try {
|
||||
localStorage.setItem(`note-content-${note.id}`, JSON.stringify({
|
||||
content: data.content,
|
||||
timestamp: newTimestamp
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error saving content to localStorage:', error);
|
||||
}
|
||||
// Update cache
|
||||
noteContentCache.set(note.id, data.content || '');
|
||||
} catch (error) {
|
||||
console.error('Error fetching note content:', error);
|
||||
setError('Failed to load note content. Please try again later.');
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to load note content. Please try again later.';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@ -128,7 +94,7 @@ export const Editor: React.FC<EditorProps> = ({ note, onSave, currentFolder = 'N
|
||||
setTitle('');
|
||||
setContent('');
|
||||
}
|
||||
}, [note, router, CACHE_EXPIRATION]);
|
||||
}, [note, router]);
|
||||
|
||||
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value);
|
||||
@ -207,23 +173,7 @@ export const Editor: React.FC<EditorProps> = ({ note, onSave, currentFolder = 'N
|
||||
console.log('Note saved successfully:', savedNote);
|
||||
|
||||
// Update content cache after successful save
|
||||
const newTimestamp = Date.now();
|
||||
|
||||
// Update in-memory cache
|
||||
contentCache.current[noteId] = {
|
||||
content,
|
||||
timestamp: newTimestamp
|
||||
};
|
||||
|
||||
// Update localStorage cache
|
||||
try {
|
||||
localStorage.setItem(`note-content-${noteId}`, JSON.stringify({
|
||||
content,
|
||||
timestamp: newTimestamp
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error updating content cache in localStorage:', error);
|
||||
}
|
||||
noteContentCache.set(noteId, content);
|
||||
|
||||
setError(null);
|
||||
onSave?.({
|
||||
|
||||
237
lib/cache-utils.ts
Normal file
237
lib/cache-utils.ts
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Cache utilities for Pages application
|
||||
* Provides centralized cache management with proper invalidation
|
||||
*/
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface CacheConfig {
|
||||
ttl: number; // Time to live in milliseconds
|
||||
keyPrefix: string;
|
||||
}
|
||||
|
||||
class CacheManager {
|
||||
private memoryCache: Map<string, CacheEntry<any>> = new Map();
|
||||
private config: CacheConfig;
|
||||
|
||||
constructor(config: CacheConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from cache (memory first, then localStorage)
|
||||
*/
|
||||
get<T>(key: string): T | null {
|
||||
const fullKey = `${this.config.keyPrefix}${key}`;
|
||||
|
||||
// Check memory cache first
|
||||
const memoryEntry = this.memoryCache.get(fullKey);
|
||||
if (memoryEntry && this.isValid(memoryEntry.timestamp)) {
|
||||
return memoryEntry.data as T;
|
||||
}
|
||||
|
||||
// Check localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem(fullKey);
|
||||
if (stored) {
|
||||
const entry: CacheEntry<T> = JSON.parse(stored);
|
||||
if (this.isValid(entry.timestamp)) {
|
||||
// Update memory cache
|
||||
this.memoryCache.set(fullKey, entry);
|
||||
return entry.data;
|
||||
} else {
|
||||
// Expired, remove it
|
||||
localStorage.removeItem(fullKey);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading from localStorage cache:', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data in cache (both memory and localStorage)
|
||||
*/
|
||||
set<T>(key: string, data: T): void {
|
||||
const fullKey = `${this.config.keyPrefix}${key}`;
|
||||
const entry: CacheEntry<T> = {
|
||||
data,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Update memory cache
|
||||
this.memoryCache.set(fullKey, entry);
|
||||
|
||||
// Update localStorage
|
||||
try {
|
||||
localStorage.setItem(fullKey, JSON.stringify(entry));
|
||||
} catch (error) {
|
||||
console.error('Error writing to localStorage cache:', error);
|
||||
// If localStorage is full, try to clear old entries
|
||||
this.clearExpired();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific cache entry
|
||||
*/
|
||||
remove(key: string): void {
|
||||
const fullKey = `${this.config.keyPrefix}${key}`;
|
||||
|
||||
// Remove from memory
|
||||
this.memoryCache.delete(fullKey);
|
||||
|
||||
// Remove from localStorage
|
||||
try {
|
||||
localStorage.removeItem(fullKey);
|
||||
} catch (error) {
|
||||
console.error('Error removing from localStorage cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache entries matching a pattern
|
||||
*/
|
||||
clearPattern(pattern: string): void {
|
||||
const fullPattern = `${this.config.keyPrefix}${pattern}`;
|
||||
|
||||
// Clear from memory
|
||||
for (const key of this.memoryCache.keys()) {
|
||||
if (key.includes(fullPattern)) {
|
||||
this.memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear from localStorage
|
||||
try {
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.includes(fullPattern)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
} catch (error) {
|
||||
console.error('Error clearing pattern from localStorage cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all expired entries
|
||||
*/
|
||||
clearExpired(): void {
|
||||
const now = Date.now();
|
||||
|
||||
// Clear from memory
|
||||
for (const [key, entry] of this.memoryCache.entries()) {
|
||||
if (!this.isValid(entry.timestamp)) {
|
||||
this.memoryCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear from localStorage
|
||||
try {
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith(this.config.keyPrefix)) {
|
||||
try {
|
||||
const entry: CacheEntry<any> = JSON.parse(localStorage.getItem(key) || '{}');
|
||||
if (!this.isValid(entry.timestamp)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
} catch {
|
||||
// Invalid entry, remove it
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
} catch (error) {
|
||||
console.error('Error clearing expired entries from localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
clearAll(): void {
|
||||
this.memoryCache.clear();
|
||||
|
||||
try {
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith(this.config.keyPrefix)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
} catch (error) {
|
||||
console.error('Error clearing all from localStorage cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache entry is still valid
|
||||
*/
|
||||
private isValid(timestamp: number): boolean {
|
||||
return (Date.now() - timestamp) < this.config.ttl;
|
||||
}
|
||||
}
|
||||
|
||||
// Export cache managers for different use cases
|
||||
export const notesCache = new CacheManager({
|
||||
ttl: 5 * 60 * 1000, // 5 minutes
|
||||
keyPrefix: 'notes-cache-'
|
||||
});
|
||||
|
||||
export const noteContentCache = new CacheManager({
|
||||
ttl: 15 * 60 * 1000, // 15 minutes
|
||||
keyPrefix: 'note-content-'
|
||||
});
|
||||
|
||||
export const foldersCache = new CacheManager({
|
||||
ttl: 2 * 60 * 1000, // 2 minutes
|
||||
keyPrefix: 'nextcloud_folders'
|
||||
});
|
||||
|
||||
/**
|
||||
* Invalidate cache for a specific folder
|
||||
*/
|
||||
export function invalidateFolderCache(userId: string, folder: string): void {
|
||||
const folderLowercase = folder.toLowerCase();
|
||||
const cacheKey = `${userId}-${folderLowercase}`;
|
||||
|
||||
// Clear notes list cache
|
||||
notesCache.remove(cacheKey);
|
||||
|
||||
// Clear all note content caches for this folder (pattern match)
|
||||
noteContentCache.clearPattern(`user-${userId}/${folderLowercase}/`);
|
||||
|
||||
console.log(`Cache invalidated for folder: ${folderLowercase}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for a specific note
|
||||
*/
|
||||
export function invalidateNoteCache(noteId: string): void {
|
||||
noteContentCache.remove(noteId);
|
||||
console.log(`Cache invalidated for note: ${noteId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches for a user
|
||||
*/
|
||||
export function clearUserCache(userId: string): void {
|
||||
notesCache.clearPattern(userId);
|
||||
noteContentCache.clearPattern(`user-${userId}/`);
|
||||
foldersCache.clearAll();
|
||||
console.log(`All caches cleared for user: ${userId}`);
|
||||
}
|
||||
16
lib/s3.ts
16
lib/s3.ts
@ -3,13 +3,21 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
|
||||
// S3 Configuration
|
||||
export const S3_CONFIG = {
|
||||
endpoint: 'https://dome-api.slm-lab.net',
|
||||
region: 'us-east-1',
|
||||
endpoint: process.env.S3_ENDPOINT || process.env.MINIO_S3_UPLOAD_BUCKET_URL || 'https://dome-api.slm-lab.net',
|
||||
region: process.env.S3_REGION || process.env.MINIO_AWS_REGION || 'us-east-1',
|
||||
bucket: process.env.S3_BUCKET || 'pages',
|
||||
accessKey: '4aBT4CMb7JIMMyUtp4Pl',
|
||||
secretKey: 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg'
|
||||
accessKey: process.env.S3_ACCESS_KEY || process.env.MINIO_ACCESS_KEY || '',
|
||||
secretKey: process.env.S3_SECRET_KEY || process.env.MINIO_SECRET_KEY || ''
|
||||
};
|
||||
|
||||
// Validate required S3 configuration
|
||||
if (!S3_CONFIG.accessKey || !S3_CONFIG.secretKey) {
|
||||
console.error('⚠️ S3 credentials are missing! Please set S3_ACCESS_KEY and S3_SECRET_KEY environment variables.');
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error('S3 credentials are required in production environment');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize S3 client for Minio
|
||||
export const s3Client = new S3Client({
|
||||
region: S3_CONFIG.region,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user