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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
}
|
||||
@@ -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' }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user