docs: add complete guide, env files, fix docker-compose

- Add GUIDE.md: complete user documentation covering installation,
  Docker deployment, AI providers, MCP server, N8N integration,
  email config, admin panel, env var reference, troubleshooting
- Add mcp-server/.env.example with all MCP-specific variables
- Update .env.docker.example with all 42 environment variables
- Fix docker-compose.yml: parameterize PostgreSQL credentials,
  add missing env vars (CUSTOM_OPENAI, AI_PROVIDER_CHAT,
  ALLOW_REGISTRATION, RESEND_API_KEY)
- Track memento-note/.env.example
This commit is contained in:
Sepehr Ramezani
2026-04-20 22:57:09 +02:00
parent e4d4e23dc7
commit 5b7cbcbc49
23 changed files with 1054 additions and 996 deletions

View File

@@ -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_..."

725
GUIDE.md Normal file
View File

@@ -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.

View File

@@ -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:

50
mcp-server/.env.example Normal file
View File

@@ -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=""

64
memento-note/.env.example Normal file
View File

@@ -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=""

View File

@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel

View File

@@ -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 (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">{t('aiSettings.title')}</h1>
<p className="text-gray-600 dark:text-gray-400">
{t('aiSettings.description')}
</p>
</div>
{/* AI Provider */}
<SettingsSection
title={t('aiSettings.provider')}
icon={<span className="text-2xl">🤖</span>}
description={t('aiSettings.providerDesc')}
>
<SettingSelect
label={t('aiSettings.provider')}
description={t('aiSettings.providerDesc')}
value={settings.aiProvider}
options={[
{
value: 'auto',
label: t('aiSettings.providerAuto'),
description: t('aiSettings.providerAutoDesc')
},
{
value: 'ollama',
label: t('aiSettings.providerOllama'),
description: t('aiSettings.providerOllamaDesc')
},
{
value: 'openai',
label: t('aiSettings.providerOpenAI'),
description: t('aiSettings.providerOpenAIDesc')
},
]}
onChange={handleProviderChange}
/>
{settings.aiProvider === 'openai' && (
<SettingInput
label={t('admin.ai.apiKey')}
description={t('admin.ai.openAIKeyDescription')}
value={apiKey}
type="password"
placeholder="sk-..."
onChange={handleApiKeyChange}
/>
)}
</SettingsSection>
{/* Feature Toggles */}
<SettingsSection
title={t('aiSettings.features')}
icon={<span className="text-2xl"></span>}
description={t('aiSettings.description')}
>
<SettingToggle
label={t('titleSuggestions.available').replace('💡 ', '')}
description={t('aiSettings.titleSuggestionsDesc')}
checked={settings.titleSuggestions}
onChange={(checked) => handleToggle('titleSuggestions', checked)}
/>
<SettingToggle
label={t('semanticSearch.exactMatch')}
description={t('semanticSearch.searching')}
checked={settings.semanticSearch}
onChange={(checked) => handleToggle('semanticSearch', checked)}
/>
<SettingToggle
label={t('paragraphRefactor.title')}
description={t('aiSettings.paragraphRefactorDesc')}
checked={settings.paragraphRefactor}
onChange={(checked) => handleToggle('paragraphRefactor', checked)}
/>
<SettingToggle
label={t('memoryEcho.title')}
description={t('memoryEcho.dailyInsight')}
checked={settings.memoryEcho}
onChange={(checked) => handleToggle('memoryEcho', checked)}
/>
{settings.memoryEcho && (
<SettingSelect
label={t('aiSettings.frequency')}
description={t('aiSettings.frequencyDesc')}
value={settings.memoryEchoFrequency}
options={[
{ value: 'daily', label: t('aiSettings.frequencyDaily') },
{ value: 'weekly', label: t('aiSettings.frequencyWeekly') },
{ value: 'custom', label: 'Custom' },
]}
onChange={handleFrequencyChange}
/>
)}
</SettingsSection>
{/* Demo Mode */}
<SettingsSection
title={t('demoMode.title')}
icon={<span className="text-2xl">🎭</span>}
description={t('demoMode.description')}
>
<SettingToggle
label={t('demoMode.title')}
description={t('demoMode.description')}
checked={settings.demoMode}
onChange={(checked) => handleToggle('demoMode', checked)}
/>
</SettingsSection>
</main>
</div>
</div>
)
}

View File

@@ -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 (
<div className="container mx-auto py-10 px-4 max-w-6xl">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Navigation */}
<aside className="lg:col-span-1">
<SettingsNav />
</aside>
{/* Main Content */}
<main className="lg:col-span-3 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">{t('profile.title')}</h1>
<p className="text-gray-600 dark:text-gray-400">
{t('profile.description')}
</p>
</div>
{/* Profile Information */}
<SettingsSection
title={t('profile.accountSettings')}
icon={<span className="text-2xl">👤</span>}
description={t('profile.description')}
>
<SettingInput
label={t('profile.displayName')}
description={t('profile.displayName')}
value={user.name}
onChange={handleNameChange}
placeholder={t('auth.namePlaceholder')}
/>
<SettingInput
label={t('profile.email')}
description={t('profile.email')}
value={user.email}
type="email"
onChange={handleEmailChange}
placeholder={t('auth.emailPlaceholder')}
/>
</SettingsSection>
{/* Preferences */}
<SettingsSection
title={t('settings.language')}
icon={<span className="text-2xl"></span>}
description={t('profile.languagePreferencesDescription')}
>
<SettingSelect
label={t('profile.preferredLanguage')}
description={t('profile.languageDescription')}
value={language}
options={[
{ value: 'auto', label: t('profile.autoDetect') },
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' },
{ value: 'es', label: 'Español' },
{ value: 'de', label: 'Deutsch' },
{ value: 'fa', label: 'فارسی' },
{ value: 'it', label: 'Italiano' },
{ value: 'pt', label: 'Português' },
{ value: 'ru', label: 'Русский' },
{ value: 'zh', label: '中文' },
{ value: 'ja', label: '日本語' },
{ value: 'ko', label: '한국어' },
{ value: 'ar', label: 'العربية' },
{ value: 'hi', label: 'हिन्दी' },
{ value: 'nl', label: 'Nederlands' },
{ value: 'pl', label: 'Polski' },
]}
onChange={handleLanguageChange}
/>
<SettingToggle
label={t('profile.showRecentNotes')}
description={t('profile.showRecentNotesDescription')}
checked={showRecentNotes}
onChange={handleRecentNotesChange}
/>
</SettingsSection>
{/* AI Settings Link */}
<div className="p-6 border rounded-lg bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950 dark:to-pink-950">
<div className="flex items-center gap-4">
<div className="text-4xl"></div>
<div className="flex-1">
<h3 className="font-semibold text-lg mb-1">{t('aiSettings.title')}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{t('aiSettings.description')}
</p>
</div>
<button
onClick={() => window.location.href = '/settings/ai'}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
{t('nav.configureAI')}
</button>
</div>
</div>
</main>
</div>
</div>
)
}

View File

@@ -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",

View File

@@ -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' }
}
}

View File

@@ -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' }
}
}

View File

@@ -5,7 +5,7 @@ export const authConfig = {
signIn: '/login',
newUser: '/register',
},
secret: "csQFtfYvQ8YtatEYSUFyslXdk2vJhZFt9D5gav/RJQg=",
secret: process.env.NEXTAUTH_SECRET,
trustHost: true,
session: {
strategy: 'jwt',

View File

@@ -113,25 +113,25 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps)
const [comparisonNotes, setComparisonNotes] = useState<Array<Partial<Note>>>([])
const [fusionNotes, setFusionNotes] = useState<Array<Partial<Note>>>([])
// Tags rejetés par l'utilisateur pour cette session
// Tags dismissed by the user for this session
const [dismissedTags, setDismissedTags] = useState<string[]>([])
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()

View File

@@ -15,14 +15,14 @@ export function useAutoTagging({ content, notebookId, enabled = true }: UseAutoT
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [error, setError] = useState<string | null>(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<string | null | undefined>(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) {

View File

@@ -17,7 +17,7 @@ export function useTitleSuggestions({ content, enabled = true }: UseTitleSuggest
const [isAnalyzing, setIsAnalyzing] = useState(false)
const [error, setError] = useState<string | null>(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)
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<TagSuggestion[]>;
/**
* Génère un vecteur d'embeddings pour la recherche sémantique.
* Generate an embedding vector for semantic search.
*/
getEmbeddings(text: string): Promise<number[]>;
/**
* Génère des suggestions de titres basées sur le contenu.
* Generate title suggestions based on content.
*/
generateTitles(prompt: string): Promise<TitleSuggestion[]>;
/**
* Génère du texte basé sur un prompt.
* Generate text based on a prompt.
*/
generateText(prompt: string): Promise<string>;
/**
* 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<any>;
/**
* 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;
}

View File

@@ -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 <email> <new-password>')
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)
}
}

View File

@@ -1,25 +1,12 @@
/**
* Script DIRECT de reset de mot de passe
* Interactive password reset script
*
* 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
* 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,24 +18,24 @@ 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({
@@ -57,12 +44,11 @@ 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('─────────────────────────────────────────');
console.log('AVAILABLE ACCOUNTS (if any):');
console.log('-------------------------');
// Afficher tous les utilisateurs de la base de données
const allUsers = await prisma.user.findMany({
select: { email: true, name: true, role: true, createdAt: true },
orderBy: { createdAt: 'desc' }
@@ -71,41 +57,41 @@ rl.question('Entrez l\'EMAIL du compte à réinitialiser : ', async (email) => {
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);
@@ -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);