diff --git a/.env.docker.example b/.env.docker.example index c681954..f1f7ead 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -1,80 +1,101 @@ -# ============================================ +# ============================================================================= # Memento - Docker Environment Configuration -# ============================================ +# ============================================================================= # Copy this file to .env.docker and update with your values. +# This file is read by docker-compose.yml via env_file directive. +# cp .env.docker.example .env.docker -# ============================================ -# Application URL -# ============================================ +# ============================================================================= +# APPLICATION URL (REQUIRED) +# ============================================================================= # Change to your server IP or domain +# Examples: +# IP: http://192.168.1.190:3000 +# Domain: http://notes.yourdomain.com +# HTTPS: https://notes.yourdomain.com NEXTAUTH_URL="http://localhost:3000" -# ============================================ -# Authentication Secret -# ============================================ +# ============================================================================= +# AUTHENTICATION SECRET (REQUIRED) +# ============================================================================= # Generate with: openssl rand -base64 32 NEXTAUTH_SECRET="changethisinproduction" -# ============================================ -# PostgreSQL Configuration -# ============================================ +# ============================================================================= +# REGISTRATION +# ============================================================================= +# Set to "false" to disable public registration (default: true) +# ALLOW_REGISTRATION=true + +# ============================================================================= +# POSTGRESQL CONFIGURATION +# ============================================================================= POSTGRES_PORT=5432 POSTGRES_DB=memento POSTGRES_USER=memento POSTGRES_PASSWORD=memento -# ============================================ -# MCP Server Configuration -# ============================================ +# ============================================================================= +# MCP SERVER CONFIGURATION +# ============================================================================= # Mode: 'stdio' (Claude Desktop, Cline) or 'sse' (N8N, HTTP) MCP_MODE="stdio" MCP_PORT="3001" -# ============================================ -# AI Provider - Tags Generation -# ============================================ +# ============================================================================= +# AI PROVIDER - TAGS GENERATION +# ============================================================================= # Options: ollama, openai, custom AI_PROVIDER_TAGS=ollama AI_MODEL_TAGS="granite4:latest" -# ============================================ -# AI Provider - Embeddings -# ============================================ +# ============================================================================= +# AI PROVIDER - EMBEDDINGS +# ============================================================================= # Options: ollama, openai, custom AI_PROVIDER_EMBEDDING=ollama AI_MODEL_EMBEDDING="embeddinggemma:latest" -# ============================================ -# Ollama Configuration -# ============================================ +# ============================================================================= +# AI PROVIDER - CHAT (optional, falls back to AI_PROVIDER_TAGS) +# ============================================================================= +# AI_PROVIDER_CHAT=ollama +# AI_MODEL_CHAT="granite4:latest" + +# ============================================================================= +# OLLAMA CONFIGURATION (if provider = ollama) +# ============================================================================= # Docker service: http://ollama:11434 # Host machine: http://host.docker.internal:11434 # Remote server: http://YOUR_SERVER_IP:11434 OLLAMA_BASE_URL="http://ollama:11434" -OLLAMA_MODEL="granite4:latest" -# ============================================ -# OpenAI Configuration -# ============================================ +# ============================================================================= +# OPENAI CONFIGURATION (if provider = openai) +# ============================================================================= # OPENAI_API_KEY="sk-..." -# ============================================ -# Custom OpenAI-Compatible Provider -# ============================================ -# OpenRouter, Groq, Together AI, Mistral, etc. +# ============================================================================= +# CUSTOM OPENAI-COMPATIBLE PROVIDER (if provider = custom) +# ============================================================================= +# Compatible with: OpenRouter, Groq, Together AI, Mistral, etc. +# OpenRouter: https://openrouter.ai/api/v1 +# Groq: https://api.groq.com/openai/v1 +# Together: https://api.together.xyz/v1 +# Mistral: https://api.mistral.ai/v1 # CUSTOM_OPENAI_API_KEY="your-api-key" # CUSTOM_OPENAI_BASE_URL="https://openrouter.ai/api/v1" -# ============================================ -# Email / SMTP Configuration -# ============================================ +# ============================================================================= +# EMAIL / SMTP (optional, required for password reset) +# ============================================================================= # SMTP_HOST="smtp.gmail.com" # SMTP_PORT="587" # SMTP_USER="your-email@gmail.com" # SMTP_PASS="your-app-password" # SMTP_FROM="noreply@memento.app" -# ============================================ -# Application Settings -# ============================================ -# ALLOW_REGISTRATION=true +# ============================================================================= +# RESEND EMAIL (alternative to SMTP, optional) +# ============================================================================= +# RESEND_API_KEY="re_..." diff --git a/GUIDE.md b/GUIDE.md new file mode 100644 index 0000000..9456e5d --- /dev/null +++ b/GUIDE.md @@ -0,0 +1,725 @@ +# Memento - Guide d'utilisation complet + +## Table des matieres + +1. [Presentation](#presentation) +2. [Architecture](#architecture) +3. [Installation locale](#installation-locale) +4. [Deploiement Docker](#deploiement-docker) +5. [Configuration des providers IA](#configuration-des-providers-ia) +6. [Serveur MCP (Model Context Protocol)](#serveur-mcp) +7. [Integrations N8N](#integrations-n8n) +8. [Configuration email (SMTP)](#configuration-email) +9. [Administration](#administration) +10. [Reference des variables d'environnement](#reference-des-variables-denvironnement) +11. [Commandes utiles](#commandes-utiles) +12. [Resolution des problemes](#resolution-des-problemes) + +--- + +## Presentation + +**Memento** est une application de prise de notes intelligente, inspiree de Google Keep, construite avec Next.js 16, TypeScript, Tailwind CSS 4, Prisma et PostgreSQL. + +### Fonctionnalites principales + +- **Notes texte et checklists** avec couleurs personnalisees (10 themes pastel) +- **Grille masonry responsive** avec drag-and-drop +- **Upload d'images** avec preservation de la taille originale +- **Notebooks et labels contextuels** pour organiser les notes +- **Recherche semantique** (embeddings IA) + recherche plein texte +- **Generation automatique de tags** via IA +- **Suggestions de titre** par IA +- **Systeme de rappels** avec notifications +- **Mode sombre/clair** avec preference systeme +- **Authentification** NextAuth.js (credentials, inscription, reset mot de passe) +- **Panneau admin** pour configurer les providers IA, SMTP, etc. +- **Serveur MCP** pour integrer avec Claude Desktop, N8N, ou tout client MCP +- **Support i18n** (FR/EN) +- **Export/import de donnees** (JSON) +- **PWA** (Progressive Web App) + +### Stack technique + +| Composant | Technologie | +|-----------|-------------| +| Frontend | Next.js 16, React 19, TypeScript, Tailwind CSS 4 | +| UI | shadcn/ui, Lucide React | +| Backend | Next.js Server Actions, API Routes | +| Base de donnees | PostgreSQL 16 via Prisma ORM 5 | +| Authentification | NextAuth.js v5 | +| IA | Vercel AI SDK (OpenAI, Ollama, custom) | +| MCP | @modelcontextprotocol/sdk | +| Email | Nodemailer (SMTP) ou Resend | +| Docker | Docker Compose (postgres + memento-note + mcp-server + ollama) | + +--- + +## Architecture + +``` +Momento/ +├── docker-compose.yml # Orchestration multi-conteneurs +├── .env.docker.example # Template config Docker +├── LICENSE # Apache 2.0 + Commons Clause +├── memento-note/ # Application Next.js +│ ├── app/ # App Router (pages, actions, API) +│ ├── components/ # Composants React +│ ├── lib/ # Logique metier +│ │ ├── ai/ # Providers IA (factory pattern) +│ │ ├── prisma.ts # Client DB +│ │ ├── mail.ts # Envoi d'emails +│ │ └── config.ts # Config depuis DB +│ ├── prisma/ # Schema + migrations PostgreSQL +│ ├── locales/ # Fichiers i18n (fr.json, en.json) +│ ├── Dockerfile # Build multi-stage (node:22-bullseye) +│ ├── docker-compose.yml # Standalone (postgres + app) +│ └── .env.example # Template dev local +├── mcp-server/ # Serveur MCP +│ ├── index.js # Mode stdio (Claude Desktop) +│ ├── index-sse.js # Mode SSE/HTTP (N8N, remote) +│ ├── tools.js # Definitions des outils MCP +│ ├── Dockerfile # Conteneur MCP (node:20-alpine) +│ └── .env.example # Template MCP +├── scripts/ # Scripts de migration +└── n8n-memento-workflow.json # Workflow N8N preconfigure +``` + +### Flux de donnees + +``` +Navigateur → Next.js App Router + ├── Server Components (lecture) + ├── Server Actions (ecriture) + └── API Routes (REST) + ↓ + Prisma ORM + ↓ + PostgreSQL 16 + ↑ + MCP Server (stdio ou SSE) + ↑ + Claude Desktop / N8N / Client MCP +``` + +--- + +## Installation locale + +### Prerequis + +- Node.js 20+ (22 recommande) +- PostgreSQL 16 +- npm + +### Etapes + +```bash +# 1. Cloner le depot +git clone https://github.com/votre-user/Momento.git +cd Momento/memento-note + +# 2. Installer les dependances +npm install --legacy-peer-deps + +# 3. Configurer l'environnement +cp .env.example .env +# Editer .env avec vos valeurs (DATABASE_URL, NEXTAUTH_SECRET, etc.) + +# 4. Creer la base de donnees et appliquer les migrations +npx prisma migrate dev + +# 5. Lancer en developpement +npm run dev + +# 6. Acceder a l'application +# http://localhost:3000 +``` + +### Premier lancement + +1. Creez un compte via la page d'inscription +2. Le premier utilisateur inscrit devient automatiquement admin +3. Accedez au panneau admin : `http://localhost:3000/admin/settings` + +--- + +## Deploiement Docker + +### Quick Start + +```bash +# 1. Cloner le depot +git clone https://github.com/votre-user/Momento.git +cd Momento + +# 2. Configurer l'environnement +cp .env.docker.example .env.docker +nano .env.docker +# Modifier NEXTAUTH_URL et NEXTAUTH_SECRET (obligatoire) + +# 3. Lancer les conteneurs +docker compose up -d + +# 4. Verifier que tout fonctionne +docker compose ps +docker compose logs -f memento-note +``` + +### Avec Ollama (IA locale) + +```bash +# Ajouter le profile ollama +docker compose --profile ollama up -d + +# Tirer un modele +docker exec memento-ollama ollama pull granite4 +docker exec memento-ollama ollama pull embeddinggemma +``` + +### Variables Docker obligatoires + +| Variable | Description | +|----------|-------------| +| `NEXTAUTH_URL` | URL publique de l'app (ex: `http://192.168.1.190:3000`) | +| `NEXTAUTH_SECRET` | Secret JWT - generer avec `openssl rand -base64 32` | + +### Ports utilises + +| Service | Port | Description | +|---------|------|-------------| +| memento-note | 3000 | Application web | +| PostgreSQL | 5432 | Base de donnees | +| MCP Server | 3001 | SSE mode uniquement | +| Ollama | 11434 | IA locale (optionnel) | + +### Reverse Proxy (Nginx) + +```nginx +server { + listen 80; + server_name notes.yourdomain.com; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + client_max_body_size 10M; +} +``` + +--- + +## Configuration des providers IA + +Memento supporte 3 providers IA, configurables independamment pour chaque fonctionnalite (tags, embeddings, chat). + +### Provider 1 : Ollama (IA locale, gratuit) + +```bash +# .env.docker +AI_PROVIDER_TAGS=ollama +AI_PROVIDER_EMBEDDING=ollama +OLLAMA_BASE_URL="http://ollama:11434" +AI_MODEL_TAGS="granite4:latest" +AI_MODEL_EMBEDDING="embeddinggemma:latest" +``` + +Lancer avec le profile ollama : +```bash +docker compose --profile ollama up -d +docker exec memento-ollama ollama pull granite4 +docker exec memento-ollama ollama pull embeddinggemma +``` + +**Ressources recommandees** : 8 Go RAM, 4 CPU + +### Provider 2 : OpenAI (cloud, payant) + +```bash +# .env.docker +AI_PROVIDER_TAGS=openai +AI_PROVIDER_EMBEDDING=openai +OPENAI_API_KEY="sk-votre-cle-ici" +AI_MODEL_TAGS="gpt-4o-mini" +AI_MODEL_EMBEDDING="text-embedding-3-small" +``` + +### Provider 3 : Custom (OpenRouter, Groq, Together, Mistral...) + +```bash +# .env.docker +AI_PROVIDER_TAGS=custom +AI_PROVIDER_EMBEDDING=custom +CUSTOM_OPENAI_API_KEY="votre-cle-api" +CUSTOM_OPENAI_BASE_URL="https://openrouter.ai/api/v1" +AI_MODEL_TAGS="openai/gpt-4o-mini" +AI_MODEL_EMBEDDING="openai/text-embedding-3-small" +``` + +| Provider | URL de base | +|----------|-------------| +| OpenRouter | `https://openrouter.ai/api/v1` | +| Groq | `https://api.groq.com/openai/v1` | +| Together AI | `https://api.together.xyz/v1` | +| Mistral | `https://api.mistral.ai/v1` | + +### Configuration mixte + +Vous pouvez utiliser des providers differents pour chaque fonctionnalite : + +```bash +# Ollama pour les tags, OpenAI pour les embeddings +AI_PROVIDER_TAGS=ollama +AI_PROVIDER_EMBEDDING=openai +OLLAMA_BASE_URL="http://ollama:11434" +OPENAI_API_KEY="sk-..." +AI_MODEL_TAGS="granite4:latest" +AI_MODEL_EMBEDDING="text-embedding-3-small" +``` + +### Configuration via le panneau admin + +Les providers IA peuvent aussi etre configures depuis l'interface : +1. Se connecter en admin +2. Aller sur `/admin/settings` +3. Section "AI Settings" +4. Choisir le provider, le modele, et entrer la cle API +5. Sauvegarder + +**Note** : Les parametres du panneau admin (stockes en DB) sont prioritaires sur les variables d'environnement. + +--- + +## Serveur MCP + +Le serveur MCP (Model Context Protocol) permet aux agents IA d'interagir avec vos notes via un protocole standardise. + +### Outils disponibles (9 outils) + +| Outil | Description | +|-------|-------------| +| `create_note` | Creer une nouvelle note | +| `get_notes` | Recuperer toutes les notes | +| `get_note` | Recuperer une note specifique | +| `update_note` | Modifier une note existante | +| `delete_note` | Supprimer une note | +| `search_notes` | Rechercher des notes par contenu | +| `get_labels` | Lister les labels | +| `toggle_pin` | Epingler/Depingler une note | +| `toggle_archive` | Archiver/Desarchiver une note | + +### Mode stdio (Claude Desktop, Cline) + +Communication via stdin/stdout, ideal pour les clients locaux. + +**Configuration Claude Desktop** (`claude_desktop_config.json`) : +```json +{ + "mcpServers": { + "memento": { + "command": "docker", + "args": ["exec", "-i", "memento-mcp", "node", "index.js"] + } + } +} +``` + +### Mode SSE (N8N, HTTP) + +Communication via HTTP Server-Sent Events, accessible sur le reseau. + +```bash +# .env.docker +MCP_MODE="sse" +MCP_PORT="3001" +``` + +Le serveur sera accessible sur `http://localhost:3001`. + +#### Endpoints SSE + +| Endpoint | Methode | Description | +|----------|---------|-------------| +| `/` | GET | Health check | +| `/sse` | GET/POST | Endpoint MCP principal | +| `/message` | POST | Envoi de messages | +| `/sessions` | GET | Sessions actives | + +#### Verification + +```bash +curl http://localhost:3001/ +# {"name":"Memento MCP SSE Server","version":"1.0.0","status":"running"} +``` + +### Configuration MCP avancee + +```bash +# .env.docker +MCP_LOG_LEVEL=info # debug, info, warn, error +MCP_REQUEST_TIMEOUT=30000 # Timeout en ms +MCP_REQUIRE_AUTH=false # Activer l'authentification +MCP_API_KEY="votre-cle" # Cle API si auth active +APP_BASE_URL="http://localhost:3000" # URL de l'app pour les liens +``` + +--- + +## Integrations N8N + +### Configuration du noeud MCP Client + +1. Ajouter un noeud **"MCP Client"** dans N8N +2. Selectionner **"HTTP Streamable"** comme transport +3. Configurer l'endpoint : `http://memento-mcp:3001/sse` (Docker) ou `http://VOTRE_IP:3001/sse` + +### Workflows preconfigures + +Le fichier `n8n-memento-workflow.json` contient un workflow pret a l'emploi pour : +- Creer des notes automatiquement +- Rechercher des notes +- Archiver les notes anciennes + +### Exemples d'utilisation + +#### Creer une note via curl (SSE mode) + +```bash +curl -X POST http://localhost:3001/sse \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "create_note", + "arguments": { + "title": "Ma note", + "content": "Contenu de la note" + } + } + }' +``` + +#### Lister les outils disponibles + +```bash +curl -X POST http://localhost:3001/sse \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} + }' +``` + +--- + +## Configuration email + +L'email est necessaire pour la reinitialisation de mot de passe et les rappels. + +### Option 1 : SMTP + +```bash +# .env.docker +SMTP_HOST="smtp.gmail.com" +SMTP_PORT="587" +SMTP_USER="votre-email@gmail.com" +SMTP_PASS="votre-mot-de-passe-app" +SMTP_FROM="noreply@votre-domaine.com" +``` + +| Provider | Host | Port | +|----------|------|------| +| Gmail | smtp.gmail.com | 587 | +| Outlook | smtp.office365.com | 587 | +| SendGrid | smtp.sendgrid.net | 587 | +| Mailgun | smtp.mailgun.org | 587 | + +### Option 2 : Resend + +```bash +# .env.docker +RESEND_API_KEY="re_votre-cle" +``` + +Les parametres `SMTP_SECURE` et `SMTP_IGNORE_CERT` sont configurables depuis le panneau admin. + +--- + +## Administration + +### Panneau admin + +Accessibles uniquement par le premier utilisateur inscrit (ou les utilisateurs avec le role admin). + +**URL** : `/admin/settings` + +### Sections configurables + +1. **AI Settings** - Provider, modele, cle API pour tags, embeddings et chat +2. **Email Settings** - Configuration SMTP +3. **General Settings** - Inscription publique activee/desactivee + +### Gestion des utilisateurs + +- Les utilisateurs sont geres via `/admin` +- Le premier utilisateur inscrit est automatiquement admin +- L'inscription peut etre desactivee avec `ALLOW_REGISTRATION=false` + +--- + +## Reference des variables d'environnement + +### Fichiers .env + +| Fichier | Usage | +|---------|-------| +| `.env.docker` | Configuration Docker (lu par docker-compose via env_file) | +| `.env.docker.example` | Template pour `.env.docker` | +| `memento-note/.env` | Configuration dev local | +| `memento-note/.env.example` | Template pour dev local | +| `mcp-server/.env` | Configuration du serveur MCP | +| `mcp-server/.env.example` | Template pour MCP | + +### Variables completes + +#### Core (obligatoire) + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `DATABASE_URL` | - | Connection string PostgreSQL | +| `NEXTAUTH_SECRET` | - | Secret JWT (`openssl rand -base64 32`) | +| `NEXTAUTH_URL` | `http://localhost:3000` | URL publique de l'app | + +#### PostgreSQL (Docker) + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `POSTGRES_USER` | `memento` | Utilisateur PostgreSQL | +| `POSTGRES_PASSWORD` | `memento` | Mot de passe PostgreSQL | +| `POSTGRES_DB` | `memento` | Nom de la base | +| `POSTGRES_PORT` | `5432` | Port expose | + +#### IA - Tags + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `AI_PROVIDER_TAGS` | - | Provider pour tags (`ollama`, `openai`, `custom`) | +| `AI_MODEL_TAGS` | `granite4:latest` | Modele pour tags | + +#### IA - Embeddings + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `AI_PROVIDER_EMBEDDING` | - | Provider pour embeddings | +| `AI_MODEL_EMBEDDING` | `embeddinggemma:latest` | Modele pour embeddings | + +#### IA - Chat + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `AI_PROVIDER_CHAT` | fallback Tags | Provider pour chat | +| `AI_MODEL_CHAT` | fallback Tags | Modele pour chat | + +#### IA - OpenAI + +| Variable | Description | +|----------|-------------| +| `OPENAI_API_KEY` | Cle API OpenAI (`sk-...`) | + +#### IA - Ollama + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `OLLAMA_BASE_URL` | - | URL du service Ollama | + +#### IA - Custom provider + +| Variable | Description | +|----------|-------------| +| `CUSTOM_OPENAI_API_KEY` | Cle API du provider | +| `CUSTOM_OPENAI_BASE_URL` | URL de base du provider | + +#### MCP Server + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `MCP_MODE` | `stdio` | Mode (`stdio` ou `sse`) | +| `MCP_PORT` | `3001` | Port SSE | +| `MCP_LOG_LEVEL` | `info` | Niveau de log | +| `MCP_REQUEST_TIMEOUT` | `30000` | Timeout (ms) | +| `MCP_REQUIRE_AUTH` | `false` | Activer authentification | +| `MCP_API_KEY` | - | Cle API pour auth | +| `APP_BASE_URL` | `http://localhost:3000` | URL de l'app | +| `USER_ID` | - | Filtrer par utilisateur | + +#### Email + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `SMTP_HOST` | - | Serveur SMTP | +| `SMTP_PORT` | `587` | Port SMTP | +| `SMTP_USER` | - | Utilisateur SMTP | +| `SMTP_PASS` | - | Mot de passe SMTP | +| `SMTP_FROM` | `noreply@memento.app` | Email expediteur | +| `RESEND_API_KEY` | - | Cle API Resend | + +#### Application + +| Variable | Defaut | Description | +|----------|--------|-------------| +| `ALLOW_REGISTRATION` | `true` | Autoriser l'inscription publique | +| `NODE_ENV` | `development` | Environnement | +| `PORT` | `3000` | Port de l'app | +| `HOSTNAME` | `0.0.0.0` | Host d'ecoute | + +--- + +## Commandes utiles + +### Docker + +```bash +# Demarrer tous les services +docker compose up -d + +# Avec Ollama +docker compose --profile ollama up -d + +# Voir les logs +docker compose logs -f +docker compose logs -f memento-note # App seulement +docker compose logs -f mcp-server # MCP seulement + +# Statut des services +docker compose ps + +# Reconstruire apres modification +docker compose down +docker compose build --no-cache +docker compose up -d + +# Acceder a un conteneur +docker compose exec memento-note sh +docker compose exec mcp-server sh + +# Arreter et supprimer les volumes (ATTENTION : perte de donnees) +docker compose down -v +``` + +### Base de donnees + +```bash +# Lancer Prisma Studio (GUI pour la DB) +cd memento-note +npx prisma studio + +# Creer une migration +npx prisma migrate dev --name nom_de_la_migration + +# Appliquer les migrations en production +npx prisma migrate deploy + +# Regenerer le client Prisma +npx prisma generate +``` + +### Developpement + +```bash +cd memento-note + +# Dev avec hot-reload +npm run dev + +# Build production +npm run build +npm start + +# Linter +npm run lint +``` + +### Deploy.sh (script de deploiement) + +```bash +cd memento-note +chmod +x deploy.sh + +./deploy.sh build # Construire l'image +./deploy.sh start # Demarrer les conteneurs +./deploy.sh logs # Voir les logs +./deploy.sh backup # Sauvegarder la DB +./deploy.sh update # Mettre a jour l'app +``` + +--- + +## Resolution des problemes + +### Erreur : `ECONNREFUSED 127.0.0.1:11434` + +**Cause** : L'app essaie d'acceder a Ollama via localhost au lieu du service Docker. + +**Solution** : Verifier que `.env.docker` contient : +``` +OLLAMA_BASE_URL="http://ollama:11434" +``` + +### Le provider IA ne change pas dans l'admin + +1. Sauvegarder les modifications dans l'admin +2. Rafraichir la page (F5) +3. Verifier la valeur affichee sous le dropdown + +### Docker ne demarre pas + +```bash +# Verifier les logs +docker compose logs memento-note + +# Verifier la DB +docker compose exec postgres pg_isready -U memento + +# Reconstruire +docker compose build --no-cache memento-note +``` + +### Mot de passe oublie sans SMTP + +Si vous n'avez pas configure l'email, vous pouvez reinitialiser le mot de passe manuellement : + +```bash +# Se connecter a la DB +docker compose exec postgres psql -U memento -d memento + +# ou en local +cd memento-note +npx prisma studio +``` + +### Les embeddings ne se generent pas + +1. Verifier que le provider d'embeddings est correctement configure +2. Verifier que le modele d'embedding est disponible +3. En admin, lancer la regeneration manuelle des embeddings + +--- + +## Licence + +Apache License 2.0 avec Commons Clause Restriction. +Usage personnel et educatif libre. Usage commercial interdit sans autorisation ecrite de l'auteur. +Voir le fichier [LICENSE](LICENSE) pour les details complets. diff --git a/docker-compose.yml b/docker-compose.yml index 55d00bd..d3ae574 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,15 +7,15 @@ services: container_name: memento-postgres restart: unless-stopped environment: - POSTGRES_USER: keepnotes - POSTGRES_PASSWORD: keepnotes - POSTGRES_DB: keepnotes + POSTGRES_USER: ${POSTGRES_USER:-memento} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-memento} + POSTGRES_DB: ${POSTGRES_DB:-memento} volumes: - postgres-data:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: - test: ["CMD-SHELL", "pg_isready -U keepnotes"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-memento}"] interval: 5s timeout: 5s retries: 5 @@ -35,7 +35,7 @@ services: ports: - "3000:3000" environment: - - DATABASE_URL=postgresql://keepnotes:keepnotes@postgres:5432/keepnotes + - DATABASE_URL=postgresql://${POSTGRES_USER:-memento}:${POSTGRES_PASSWORD:-memento}@postgres:5432/${POSTGRES_DB:-memento} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-changethisinproduction} - NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000} - NODE_ENV=production @@ -51,11 +51,16 @@ services: # AI Providers - AI_PROVIDER_TAGS=${AI_PROVIDER_TAGS} - AI_PROVIDER_EMBEDDING=${AI_PROVIDER_EMBEDDING} + - AI_PROVIDER_CHAT=${AI_PROVIDER_CHAT} - OPENAI_API_KEY=${OPENAI_API_KEY} - OLLAMA_BASE_URL=${OLLAMA_BASE_URL} - - OLLAMA_MODEL=${OLLAMA_MODEL} - AI_MODEL_TAGS=${AI_MODEL_TAGS} - AI_MODEL_EMBEDDING=${AI_MODEL_EMBEDDING} + - AI_MODEL_CHAT=${AI_MODEL_CHAT} + - CUSTOM_OPENAI_API_KEY=${CUSTOM_OPENAI_API_KEY} + - CUSTOM_OPENAI_BASE_URL=${CUSTOM_OPENAI_BASE_URL} + - ALLOW_REGISTRATION=${ALLOW_REGISTRATION:-true} + - RESEND_API_KEY=${RESEND_API_KEY} volumes: - uploads-data:/app/public/uploads depends_on: @@ -97,7 +102,7 @@ services: - MCP_MODE=${MCP_MODE:-stdio} - PORT=${MCP_PORT:-3001} # Database - connect to shared PostgreSQL - - DATABASE_URL=postgresql://keepnotes:keepnotes@postgres:5432/keepnotes + - DATABASE_URL=postgresql://${POSTGRES_USER:-memento}:${POSTGRES_PASSWORD:-memento}@postgres:5432/${POSTGRES_DB:-memento} - NODE_ENV=production depends_on: postgres: diff --git a/mcp-server/.env.example b/mcp-server/.env.example new file mode 100644 index 0000000..0bc30b6 --- /dev/null +++ b/mcp-server/.env.example @@ -0,0 +1,50 @@ +# ============================================================================= +# MCP Server - Environment Variables +# ============================================================================= +# Copy this file to .env and fill in the values. +# cp .env.example .env + +# ============================================================================= +# DATABASE (REQUIRED) +# ============================================================================= +# Local development (if sharing SQLite with memento-note): +# DATABASE_URL="file:../memento-note/prisma/dev.db" +# PostgreSQL: +DATABASE_URL="postgresql://memento:memento@localhost:5432/memento" + +# ============================================================================= +# MCP SERVER MODE +# ============================================================================= +# 'stdio' = Standard I/O (Claude Desktop, Cline, etc.) +# 'sse' = Server-Sent Events / HTTP (N8N, remote access, port 3001) +MCP_MODE="stdio" + +# ============================================================================= +# SSE MODE SETTINGS (only if MCP_MODE="sse") +# ============================================================================= +# Port for HTTP/SSE mode +# PORT=3001 + +# Request timeout in milliseconds (default: 30000) +# MCP_REQUEST_TIMEOUT=30000 + +# Log level: debug, info, warn, error (default: info) +# MCP_LOG_LEVEL=info + +# ============================================================================= +# AUTHENTICATION (SSE mode only) +# ============================================================================= +# Enable authentication (default: false in dev mode) +# MCP_REQUIRE_AUTH=false + +# Static API key for authentication (if MCP_REQUIRE_AUTH=true) +# MCP_API_KEY="your-secret-api-key" + +# ============================================================================= +# APPLICATION +# ============================================================================= +# Base URL of the Memento web app (for generating links in responses) +# APP_BASE_URL="http://localhost:3000" + +# Filter data to a specific user ID (optional, null = all users) +# USER_ID="" diff --git a/memento-note/.env.example b/memento-note/.env.example new file mode 100644 index 0000000..4e567df --- /dev/null +++ b/memento-note/.env.example @@ -0,0 +1,64 @@ +# ============================================================================= +# Memento Note - Environment Variables +# Copy this file to .env and fill in the values +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Core (required) +# ----------------------------------------------------------------------------- +DATABASE_URL="postgresql://user:password@localhost:5432/memento" +NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32" +NEXTAUTH_URL="http://localhost:3000" + +# ----------------------------------------------------------------------------- +# Registration +# ----------------------------------------------------------------------------- +# Set to "false" to disable public registration (default: true) +# ALLOW_REGISTRATION="true" + +# ----------------------------------------------------------------------------- +# AI Providers +# ----------------------------------------------------------------------------- +# Main provider: "openai" | "ollama" | "deepseek" | "openrouter" | "custom-openai" +# AI_PROVIDER="openai" + +# Per-feature provider overrides (optional, falls back to AI_PROVIDER) +# AI_PROVIDER_CHAT="openai" +# AI_PROVIDER_TAGS="openai" +# AI_PROVIDER_EMBEDDING="openai" + +# Model names (optional, uses provider defaults) +# AI_MODEL_CHAT="gpt-4o-mini" +# AI_MODEL_TAGS="gpt-4o-mini" +# AI_MODEL_EMBEDDING="text-embedding-3-small" + +# OpenAI +# OPENAI_API_KEY="sk-..." + +# Ollama (local) +# OLLAMA_BASE_URL="http://localhost:11434" + +# Custom OpenAI-compatible endpoint +# CUSTOM_OPENAI_API_KEY="..." +# CUSTOM_OPENAI_BASE_URL="https://your-provider.com/v1" + +# ----------------------------------------------------------------------------- +# Email (at least one provider required for password reset) +# ----------------------------------------------------------------------------- +# Resend (https://resend.com) +# RESEND_API_KEY="re_..." + +# SMTP +# SMTP_HOST="smtp.example.com" +# SMTP_PORT="587" +# SMTP_USER="" +# SMTP_PASS="" +# SMTP_FROM="noreply@example.com" +# SMTP_SECURE="false" +# SMTP_IGNORE_CERT="false" + +# ----------------------------------------------------------------------------- +# MCP (Model Context Protocol) +# ----------------------------------------------------------------------------- +# MCP_SERVER_MODE="disabled" +# MCP_SERVER_URL="" diff --git a/memento-note/.gitignore b/memento-note/.gitignore index 86dde0f..9b1afb1 100644 --- a/memento-note/.gitignore +++ b/memento-note/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel diff --git a/memento-note/app/(main)/settings/ai/page-new.tsx b/memento-note/app/(main)/settings/ai/page-new.tsx deleted file mode 100644 index e44276b..0000000 --- a/memento-note/app/(main)/settings/ai/page-new.tsx +++ /dev/null @@ -1,184 +0,0 @@ -'use client' - -import { useState } from 'react' -import { SettingsNav, SettingsSection, SettingToggle, SettingSelect, SettingInput } from '@/components/settings' -import { updateAISettings } from '@/app/actions/ai-settings' -import { toast } from 'sonner' -import { useLanguage } from '@/lib/i18n' - -export default function AISettingsPage() { - const { t } = useLanguage() - const [apiKey, setApiKey] = useState('') - - // Mock settings state - in real implementation, load from server - const [settings, setSettings] = useState({ - titleSuggestions: true, - semanticSearch: true, - paragraphRefactor: true, - memoryEcho: true, - memoryEchoFrequency: 'daily' as 'daily' | 'weekly' | 'custom', - aiProvider: 'auto' as 'auto' | 'openai' | 'ollama', - preferredLanguage: 'auto' as 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl', - demoMode: false - }) - - const handleToggle = async (feature: string, value: boolean) => { - setSettings(prev => ({ ...prev, [feature]: value })) - try { - await updateAISettings({ [feature]: value }) - } catch (error) { - toast.error(t('aiSettings.error')) - setSettings(settings) // Revert on error - } - } - - const handleFrequencyChange = async (value: string) => { - setSettings(prev => ({ ...prev, memoryEchoFrequency: value as any })) - try { - await updateAISettings({ memoryEchoFrequency: value as any }) - } catch (error) { - toast.error(t('aiSettings.error')) - } - } - - const handleProviderChange = async (value: string) => { - setSettings(prev => ({ ...prev, aiProvider: value as any })) - try { - await updateAISettings({ aiProvider: value as any }) - } catch (error) { - toast.error(t('aiSettings.error')) - } - } - - const handleApiKeyChange = async (value: string) => { - setApiKey(value) - // TODO: Implement API key persistence - - } - - return ( -
-
- {/* Sidebar Navigation */} - - - {/* Main Content */} -
-
-

{t('aiSettings.title')}

-

- {t('aiSettings.description')} -

-
- - {/* AI Provider */} - 🤖} - description={t('aiSettings.providerDesc')} - > - - - {settings.aiProvider === 'openai' && ( - - )} - - - {/* Feature Toggles */} - ✨} - description={t('aiSettings.description')} - > - handleToggle('titleSuggestions', checked)} - /> - - handleToggle('semanticSearch', checked)} - /> - - handleToggle('paragraphRefactor', checked)} - /> - - handleToggle('memoryEcho', checked)} - /> - - {settings.memoryEcho && ( - - )} - - - {/* Demo Mode */} - 🎭} - description={t('demoMode.description')} - > - handleToggle('demoMode', checked)} - /> - -
-
-
- ) -} diff --git a/memento-note/app/(main)/settings/profile/page-new.tsx b/memento-note/app/(main)/settings/profile/page-new.tsx deleted file mode 100644 index f81e852..0000000 --- a/memento-note/app/(main)/settings/profile/page-new.tsx +++ /dev/null @@ -1,155 +0,0 @@ -'use client' - -import { useState } from 'react' -import { SettingsNav, SettingsSection, SettingToggle, SettingInput, SettingSelect } from '@/components/settings' -import { updateAISettings } from '@/app/actions/ai-settings' -import { toast } from 'sonner' -import { useLanguage } from '@/lib/i18n' - -export default function ProfileSettingsPage() { - const { t } = useLanguage() - - // Mock user data - in real implementation, load from server - const [user, setUser] = useState({ - name: 'John Doe', - email: 'john@example.com' - }) - - const [language, setLanguage] = useState('auto') - const [showRecentNotes, setShowRecentNotes] = useState(false) - - const handleNameChange = async (value: string) => { - setUser(prev => ({ ...prev, name: value })) - // TODO: Implement profile update - - } - - const handleEmailChange = async (value: string) => { - setUser(prev => ({ ...prev, email: value })) - // TODO: Implement email update - - } - - const handleLanguageChange = async (value: string) => { - setLanguage(value) - try { - await updateAISettings({ preferredLanguage: value as any }) - } catch (error) { - console.error('Error updating language:', error) - toast.error(t('aiSettings.error')) - } - } - - const handleRecentNotesChange = async (enabled: boolean) => { - setShowRecentNotes(enabled) - try { - await updateAISettings({ showRecentNotes: enabled }) - } catch (error) { - console.error('Error updating recent notes setting:', error) - toast.error(t('aiSettings.error')) - } - } - - return ( -
-
- {/* Sidebar Navigation */} - - - {/* Main Content */} -
-
-

{t('profile.title')}

-

- {t('profile.description')} -

-
- - {/* Profile Information */} - 👤} - description={t('profile.description')} - > - - - - - - {/* Preferences */} - ⚙️} - description={t('profile.languagePreferencesDescription')} - > - - - - - - {/* AI Settings Link */} -
-
-
-
-

{t('aiSettings.title')}

-

- {t('aiSettings.description')} -

-
- -
-
-
-
-
- ) -} diff --git a/memento-note/app/actions/auth-reset.ts b/memento-note/app/actions/auth-reset.ts index e9498fb..f4ba1a4 100644 --- a/memento-note/app/actions/auth-reset.ts +++ b/memento-note/app/actions/auth-reset.ts @@ -6,7 +6,7 @@ import { getSystemConfig } from '@/lib/config' import bcrypt from 'bcryptjs' import { getEmailTemplate } from '@/lib/email-template' -// Helper simple pour générer un token sans dépendance externe lourde +// Simple helper to generate a token without heavy external dependencies function generateToken() { const array = new Uint8Array(32); globalThis.crypto.getRandomValues(array); @@ -19,7 +19,7 @@ export async function forgotPassword(email: string) { try { const user = await prisma.user.findUnique({ where: { email: email.toLowerCase() } }); if (!user) { - // Pour des raisons de sécurité, on ne dit pas si l'email existe ou pas + // For security reasons, don't reveal whether the email exists return { success: true }; } @@ -34,7 +34,7 @@ export async function forgotPassword(email: string) { } }); - const resetLink = `${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/reset-password?token=${token}`; + const resetLink = `${process.env.NEXTAUTH_URL}/reset-password?token=${token}`; const html = getEmailTemplate( "Reset your Password", diff --git a/memento-note/app/actions/profile-broken.ts b/memento-note/app/actions/profile-broken.ts deleted file mode 100644 index 4e0da9a..0000000 --- a/memento-note/app/actions/profile-broken.ts +++ /dev/null @@ -1,232 +0,0 @@ -'use server' - -import { revalidatePath } from 'next/cache' -import prisma from '@/lib/prisma' -import { auth } from '@/auth' -import bcrypt from 'bcryptjs' -import { z } from 'zod' - -const ProfileSchema = z.object({ - name: z.string().min(2, "Name must be at least 2 characters"), - email: z.string().email().optional(), // Email change might require verification logic, keeping it simple for now or read-only -}) - -const PasswordSchema = z.object({ - currentPassword: z.string().min(1, "Current password is required"), - newPassword: z.string().min(6, "New password must be at least 6 characters"), - confirmPassword: z.string().min(6, "Confirm password must be at least 6 characters"), -}).refine((data) => data.newPassword === data.confirmPassword, { - message: "Passwords don't match", - path: ["confirmPassword"], -}) - -export async function updateProfile(data: { name: string }) { - const session = await auth() - if (!session?.user?.id) throw new Error('Unauthorized') - - const validated = ProfileSchema.safeParse(data) - if (!validated.success) { - return { error: validated.error.flatten().fieldErrors } - } - - try { - await prisma.user.update({ - where: { id: session.user.id }, - data: { name: validated.data.name }, - }) - revalidatePath('/settings/profile') - return { success: true } - } catch (error) { - return { error: { _form: ['Failed to update profile'] } } - } -} - -export async function changePassword(formData: FormData) { - const session = await auth() - if (!session?.user?.id) throw new Error('Unauthorized') - - const rawData = { - currentPassword: formData.get('currentPassword'), - newPassword: formData.get('newPassword'), - confirmPassword: formData.get('confirmPassword'), - } - - const validated = PasswordSchema.safeParse(rawData) - if (!validated.success) { - return { error: validated.error.flatten().fieldErrors } - } - - const { currentPassword, newPassword } = validated.data - - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - }) - - if (!user || !user.password) { - return { error: { _form: ['User not found'] } } - } - - const passwordsMatch = await bcrypt.compare(currentPassword, user.password) - if (!passwordsMatch) { - return { error: { currentPassword: ['Incorrect current password'] } } - } - - const hashedPassword = await bcrypt.hash(newPassword, 10) - - try { - await prisma.user.update({ - where: { id: session.user.id }, - data: { password: hashedPassword }, - }) - return { success: true } - } catch (error) { - return { error: { _form: ['Failed to change password'] } } - } -} - -export async function updateTheme(theme: string) { - const session = await auth() - if (!session?.user?.id) return { error: 'Unauthorized' } - - try { - await prisma.user.update({ - where: { id: session.user.id }, - data: { theme }, - }) - revalidatePath('/') - revalidatePath('/settings/profile') - return { success: true } - } catch (error) { - return { error: 'Failed to update theme' } - } -} - -export async function updateLanguage(language: string) { - const session = await auth() - if (!session?.user?.id) return { error: 'Unauthorized' } - - try { - // Update or create UserAISettings with the preferred language - await prisma.userAISettings.upsert({ - where: { userId: session.user.id }, - create: { - userId: session.user.id, - preferredLanguage: language, - }, - update: { - preferredLanguage: language, - }, - }) - - // Note: The language will be applied on next page load - // The client component should handle updating localStorage and reloading - revalidatePath('/') - revalidatePath('/settings/profile') - return { success: true, language } - } catch (error) { - console.error('Failed to update language:', error) - return { error: 'Failed to update language' } - } -} - -export async function updateFontSize(fontSize: string) { - const session = await auth() - if (!session?.user?.id) return { error: 'Unauthorized' } - - try { - // Check if UserAISettings exists - const existing = await prisma.userAISettings.findUnique({ - where: { userId: session.user.id } - }) - - let result - if (existing) { - // Update existing - only update fontSize field - result = await prisma.userAISettings.update({ - where: { userId: session.user.id }, - data: { fontSize: fontSize } - }) - } else { - // Create new with all required fields - result = await prisma.userAISettings.create({ - data: { - userId: session.user.id, - fontSize: fontSize, - // Set default values for required fields - titleSuggestions: true, - semanticSearch: true, - paragraphRefactor: true, - memoryEcho: true, - memoryEchoFrequency: 'daily', - aiProvider: 'auto', - preferredLanguage: 'auto', - showRecentNotes: false - } - }) - } - - revalidatePath('/') - revalidatePath('/settings/profile') - return { success: true, fontSize } - } catch (error) { - console.error('[updateFontSize] Failed to update font size:', error) - return { error: 'Failed to update font size' } - } -} - -export async function updateShowRecentNotes(showRecentNotes: boolean) { - const session = await auth() - if (!session?.user?.id) return { error: 'Unauthorized' } - - try { - // Use EXACT same pattern as updateFontSize which works - const existing = await prisma.userAISettings.findUnique({ - where: { userId: session.user.id } - }) - - if (existing) { - // Try Prisma client first, fallback to raw SQL if field doesn't exist in client - try { - await prisma.userAISettings.update({ - where: { userId: session.user.id }, - data: { showRecentNotes: showRecentNotes } as any - }) - } catch (prismaError: any) { - // If Prisma client doesn't know about showRecentNotes, use raw SQL - if (prismaError?.message?.includes('Unknown argument') || prismaError?.code === 'P2009') { - const value = showRecentNotes ? 1 : 0 - await prisma.$executeRaw` - UPDATE UserAISettings - SET showRecentNotes = ${value} - WHERE userId = ${session.user.id} - ` - } else { - throw prismaError - } - } - } else { - // Create new - same as updateFontSize - await prisma.userAISettings.create({ - data: { - userId: session.user.id, - titleSuggestions: true, - semanticSearch: true, - paragraphRefactor: true, - memoryEcho: true, - memoryEchoFrequency: 'daily', - aiProvider: 'auto', - preferredLanguage: 'auto', - fontSize: 'medium', - showRecentNotes: showRecentNotes - } as any - }) - } - - revalidatePath('/') - revalidatePath('/settings/profile') - return { success: true, showRecentNotes } - } catch (error) { - console.error('[updateShowRecentNotes] Failed:', error) - return { error: 'Failed to update show recent notes setting' } - } -} diff --git a/memento-note/app/actions/profile-old.ts b/memento-note/app/actions/profile-old.ts deleted file mode 100644 index 4e0da9a..0000000 --- a/memento-note/app/actions/profile-old.ts +++ /dev/null @@ -1,232 +0,0 @@ -'use server' - -import { revalidatePath } from 'next/cache' -import prisma from '@/lib/prisma' -import { auth } from '@/auth' -import bcrypt from 'bcryptjs' -import { z } from 'zod' - -const ProfileSchema = z.object({ - name: z.string().min(2, "Name must be at least 2 characters"), - email: z.string().email().optional(), // Email change might require verification logic, keeping it simple for now or read-only -}) - -const PasswordSchema = z.object({ - currentPassword: z.string().min(1, "Current password is required"), - newPassword: z.string().min(6, "New password must be at least 6 characters"), - confirmPassword: z.string().min(6, "Confirm password must be at least 6 characters"), -}).refine((data) => data.newPassword === data.confirmPassword, { - message: "Passwords don't match", - path: ["confirmPassword"], -}) - -export async function updateProfile(data: { name: string }) { - const session = await auth() - if (!session?.user?.id) throw new Error('Unauthorized') - - const validated = ProfileSchema.safeParse(data) - if (!validated.success) { - return { error: validated.error.flatten().fieldErrors } - } - - try { - await prisma.user.update({ - where: { id: session.user.id }, - data: { name: validated.data.name }, - }) - revalidatePath('/settings/profile') - return { success: true } - } catch (error) { - return { error: { _form: ['Failed to update profile'] } } - } -} - -export async function changePassword(formData: FormData) { - const session = await auth() - if (!session?.user?.id) throw new Error('Unauthorized') - - const rawData = { - currentPassword: formData.get('currentPassword'), - newPassword: formData.get('newPassword'), - confirmPassword: formData.get('confirmPassword'), - } - - const validated = PasswordSchema.safeParse(rawData) - if (!validated.success) { - return { error: validated.error.flatten().fieldErrors } - } - - const { currentPassword, newPassword } = validated.data - - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - }) - - if (!user || !user.password) { - return { error: { _form: ['User not found'] } } - } - - const passwordsMatch = await bcrypt.compare(currentPassword, user.password) - if (!passwordsMatch) { - return { error: { currentPassword: ['Incorrect current password'] } } - } - - const hashedPassword = await bcrypt.hash(newPassword, 10) - - try { - await prisma.user.update({ - where: { id: session.user.id }, - data: { password: hashedPassword }, - }) - return { success: true } - } catch (error) { - return { error: { _form: ['Failed to change password'] } } - } -} - -export async function updateTheme(theme: string) { - const session = await auth() - if (!session?.user?.id) return { error: 'Unauthorized' } - - try { - await prisma.user.update({ - where: { id: session.user.id }, - data: { theme }, - }) - revalidatePath('/') - revalidatePath('/settings/profile') - return { success: true } - } catch (error) { - return { error: 'Failed to update theme' } - } -} - -export async function updateLanguage(language: string) { - const session = await auth() - if (!session?.user?.id) return { error: 'Unauthorized' } - - try { - // Update or create UserAISettings with the preferred language - await prisma.userAISettings.upsert({ - where: { userId: session.user.id }, - create: { - userId: session.user.id, - preferredLanguage: language, - }, - update: { - preferredLanguage: language, - }, - }) - - // Note: The language will be applied on next page load - // The client component should handle updating localStorage and reloading - revalidatePath('/') - revalidatePath('/settings/profile') - return { success: true, language } - } catch (error) { - console.error('Failed to update language:', error) - return { error: 'Failed to update language' } - } -} - -export async function updateFontSize(fontSize: string) { - const session = await auth() - if (!session?.user?.id) return { error: 'Unauthorized' } - - try { - // Check if UserAISettings exists - const existing = await prisma.userAISettings.findUnique({ - where: { userId: session.user.id } - }) - - let result - if (existing) { - // Update existing - only update fontSize field - result = await prisma.userAISettings.update({ - where: { userId: session.user.id }, - data: { fontSize: fontSize } - }) - } else { - // Create new with all required fields - result = await prisma.userAISettings.create({ - data: { - userId: session.user.id, - fontSize: fontSize, - // Set default values for required fields - titleSuggestions: true, - semanticSearch: true, - paragraphRefactor: true, - memoryEcho: true, - memoryEchoFrequency: 'daily', - aiProvider: 'auto', - preferredLanguage: 'auto', - showRecentNotes: false - } - }) - } - - revalidatePath('/') - revalidatePath('/settings/profile') - return { success: true, fontSize } - } catch (error) { - console.error('[updateFontSize] Failed to update font size:', error) - return { error: 'Failed to update font size' } - } -} - -export async function updateShowRecentNotes(showRecentNotes: boolean) { - const session = await auth() - if (!session?.user?.id) return { error: 'Unauthorized' } - - try { - // Use EXACT same pattern as updateFontSize which works - const existing = await prisma.userAISettings.findUnique({ - where: { userId: session.user.id } - }) - - if (existing) { - // Try Prisma client first, fallback to raw SQL if field doesn't exist in client - try { - await prisma.userAISettings.update({ - where: { userId: session.user.id }, - data: { showRecentNotes: showRecentNotes } as any - }) - } catch (prismaError: any) { - // If Prisma client doesn't know about showRecentNotes, use raw SQL - if (prismaError?.message?.includes('Unknown argument') || prismaError?.code === 'P2009') { - const value = showRecentNotes ? 1 : 0 - await prisma.$executeRaw` - UPDATE UserAISettings - SET showRecentNotes = ${value} - WHERE userId = ${session.user.id} - ` - } else { - throw prismaError - } - } - } else { - // Create new - same as updateFontSize - await prisma.userAISettings.create({ - data: { - userId: session.user.id, - titleSuggestions: true, - semanticSearch: true, - paragraphRefactor: true, - memoryEcho: true, - memoryEchoFrequency: 'daily', - aiProvider: 'auto', - preferredLanguage: 'auto', - fontSize: 'medium', - showRecentNotes: showRecentNotes - } as any - }) - } - - revalidatePath('/') - revalidatePath('/settings/profile') - return { success: true, showRecentNotes } - } catch (error) { - console.error('[updateShowRecentNotes] Failed:', error) - return { error: 'Failed to update show recent notes setting' } - } -} diff --git a/memento-note/auth.config.ts b/memento-note/auth.config.ts index b5b668a..865f49b 100644 --- a/memento-note/auth.config.ts +++ b/memento-note/auth.config.ts @@ -5,7 +5,7 @@ export const authConfig = { signIn: '/login', newUser: '/register', }, - secret: "csQFtfYvQ8YtatEYSUFyslXdk2vJhZFt9D5gav/RJQg=", + secret: process.env.NEXTAUTH_SECRET, trustHost: true, session: { strategy: 'jwt', diff --git a/memento-note/components/note-editor.tsx b/memento-note/components/note-editor.tsx index 3744536..32f93fa 100644 --- a/memento-note/components/note-editor.tsx +++ b/memento-note/components/note-editor.tsx @@ -113,25 +113,25 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) const [comparisonNotes, setComparisonNotes] = useState>>([]) const [fusionNotes, setFusionNotes] = useState>>([]) - // Tags rejetés par l'utilisateur pour cette session + // Tags dismissed by the user for this session const [dismissedTags, setDismissedTags] = useState([]) const colorClasses = NOTE_COLORS[color as NoteColor] || NOTE_COLORS.default const handleSelectGhostTag = async (tag: string) => { - // Vérification insensible à la casse + // Case-insensitive check const tagExists = labels.some(l => l.toLowerCase() === tag.toLowerCase()) if (!tagExists) { setLabels(prev => [...prev, tag]) - // Créer le label globalement s'il n'existe pas + // Create the label globally if it doesn't exist const globalExists = globalLabels.some(l => l.name.toLowerCase() === tag.toLowerCase()) if (!globalExists) { try { await addLabel(tag) } catch (err) { - console.error('Erreur création label auto:', err) + console.error('Error creating auto-label:', err) } } toast.success(t('ai.tagAdded', { tag })) @@ -142,8 +142,8 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) setDismissedTags(prev => [...prev, tag]) } - // Filtrer les suggestions pour ne pas afficher celles rejetées par l'utilisateur - // ni celles déjà présentes sur la note + // Filter suggestions to exclude dismissed ones + // and those already present on the note const existingLabelsLower = (note.labels || []).map((l) => l.toLowerCase()) const filteredSuggestions = suggestions.filter(s => { if (!s || !s.tag) return false @@ -241,7 +241,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) setTitleSuggestions(data.suggestions || []) toast.success(t('ai.titlesGenerated', { count: data.suggestions.length })) } catch (error: any) { - console.error('Erreur génération titres:', error) + console.error('Error generating titles:', error) toast.error(error.message || t('ai.titleGenerationFailed')) } finally { setIsGeneratingTitles(false) @@ -311,7 +311,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) option: data.option }) } catch (error: any) { - console.error('Erreur reformulation:', error) + console.error('Error reformulating:', error) toast.error(error.message || t('ai.reformulationFailed')) } finally { setIsReformulating(false) @@ -494,10 +494,10 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {}) } - // Rafraîchir les labels globaux pour refléter les suppressions éventuelles (orphans) + // Refresh global labels to reflect any deletions (orphans) await refreshLabels() - // Rafraîchir la liste des notes + // Refresh the notes list triggerRefresh() onClose() diff --git a/memento-note/hooks/use-auto-tagging.ts b/memento-note/hooks/use-auto-tagging.ts index 932718a..f2336a2 100644 --- a/memento-note/hooks/use-auto-tagging.ts +++ b/memento-note/hooks/use-auto-tagging.ts @@ -15,14 +15,14 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT const [isAnalyzing, setIsAnalyzing] = useState(false); const [error, setError] = useState(null); - // Debounce le contenu de 1.5s + // Debounce content by 1.5s const debouncedContent = useDebounce(content, 1500); // Track previous notebookId to detect when note is moved to a notebook const previousNotebookId = useRef(notebookId); const analyzeContent = async (contentToAnalyze: string) => { - // CRITICAL: Don't suggest labels in "Notes générales" (notebookId is null) + // CRITICAL: Don't suggest labels in "General Notes" (notebookId is null) // Labels should ONLY appear within notebooks, not in the general notes section if (!notebookId) { setSuggestions([]); @@ -49,13 +49,13 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT }); if (!response.ok) { - throw new Error('Erreur lors de l\'analyse'); + throw new Error('Error during analysis'); } const data = await response.json(); setSuggestions(data.tags || []); } catch (err) { - setError('Impossible de générer des suggestions'); + setError('Failed to generate suggestions'); } finally { setIsAnalyzing(false); } @@ -78,7 +78,7 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT const prev = previousNotebookId.current; previousNotebookId.current = notebookId; - // Detect when note is moved FROM "Notes générales" (null) TO a notebook + // Detect when note is moved FROM "General Notes" (null) TO a notebook const wasMovedToNotebook = (prev === null || prev === undefined) && notebookId; if (wasMovedToNotebook && content && content.length >= 10) { diff --git a/memento-note/hooks/use-title-suggestions.ts b/memento-note/hooks/use-title-suggestions.ts index 7281aaa..b76fd20 100644 --- a/memento-note/hooks/use-title-suggestions.ts +++ b/memento-note/hooks/use-title-suggestions.ts @@ -17,7 +17,7 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest const [isAnalyzing, setIsAnalyzing] = useState(false) const [error, setError] = useState(null) - // Debounce le contenu de 2s pour éviter trop d'appels + // Debounce content by 2s to avoid excessive API calls const debouncedContent = useDebounce(content, 2000) useEffect(() => { @@ -28,7 +28,7 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest const wordCount = debouncedContent.split(/\s+/).length - // Il faut au moins 10 mots + // Need at least 10 words if (wordCount < 10) { setSuggestions([]) return @@ -49,14 +49,14 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest if (!response.ok) { const errorData = await response.json() - throw new Error(errorData.error || 'Erreur lors de la génération des titres') + throw new Error(errorData.error || 'Error generating title suggestions') } const data = await response.json() setSuggestions(data.suggestions || []) } catch (err) { console.error('❌ Title suggestions error:', err) - setError('Impossible de générer des suggestions de titres') + setError('Failed to generate title suggestions') } finally { setIsAnalyzing(false) } diff --git a/memento-note/lib/ai/providers/custom-openai.ts b/memento-note/lib/ai/providers/custom-openai.ts index fec4e4b..535256a 100644 --- a/memento-note/lib/ai/providers/custom-openai.ts +++ b/memento-note/lib/ai/providers/custom-openai.ts @@ -40,17 +40,17 @@ export class CustomOpenAIProvider implements AIProvider { model: this.model, schema: z.object({ tags: z.array(z.object({ - tag: z.string().describe('Le nom du tag, court et en minuscules'), - confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1') + tag: z.string().describe('Short tag name in lowercase'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') })) }), - prompt: `Analyse la note suivante et suggère entre 1 et 5 tags pertinents. - Contenu de la note: "${content}"`, + prompt: `Analyze the following note and suggest 1 to 5 relevant tags. + Note content: "${content}"`, }); return object.tags; } catch (e) { - console.error('Erreur génération tags Custom OpenAI:', e); + console.error('Error generating tags (Custom OpenAI):', e); return []; } } @@ -63,7 +63,7 @@ export class CustomOpenAIProvider implements AIProvider { }); return embedding; } catch (e) { - console.error('Erreur embeddings Custom OpenAI:', e); + console.error('Error generating embeddings (Custom OpenAI):', e); return []; } } @@ -74,8 +74,8 @@ export class CustomOpenAIProvider implements AIProvider { model: this.model, schema: z.object({ titles: z.array(z.object({ - title: z.string().describe('Le titre suggéré'), - confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1') + title: z.string().describe('Suggested title'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') })) }), prompt: prompt, @@ -83,7 +83,7 @@ export class CustomOpenAIProvider implements AIProvider { return object.titles; } catch (e) { - console.error('Erreur génération titres Custom OpenAI:', e); + console.error('Error generating titles (Custom OpenAI):', e); return []; } } @@ -97,7 +97,7 @@ export class CustomOpenAIProvider implements AIProvider { return text.trim(); } catch (e) { - console.error('Erreur génération texte Custom OpenAI:', e); + console.error('Error generating text (Custom OpenAI):', e); throw e; } } @@ -112,7 +112,7 @@ export class CustomOpenAIProvider implements AIProvider { return { text: text.trim() }; } catch (e) { - console.error('Erreur chat Custom OpenAI:', e); + console.error('Error in chat (Custom OpenAI):', e); throw e; } } diff --git a/memento-note/lib/ai/providers/deepseek.ts b/memento-note/lib/ai/providers/deepseek.ts index b4db2c9..d9abd51 100644 --- a/memento-note/lib/ai/providers/deepseek.ts +++ b/memento-note/lib/ai/providers/deepseek.ts @@ -24,17 +24,17 @@ export class DeepSeekProvider implements AIProvider { model: this.model, schema: z.object({ tags: z.array(z.object({ - tag: z.string().describe('Le nom du tag, court et en minuscules'), - confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1') + tag: z.string().describe('Short tag name in lowercase'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') })) }), - prompt: `Analyse la note suivante et suggère entre 1 et 5 tags pertinents. - Contenu de la note: "${content}"`, + prompt: `Analyze the following note and suggest 1 to 5 relevant tags. + Note content: "${content}"`, }); return object.tags; } catch (e) { - console.error('Erreur génération tags DeepSeek:', e); + console.error('Error generating tags (DeepSeek):', e); return []; } } @@ -47,7 +47,7 @@ export class DeepSeekProvider implements AIProvider { }); return embedding; } catch (e) { - console.error('Erreur embeddings DeepSeek:', e); + console.error('Error generating embeddings (DeepSeek):', e); return []; } } @@ -58,8 +58,8 @@ export class DeepSeekProvider implements AIProvider { model: this.model, schema: z.object({ titles: z.array(z.object({ - title: z.string().describe('Le titre suggéré'), - confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1') + title: z.string().describe('Suggested title'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') })) }), prompt: prompt, @@ -67,7 +67,7 @@ export class DeepSeekProvider implements AIProvider { return object.titles; } catch (e) { - console.error('Erreur génération titres DeepSeek:', e); + console.error('Error generating titles (DeepSeek):', e); return []; } } @@ -81,7 +81,7 @@ export class DeepSeekProvider implements AIProvider { return text.trim(); } catch (e) { - console.error('Erreur génération texte DeepSeek:', e); + console.error('Error generating text (DeepSeek):', e); throw e; } } @@ -96,7 +96,7 @@ export class DeepSeekProvider implements AIProvider { return { text: text.trim() }; } catch (e) { - console.error('Erreur chat DeepSeek:', e); + console.error('Error in chat (DeepSeek):', e); throw e; } } diff --git a/memento-note/lib/ai/providers/ollama.ts b/memento-note/lib/ai/providers/ollama.ts index c2ecc64..4923879 100644 --- a/memento-note/lib/ai/providers/ollama.ts +++ b/memento-note/lib/ai/providers/ollama.ts @@ -75,7 +75,7 @@ Note content: "${content}"`; return JSON.parse(jsonMatch[0]); } - // Support pour le format { "tags": [...] } + // Support for { "tags": [...] } format const objectMatch = text.match(/\{\s*"tags"\s*:\s*(\[[\s\S]*\])\s*\}/); if (objectMatch && objectMatch[1]) { return JSON.parse(objectMatch[1]); @@ -83,7 +83,7 @@ Note content: "${content}"`; return []; } catch (e) { - console.error('Erreur API directe Ollama:', e); + console.error('Error in Ollama API:', e); return []; } } @@ -104,7 +104,7 @@ Note content: "${content}"`; const data = await response.json(); return data.embedding; } catch (e) { - console.error('Erreur embeddings directs Ollama:', e); + console.error('Error generating embeddings (Ollama):', e); return []; } } @@ -116,7 +116,7 @@ Note content: "${content}"`; headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: this.modelName, - prompt: `${prompt}\n\nRéponds UNIQUEMENT sous forme de tableau JSON : [{"title": "string", "confidence": number}]`, + prompt: `${prompt}\n\nRespond ONLY as a JSON array: [{"title": "string", "confidence": number}]`, stream: false, }), }); @@ -126,7 +126,7 @@ Note content: "${content}"`; const data = await response.json(); const text = data.response; - // Extraire le JSON de la réponse + // Extract JSON from response const jsonMatch = text.match(/\[\s*\{[\s\S]*\}\s*\]/); if (jsonMatch) { return JSON.parse(jsonMatch[0]); @@ -134,7 +134,7 @@ Note content: "${content}"`; return []; } catch (e) { - console.error('Erreur génération titres Ollama:', e); + console.error('Error generating titles (Ollama):', e); return []; } } @@ -156,7 +156,7 @@ Note content: "${content}"`; const data = await response.json(); return data.response.trim(); } catch (e) { - console.error('Erreur génération texte Ollama:', e); + console.error('Error generating text (Ollama):', e); throw e; } } @@ -187,7 +187,7 @@ Note content: "${content}"`; const data = await response.json(); return { text: data.message?.content?.trim() || '' }; } catch (e) { - console.error('Erreur chat Ollama:', e); + console.error('Error in chat (Ollama):', e); throw e; } } diff --git a/memento-note/lib/ai/providers/openai.ts b/memento-note/lib/ai/providers/openai.ts index 3bbd96b..4a4b447 100644 --- a/memento-note/lib/ai/providers/openai.ts +++ b/memento-note/lib/ai/providers/openai.ts @@ -24,17 +24,17 @@ export class OpenAIProvider implements AIProvider { model: this.model, schema: z.object({ tags: z.array(z.object({ - tag: z.string().describe('Le nom du tag, court et en minuscules'), - confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1') + tag: z.string().describe('Short tag name in lowercase'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') })) }), - prompt: `Analyse la note suivante et suggère entre 1 et 5 tags pertinents. - Contenu de la note: "${content}"`, + prompt: `Analyze the following note and suggest 1 to 5 relevant tags. + Note content: "${content}"`, }); return object.tags; } catch (e) { - console.error('Erreur génération tags OpenAI:', e); + console.error('Error generating tags (OpenAI):', e); return []; } } @@ -47,7 +47,7 @@ export class OpenAIProvider implements AIProvider { }); return embedding; } catch (e) { - console.error('Erreur embeddings OpenAI:', e); + console.error('Error generating embeddings (OpenAI):', e); return []; } } @@ -58,8 +58,8 @@ export class OpenAIProvider implements AIProvider { model: this.model, schema: z.object({ titles: z.array(z.object({ - title: z.string().describe('Le titre suggéré'), - confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1') + title: z.string().describe('Suggested title'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') })) }), prompt: prompt, @@ -67,7 +67,7 @@ export class OpenAIProvider implements AIProvider { return object.titles; } catch (e) { - console.error('Erreur génération titres OpenAI:', e); + console.error('Error generating titles (OpenAI):', e); return []; } } @@ -81,7 +81,7 @@ export class OpenAIProvider implements AIProvider { return text.trim(); } catch (e) { - console.error('Erreur génération texte OpenAI:', e); + console.error('Error generating text (OpenAI):', e); throw e; } } @@ -96,7 +96,7 @@ export class OpenAIProvider implements AIProvider { return { text: text.trim() }; } catch (e) { - console.error('Erreur chat OpenAI:', e); + console.error('Error in chat (OpenAI):', e); throw e; } } diff --git a/memento-note/lib/ai/providers/openrouter.ts b/memento-note/lib/ai/providers/openrouter.ts index 6018f52..f39616f 100644 --- a/memento-note/lib/ai/providers/openrouter.ts +++ b/memento-note/lib/ai/providers/openrouter.ts @@ -24,17 +24,17 @@ export class OpenRouterProvider implements AIProvider { model: this.model, schema: z.object({ tags: z.array(z.object({ - tag: z.string().describe('Le nom du tag, court et en minuscules'), - confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1') + tag: z.string().describe('Short tag name in lowercase'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') })) }), - prompt: `Analyse la note suivante et suggère entre 1 et 5 tags pertinents. - Contenu de la note: "${content}"`, + prompt: `Analyze the following note and suggest 1 to 5 relevant tags. + Note content: "${content}"`, }); return object.tags; } catch (e) { - console.error('Erreur génération tags OpenRouter:', e); + console.error('Error generating tags (OpenRouter):', e); return []; } } @@ -47,7 +47,7 @@ export class OpenRouterProvider implements AIProvider { }); return embedding; } catch (e) { - console.error('Erreur embeddings OpenRouter:', e); + console.error('Error generating embeddings (OpenRouter):', e); return []; } } @@ -58,8 +58,8 @@ export class OpenRouterProvider implements AIProvider { model: this.model, schema: z.object({ titles: z.array(z.object({ - title: z.string().describe('Le titre suggéré'), - confidence: z.number().min(0).max(1).describe('Le niveau de confiance entre 0 et 1') + title: z.string().describe('Suggested title'), + confidence: z.number().min(0).max(1).describe('Confidence level between 0 and 1') })) }), prompt: prompt, @@ -67,7 +67,7 @@ export class OpenRouterProvider implements AIProvider { return object.titles; } catch (e) { - console.error('Erreur génération titres OpenRouter:', e); + console.error('Error generating titles (OpenRouter):', e); return []; } } @@ -81,7 +81,7 @@ export class OpenRouterProvider implements AIProvider { return text.trim(); } catch (e) { - console.error('Erreur génération texte OpenRouter:', e); + console.error('Error generating text (OpenRouter):', e); throw e; } } @@ -96,7 +96,7 @@ export class OpenRouterProvider implements AIProvider { return { text: text.trim() }; } catch (e) { - console.error('Erreur chat OpenRouter:', e); + console.error('Error in chat (OpenRouter):', e); throw e; } } diff --git a/memento-note/lib/ai/types.ts b/memento-note/lib/ai/types.ts index 66f9e15..6b1030c 100644 --- a/memento-note/lib/ai/types.ts +++ b/memento-note/lib/ai/types.ts @@ -29,32 +29,32 @@ export interface ToolCallResult { export interface AIProvider { /** - * Analyse le contenu et suggère des tags pertinents. + * Analyze content and suggest relevant tags. */ generateTags(content: string, language?: string): Promise; /** - * Génère un vecteur d'embeddings pour la recherche sémantique. + * Generate an embedding vector for semantic search. */ getEmbeddings(text: string): Promise; /** - * Génère des suggestions de titres basées sur le contenu. + * Generate title suggestions based on content. */ generateTitles(prompt: string): Promise; /** - * Génère du texte basé sur un prompt. + * Generate text based on a prompt. */ generateText(prompt: string): Promise; /** - * Fournit une réponse de chat (utilisé pour le système agentique) + * Provide a chat response (used for the agentic system) */ chat(messages: any[], systemPrompt?: string): Promise; /** - * Retourne le modèle AI SDK pour le streaming direct (utilisé par l'API route) + * Return the AI SDK model for direct streaming (used by API route) */ getModel(): any; @@ -69,7 +69,7 @@ export type AIProviderType = 'openai' | 'ollama'; export interface AIConfig { provider: AIProviderType; apiKey?: string; - baseUrl?: string; // Utile pour Ollama + baseUrl?: string; // Used for Ollama model?: string; embeddingModel?: string; } diff --git a/memento-note/scripts/reset-password-auto.ts b/memento-note/scripts/reset-password-auto.ts index 18059ce..bde20de 100644 --- a/memento-note/scripts/reset-password-auto.ts +++ b/memento-note/scripts/reset-password-auto.ts @@ -3,8 +3,19 @@ import { prisma } from '../lib/prisma' import bcrypt from 'bcryptjs' async function main() { - const email = 'test@example.com' - const newPassword = 'password123' + const email = process.argv[2] + const newPassword = process.argv[3] + + if (!email || !newPassword) { + console.error('Usage: npx tsx scripts/reset-password-auto.ts ') + console.error('Example: npx tsx scripts/reset-password-auto.ts user@example.com mynewpassword') + process.exit(1) + } + + if (newPassword.length < 6) { + console.error('Password must be at least 6 characters') + process.exit(1) + } console.log(`Resetting password for ${email}...`) @@ -19,9 +30,9 @@ async function main() { resetTokenExpiry: null }, }) - console.log(`✅ Password successfully reset for ${user.email}`) + console.log(`Password successfully reset for ${user.email}`) } catch (error) { - console.error('❌ Error resetting password:', error) + console.error('Error resetting password:', error) process.exit(1) } } diff --git a/memento-note/scripts/reset-password.js b/memento-note/scripts/reset-password.js index a9acccc..2078b9d 100644 --- a/memento-note/scripts/reset-password.js +++ b/memento-note/scripts/reset-password.js @@ -1,25 +1,12 @@ /** - * Script DIRECT de reset de mot de passe - * - * POURQUOI CE SCRIPT ? - * ----------------- - * - Le compte `test@example.com` N'EXISTE PAS (vous avez raison !) - * - L'envoi d'email nécessite une configuration SMTP complexe - * - VOUS VOULEZ UNE SOLUTION DIRECTE, SANS PERDRE DE TEMPS - * - * CE QUE FAIT CE SCRIPT : - * ------------------- - * - Réinitialise DIRECTEMENT le mot de passe d'un compte existant - * - Sans avoir besoin d'email - * - Sans avoir besoin d'interface graphique - * - * COMMENT UTILISER : - * --------------- - * 1. Ouvrez un terminal dans le dossier memento-note - * 2. Exécutez: node scripts/reset-password.js - * 3. Quand demandé, entrez l'email du compte à réinitialiser - * 4. Entrez le nouveau mot de passe (2 fois pour confirmation) - * 5. FINI ! Connectez-vous avec le nouveau mot de passe + * Interactive password reset script + * + * USAGE: + * 1. Open a terminal in the memento-note directory + * 2. Run: node scripts/reset-password.js + * 3. Enter the email of the account to reset + * 4. Enter the new password (twice for confirmation) + * 5. Done! Log in with the new password */ const readline = require('readline'); @@ -31,25 +18,25 @@ const rl = readline.createInterface({ output: process.stdout }); -console.log('╔══════════════════════════════════════════════════════════════════╗'); -console.log('║ RESET DE MOT DE PASSE DIRECT ║'); -console.log('║ ║'); -console.log('║ ⚠️ ATTENTION : Utilisez seulement pour VOTRE propre compte ! ║'); -console.log('║ ║'); -console.log('╚══════════════════════════════════════════════════════════════════════╝'); +console.log('================================================================'); +console.log(' INTERACTIVE PASSWORD RESET'); +console.log(''); +console.log(' WARNING: Use only for your own account!'); +console.log(''); +console.log('================================================================'); console.log(''); -rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => { +rl.question('Enter the EMAIL of the account to reset: ', async (email) => { if (!email || !email.includes('@')) { - console.log('❌ Erreur : Email invalide !'); + console.log('Error: Invalid email!'); rl.close(); process.exit(1); } email = email.toLowerCase().trim(); - - console.log(`🔍 Recherche du compte : ${email}...`); - + + console.log(`Looking up account: ${email}...`); + try { const user = await prisma.user.findUnique({ where: { email: email } @@ -57,56 +44,55 @@ rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => { if (!user) { console.log(''); - console.log('❌ ERREUR : AUCUN compte trouvé avec cet email !'); + console.log('ERROR: No account found with this email!'); console.log(''); - console.log('📋 COMPTES DISPONIBLES (si existants) :'); - console.log('─────────────────────────────────────────'); - - // Afficher tous les utilisateurs de la base de données + console.log('AVAILABLE ACCOUNTS (if any):'); + console.log('-------------------------'); + const allUsers = await prisma.user.findMany({ select: { email: true, name: true, role: true, createdAt: true }, orderBy: { createdAt: 'desc' } }); - + if (allUsers.length > 0) { console.log(''); allUsers.forEach((u, index) => { - console.log(`${index + 1}. 📧 Email: ${u.email}`); - console.log(` 👤 Nom: ${u.name || 'N/A'}`); - console.log(` 🏷️ Rôle: ${u.role}`); - console.log(` 📅 Créé: ${u.createdAt.toLocaleString('fr-FR')}`); + console.log(`${index + 1}. Email: ${u.email}`); + console.log(` Name: ${u.name || 'N/A'}`); + console.log(` Role: ${u.role}`); + console.log(` Created: ${u.createdAt.toLocaleString()}`); console.log(''); }); } else { - console.log(' (Aucun compte dans la base de données)'); + console.log(' (No accounts in the database)'); } - - console.log('─────────────────────────────────────────'); + + console.log('-------------------------'); console.log(''); rl.close(); process.exit(1); } - console.log(`✅ Compte trouvé : ${user.email} (${user.name})`); + console.log(`Account found: ${user.email} (${user.name})`); console.log(''); - rl.question('Entrez le NOUVEAU mot de passe (minimum 6 caractères) : ', async (newPassword) => { + rl.question('Enter the NEW password (minimum 6 characters): ', async (newPassword) => { if (!newPassword || newPassword.length < 6) { - console.log('❌ Erreur : Le mot de passe doit avoir au moins 6 caractères !'); + console.log('Error: Password must be at least 6 characters!'); rl.close(); process.exit(1); } - rl.question('Confirmez le nouveau mot de passe : ', async (confirmPassword) => { + rl.question('Confirm the new password: ', async (confirmPassword) => { if (newPassword !== confirmPassword) { - console.log('❌ Erreur : Les mots de passe ne correspondent pas !'); + console.log('Error: Passwords do not match!'); rl.close(); process.exit(1); } console.log(''); - console.log('🔄 Réinitialisation du mot de passe en cours...'); - + console.log('Resetting password...'); + const hashedPassword = await bcrypt.hash(newPassword, 10); await prisma.user.update({ @@ -118,17 +104,15 @@ rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => { } }); - console.log('✅ SUCCÈS ! Le mot de passe a été réinitialisé !'); + console.log('SUCCESS! Password has been reset!'); console.log(''); - console.log('═══════════════════════════════════════════════════════════════════════'); - console.log('🎉 VOUS POUVEZ MAINTENANT VOUS CONNECTER !'); - console.log('═════════════════════════════════════════════════════════════════════'); + console.log('================================================================'); + console.log('YOU CAN NOW LOG IN!'); + console.log('================================================================'); console.log(''); - console.log('📱 URL de connexion : http://localhost:3000/login'); - console.log('📧 Email :', email); - console.log('🔑 Mot de passe :', newPassword); - console.log(''); - console.log('⏩ Copiez ces informations et connectez-vous !'); + console.log('Login URL: http://localhost:3000/login'); + console.log('Email:', email); + console.log('Password:', newPassword); console.log(''); rl.close(); @@ -137,7 +121,7 @@ rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => { }); } catch (error) { console.log(''); - console.log('❌ ERREUR lors de la réinitialisation :'); + console.log('ERROR during password reset:'); console.error(error); rl.close(); process.exit(1);