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

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
*
* 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);