diff --git a/GITEA-ACTIONS.md b/GITEA-ACTIONS.md index 05ccb0c..a00825e 100644 --- a/GITEA-ACTIONS.md +++ b/GITEA-ACTIONS.md @@ -85,17 +85,50 @@ Aller sur : **`Momento → Settings → Actions → Secrets`** ## Email avec Resend -### Mode test (sans domaine vérifié) -- Resend ne permet d'envoyer qu'**à l'adresse du compte Resend** (`devparsa75@gmail.com`). -- L'adresse `from` utilisée automatiquement : `Memento `. +### Comment ça fonctionne -### Mode production (domaine vérifié) -1. Vérifier un domaine sur [resend.com/domains](https://resend.com/domains) -2. Configurer `SMTP_FROM` (variable Gitea) avec une adresse de ce domaine : - ``` - SMTP_FROM = noreply@parsanet.org - ``` -3. Resend utilisera cette adresse et pourra envoyer à n'importe qui. +Resend est un service d'envoi d'emails transactionnels (reset de mot de passe, notifications...). + +### Étape 1 — Créer un compte Resend +1. Aller sur [resend.com](https://resend.com) → **Sign up** +2. Copier la clé API : `re_xxxxxxxxxxxx` +3. Ajouter comme secret Gitea : `RESEND_API_KEY = re_xxxxxxxxxxxx` + +### Étape 2 — Vérifier votre domaine (obligatoire pour envoyer à tout le monde) + +> **Sans domaine vérifié** : Resend ne peut envoyer qu'**à l'adresse email du compte Resend**. +> Cela bloque les emails de reset de mot de passe pour les autres utilisateurs. + +1. Aller sur [resend.com/domains](https://resend.com/domains) → **Add Domain** +2. Entrer votre domaine, ex: `mondomaine.com` +3. Resend affiche des enregistrements DNS à ajouter chez votre registrar/Cloudflare : + +``` +Type Nom Valeur +TXT _dmarc.mondomaine.com v=DMARC1; p=none; +TXT mondomaine.com v=spf1 include:amazonses.com ~all +CNAME resend._domainkey resend._domainkey.mondomaine.com → (valeur fournie par Resend) +MX bounce.mondomaine.com feedback-smtp.us-east-1.amazonses.com +``` + +4. Attendre 1-5 minutes → le statut passe à **"Verified"** ✅ + +### Étape 3 — Configurer l'adresse expéditeur + +Dans les variables Gitea : +``` +SMTP_FROM = noreply@mondomaine.com +``` + +Ou dans l'interface admin : **Admin → Settings → Configuration Email → Resend** + +### Résumé + +| Situation | Comportement | +|-----------|-------------| +| Pas de domaine vérifié | Envoi uniquement vers l'email du compte Resend | +| Domaine vérifié + `SMTP_FROM` configuré | Envoi vers n'importe qui ✅ | +| `SMTP_FROM` vide | Utilise `noreply@mondomaine.com` si domaine vérifié, sinon `onboarding@resend.dev` | --- diff --git a/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx b/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx index f8348c1..cb5f0fb 100644 --- a/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx +++ b/memento-note/app/(admin)/admin/settings/admin-settings-form.tsx @@ -42,6 +42,8 @@ export function AdminSettingsForm({ config }: { config: Record } // Email provider state const [emailProvider, setEmailProvider] = useState<'resend' | 'smtp'>(config.EMAIL_PROVIDER as 'resend' | 'smtp' || (config.RESEND_API_KEY ? 'resend' : 'smtp')) const [emailTestResult, setEmailTestResult] = useState<{ provider: 'resend' | 'smtp'; success: boolean; message?: string } | null>(null) + const [isTestingSearch, setIsTestingSearch] = useState(false) + const [searchTestResult, setSearchTestResult] = useState<{ success: boolean; message: string } | null>(null) // AI Provider state - separated for tags, embeddings, and chat const [tagsProvider, setTagsProvider] = useState((config.AI_PROVIDER_TAGS as AIProvider) || 'ollama') @@ -308,6 +310,28 @@ export function AdminSettingsForm({ config }: { config: Record } } } + const handleTestSearch = async () => { + setIsTestingSearch(true) + setSearchTestResult(null) + try { + const url = (document.getElementById('SEARXNG_URL') as HTMLInputElement)?.value + || config.SEARXNG_URL || 'http://localhost:8080' + const apiKey = (document.getElementById('BRAVE_SEARCH_API_KEY') as HTMLInputElement)?.value + || config.BRAVE_SEARCH_API_KEY || '' + const res = await fetch('/api/admin/test-search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ provider: webSearchProvider, searxngUrl: url, braveApiKey: apiKey }), + }) + const data = await res.json() + setSearchTestResult(data) + } catch (e: any) { + setSearchTestResult({ success: false, message: e.message }) + } finally { + setIsTestingSearch(false) + } + } + const handleSaveTools = async (formData: FormData) => { setIsSaving(true) const data = { @@ -1064,9 +1088,25 @@ export function AdminSettingsForm({ config }: { config: Record }

{t('admin.tools.jinaKeyDescription')}

+ + {/* Résultat du test */} + {searchTestResult && ( +
+ + {searchTestResult.message} +
+ )} - + + diff --git a/memento-note/app/api/admin/test-search/route.ts b/memento-note/app/api/admin/test-search/route.ts new file mode 100644 index 0000000..7057c6f --- /dev/null +++ b/memento-note/app/api/admin/test-search/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth' + +export async function POST(request: NextRequest) { + const session = await auth() + if (!session?.user?.id || (session.user as any).role !== 'ADMIN') { + return NextResponse.json({ success: false, message: 'Unauthorized' }, { status: 401 }) + } + + const { provider, searxngUrl, braveApiKey } = await request.json() + + try { + if (provider === 'brave' || provider === 'both') { + if (!braveApiKey) { + if (provider === 'brave') { + return NextResponse.json({ success: false, message: 'Clé API Brave manquante.' }) + } + } else { + const res = await fetch( + 'https://api.search.brave.com/res/v1/web/search?q=test&count=1', + { headers: { 'Accept': 'application/json', 'X-Subscription-Token': braveApiKey } } + ) + if (!res.ok) { + return NextResponse.json({ success: false, message: `Brave Search — erreur ${res.status}: clé API invalide ou expirée.` }) + } + if (provider === 'brave') { + return NextResponse.json({ success: true, message: 'Brave Search fonctionne correctement.' }) + } + } + } + + // Test SearXNG + const url = (searxngUrl || 'http://localhost:8080').replace(/\/+$/, '') + const res = await fetch(`${url}/search?q=test&format=json`, { + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(5000), + }) + + if (!res.ok) { + return NextResponse.json({ success: false, message: `SearXNG — erreur HTTP ${res.status}. Vérifiez l'URL et que le serveur est démarré.` }) + } + + const data = await res.json() + const count = data.results?.length ?? 0 + + return NextResponse.json({ + success: true, + message: `SearXNG fonctionne correctement — ${count} résultat(s) retourné(s) pour la requête de test.`, + }) + } catch (e: any) { + const msg = e.name === 'TimeoutError' + ? 'Timeout — SearXNG ne répond pas dans les 5 secondes. Vérifiez l\'URL et que le serveur est accessible.' + : `Erreur de connexion : ${e.message}` + return NextResponse.json({ success: false, message: msg }) + } +}