feat: add web search test button in admin tools, update Resend docs
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 30s
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 30s
Made-with: Cursor
This commit is contained in:
@@ -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 <onboarding@resend.dev>`.
|
||||
### 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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
// 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<AIProvider>((config.AI_PROVIDER_TAGS as AIProvider) || 'ollama')
|
||||
@@ -308,6 +310,28 @@ export function AdminSettingsForm({ config }: { config: Record<string, string> }
|
||||
}
|
||||
}
|
||||
|
||||
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<string, string> }
|
||||
<Input id="JINA_API_KEY" name="JINA_API_KEY" type="password" defaultValue={config.JINA_API_KEY || ''} placeholder={t('admin.tools.jinaKeyOptional')} />
|
||||
<p className="text-xs text-muted-foreground">{t('admin.tools.jinaKeyDescription')}</p>
|
||||
</div>
|
||||
|
||||
{/* Résultat du test */}
|
||||
{searchTestResult && (
|
||||
<div className={`rounded-lg border p-3 text-sm flex items-start gap-2 ${searchTestResult.success ? 'border-green-200 bg-green-50 text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300' : 'border-red-200 bg-red-50 text-red-800 dark:border-red-800 dark:bg-red-950/30 dark:text-red-300'}`}>
|
||||
<span className={`mt-0.5 inline-block w-2 h-2 rounded-full flex-shrink-0 ${searchTestResult.success ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
<span>{searchTestResult.message}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button type="submit" disabled={isSaving}>{t('admin.tools.saveSettings')}</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleTestSearch}
|
||||
disabled={isTestingSearch}
|
||||
>
|
||||
{isTestingSearch ? 'Test en cours…' : 'Tester la recherche web'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
56
memento-note/app/api/admin/test-search/route.ts
Normal file
56
memento-note/app/api/admin/test-search/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user