mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Feature: Simplification admin Google Calendar et assistant IA
- Événements toute la journée affichés correctement (au lieu de 1:00) - Édition des événements avec synchronisation Google Calendar - Configuration Google Calendar simplifiée (OAuth 2.0 uniquement) - Ajout du champ app_url pour éviter URLs codées en dur - Assistant IA simplifié (sélecteur unique au lieu de recherche + liste) - Remplacement URLs hardcodées par configuration app_url
This commit is contained in:
2 files changed
+92
-250
No files matched your search
@@ -33,14 +33,19 @@ async function getAuthType() {
|
||||
async function getOAuth2Client(customRedirectUri = null) {
|
||||
const clientId = await getOne("SELECT value FROM settings WHERE key = 'google_client_id'");
|
||||
const clientSecret = await getOne("SELECT value FROM settings WHERE key = 'google_client_secret'");
|
||||
const appUrl = await getOne("SELECT value FROM settings WHERE key = 'app_url'");
|
||||
|
||||
if (!clientId || !clientId.value || !clientSecret || !clientSecret.value) {
|
||||
throw new Error('Client ID et Client Secret non configurés');
|
||||
}
|
||||
|
||||
// Utiliser l'URI personnalisée si fournie, sinon utiliser l'URL de production
|
||||
// Utiliser l'URI personnalisée si fournie, sinon utiliser l'URL du site configurée
|
||||
const redirectUri = customRedirectUri ||
|
||||
'https://note.ffnancy.fr/api/calendar/oauth-callback';
|
||||
(appUrl?.value ? `${appUrl.value}/api/calendar/oauth-callback` : null);
|
||||
|
||||
if (!redirectUri) {
|
||||
throw new Error('URL du site non configurée. Veuillez configurer l\'URL dans les paramètres.');
|
||||
}
|
||||
|
||||
return new google.auth.OAuth2(
|
||||
clientId.value,
|
||||
|
||||
+85
-248
@@ -98,7 +98,6 @@ const Index = () => {
|
||||
const [chatLoading, setChatLoading] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<string>("");
|
||||
const [defaultModel, setDefaultModel] = useState<string>("");
|
||||
const [modelSearchQuery, setModelSearchQuery] = useState<string>("");
|
||||
|
||||
// Debounce timer for auto-save
|
||||
const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -1711,182 +1710,63 @@ const Index = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-5 w-5" />
|
||||
Configuration Google Calendar
|
||||
Configuration Google Calendar OAuth 2.0
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Choix du type d'authentification */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-auth-type">Méthode d'authentification</Label>
|
||||
<Select
|
||||
value={settings.google_auth_type || 'oauth2'}
|
||||
onValueChange={(value) => setSettings({ ...settings, google_auth_type: value as 'oauth2' | 'service_account' | 'api_externe' })}
|
||||
>
|
||||
<SelectTrigger id="google-auth-type">
|
||||
<SelectValue placeholder="Sélectionnez une méthode" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="oauth2">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">OAuth 2.0</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Calendrier personnel avec consentement utilisateur
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="service_account">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Service Account</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Clé JSON pour calendriers partagés (sans interaction)
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="api_externe">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">API externe</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Connectez-vous à une API externe pour Google Calendar
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>OAuth 2.0</strong> : Pour accéder au calendrier personnel avec autorisation de l'utilisateur.<br/>
|
||||
<strong>Service Account</strong> : Pour accéder à des calendriers partagés sans interaction utilisateur.<br/>
|
||||
<strong>API externe</strong> : Utilisez votre propre API backend pour gérer la connexion à Google Calendar.
|
||||
<Label htmlFor="app-url">URL du site</Label>
|
||||
<Input
|
||||
id="app-url"
|
||||
type="url"
|
||||
placeholder="https://votre-domaine.com"
|
||||
value={settings.app_url || ''}
|
||||
onChange={(e) => setSettings({ ...settings, app_url: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
URL de votre site (utilisée pour les redirections OAuth)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Configuration OAuth2 */}
|
||||
{(!settings.google_auth_type || settings.google_auth_type === 'oauth2') && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-client-id">Client ID Google</Label>
|
||||
<Input
|
||||
id="google-client-id"
|
||||
type="text"
|
||||
placeholder="123456789-abcdef.apps.googleusercontent.com"
|
||||
value={settings.google_client_id || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-client-id">Client ID Google</Label>
|
||||
<Input
|
||||
id="google-client-id"
|
||||
type="text"
|
||||
placeholder="123456789-abcdef.apps.googleusercontent.com"
|
||||
value={settings.google_client_id || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-client-secret">Client Secret Google</Label>
|
||||
<Input
|
||||
id="google-client-secret"
|
||||
type="password"
|
||||
placeholder="GOCSPX-..."
|
||||
value={settings.google_client_secret || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Créez un projet OAuth 2.0 sur{' '}
|
||||
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||
Google Cloud Console
|
||||
</a>
|
||||
{' '}et activez l'API Google Calendar.
|
||||
</p>
|
||||
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200 mb-1">
|
||||
⚠️ IMPORTANT : URI de redirection autorisée
|
||||
</p>
|
||||
<p className="text-xs text-yellow-800 dark:text-yellow-300 mb-2">
|
||||
Ajoutez cette URL exacte dans la console Google Cloud (OAuth 2.0 Client IDs → URIs de redirection autorisées) :
|
||||
</p>
|
||||
<code className="block p-2 bg-white dark:bg-gray-800 border border-yellow-300 dark:border-yellow-700 rounded text-xs font-mono break-all">
|
||||
{window.location.origin}/api/calendar/oauth-callback
|
||||
</code>
|
||||
<p className="text-xs text-yellow-800 dark:text-yellow-300 mt-2">
|
||||
💡 Si vous utilisez un domaine différent, créez un fichier .env avec APP_URL=https://votre-domaine.com
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Configuration Service Account */}
|
||||
{settings.google_auth_type === 'service_account' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-service-account-key">Clé Service Account (JSON)</Label>
|
||||
<textarea
|
||||
id="google-service-account-key"
|
||||
className="w-full min-h-[200px] p-3 text-xs font-mono border rounded-md"
|
||||
placeholder='Collez ici le contenu du fichier JSON de votre Service Account { "type": "service_account", "project_id": "...", "private_key_id": "...", ... }'
|
||||
value={settings.google_service_account_key || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_service_account_key: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Créez un Service Account sur{' '}
|
||||
<a href="https://console.cloud.google.com/iam-admin/serviceaccounts" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||
Google Cloud Console
|
||||
</a>
|
||||
{' '}et téléchargez la clé JSON.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-calendar-email">Email du calendrier à synchroniser</Label>
|
||||
<Input
|
||||
id="google-calendar-email"
|
||||
type="email"
|
||||
placeholder="votre-email@gmail.com"
|
||||
value={settings.google_calendar_email || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_calendar_email: e.target.value })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
⚠️ Important : Vous devez partager votre calendrier avec l'email du Service Account (visible dans le JSON)
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Configuration API Key */}
|
||||
{settings.google_auth_type === 'api_externe' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-api-key">Clé API Google Calendar</Label>
|
||||
<Input
|
||||
id="google-api-key"
|
||||
type="password"
|
||||
placeholder="AIzaSy..."
|
||||
value={settings.google_calendar_api_key || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_calendar_api_key: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Entrez votre clé API Google Calendar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-calendar-id-api">ID du calendrier</Label>
|
||||
<Input
|
||||
id="google-calendar-id-api"
|
||||
type="text"
|
||||
placeholder="primary ou votre-email@gmail.com"
|
||||
value={settings.google_calendar_id || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_calendar_id: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Utilisez "primary" pour votre calendrier principal ou l'email du calendrier spécifique.
|
||||
</p>
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<p className="text-sm font-semibold text-blue-900 dark:text-blue-200 mb-1">
|
||||
ℹ️ Obtenir une clé API
|
||||
</p>
|
||||
<p className="text-xs text-blue-800 dark:text-blue-300 mb-2">
|
||||
1. Allez sur <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="underline">Google Cloud Console</a><br/>
|
||||
2. Créez une clé API<br/>
|
||||
3. Activez l'API Google Calendar<br/>
|
||||
4. Copiez la clé API ici
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="google-client-secret">Client Secret Google</Label>
|
||||
<Input
|
||||
id="google-client-secret"
|
||||
type="password"
|
||||
placeholder="GOCSPX-..."
|
||||
value={settings.google_client_secret || ''}
|
||||
onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Créez un projet OAuth 2.0 sur{' '}
|
||||
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
||||
Google Cloud Console
|
||||
</a>
|
||||
{' '}et activez l'API Google Calendar.
|
||||
</p>
|
||||
<div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<p className="text-sm font-semibold text-yellow-900 dark:text-yellow-200 mb-1">
|
||||
⚠️ IMPORTANT : URI de redirection autorisée
|
||||
</p>
|
||||
<p className="text-xs text-yellow-800 dark:text-yellow-300 mb-2">
|
||||
Ajoutez cette URL exacte dans la console Google Cloud (OAuth 2.0 Client IDs → URIs de redirection autorisées) :
|
||||
</p>
|
||||
<code className="block p-2 bg-white dark:bg-gray-800 border border-yellow-300 dark:border-yellow-700 rounded text-xs font-mono break-all">
|
||||
{settings.app_url || window.location.origin}/api/calendar/oauth-callback
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
||||
@@ -1895,22 +1775,14 @@ const Index = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(
|
||||
((!settings.google_auth_type || settings.google_auth_type === 'oauth2') && settings.google_client_id && settings.google_client_secret) ||
|
||||
(settings.google_auth_type === 'service_account' && settings.google_service_account_key) ||
|
||||
(settings.google_auth_type === 'api_externe' && settings.google_calendar_api_key)
|
||||
) && (
|
||||
{settings.google_client_id && settings.google_client_secret && (
|
||||
<>
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="font-semibold mb-2">Statut de connexion</h3>
|
||||
{calendarAuthStatus.isAuthenticated && !calendarAuthStatus.isExpired ? (
|
||||
<div className="flex items-center gap-2 text-green-600">
|
||||
<div className="h-2 w-2 rounded-full bg-green-600"></div>
|
||||
<span>Connecté à Google Calendar ({
|
||||
calendarAuthStatus.authType === 'service_account' ? 'Service Account' :
|
||||
calendarAuthStatus.authType === 'api_externe' ? 'API Key' :
|
||||
'OAuth 2.0'
|
||||
})</span>
|
||||
<span>Connecté à Google Calendar (OAuth 2.0)</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-yellow-600">
|
||||
@@ -1921,12 +1793,12 @@ const Index = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{calendarAuthStatus.needsReauth && (!settings.google_auth_type || settings.google_auth_type === 'oauth2') ? (
|
||||
{calendarAuthStatus.needsReauth ? (
|
||||
<Button onClick={handleGoogleConnect} className="flex items-center gap-2">
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
Se connecter avec Google
|
||||
</Button>
|
||||
) : !calendarAuthStatus.needsReauth && (!settings.google_auth_type || settings.google_auth_type === 'oauth2') ? (
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
@@ -1957,23 +1829,7 @@ const Index = () => {
|
||||
Déconnecter
|
||||
</Button>
|
||||
</>
|
||||
) : settings.google_auth_type === 'api_externe' || settings.google_auth_type === 'service_account' ? (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await CalendarService.sync();
|
||||
showSuccess(`${result.syncedCount} événements synchronisés`);
|
||||
await loadCalendarEvents();
|
||||
} catch (error) {
|
||||
showError("Erreur lors de la synchronisation");
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Synchroniser
|
||||
</Button>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -2519,60 +2375,41 @@ const Index = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{/* Search field */}
|
||||
<Input
|
||||
placeholder="Rechercher un modèle..."
|
||||
value={modelSearchQuery}
|
||||
onChange={(e) => setModelSearchQuery(e.target.value)}
|
||||
className="text-xs h-8"
|
||||
disabled={loadingModels}
|
||||
/>
|
||||
|
||||
{/* Models list */}
|
||||
<div className="max-h-32 overflow-y-auto border rounded-md">
|
||||
<div className="mt-3 flex gap-2">
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(e) => setSelectedModel(e.target.value)}
|
||||
className="flex-1 text-xs px-2 py-1.5 border border-input rounded-md bg-background"
|
||||
disabled={chatLoading || loadingModels}
|
||||
>
|
||||
{loadingModels ? (
|
||||
<div className="text-xs text-center py-2 text-muted-foreground">
|
||||
Chargement des modèles...
|
||||
</div>
|
||||
<option value="">Chargement des modèles...</option>
|
||||
) : openRouterModels.length === 0 ? (
|
||||
<div className="text-xs text-center py-2 text-muted-foreground">
|
||||
Aucun modèle disponible
|
||||
</div>
|
||||
<option value="">Aucun modèle disponible</option>
|
||||
) : (
|
||||
openRouterModels
|
||||
.filter(model =>
|
||||
model.name.toLowerCase().includes(modelSearchQuery.toLowerCase()) ||
|
||||
model.id.toLowerCase().includes(modelSearchQuery.toLowerCase())
|
||||
)
|
||||
.map(model => (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`flex items-center justify-between px-2 py-1.5 hover:bg-accent cursor-pointer text-xs ${
|
||||
selectedModel === model.id ? 'bg-accent' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedModel(model.id)}
|
||||
>
|
||||
<span className="flex-1 truncate">{model.name}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSetDefaultModel(model.id);
|
||||
}}
|
||||
>
|
||||
{defaultModel === model.id ? (
|
||||
<span className="text-yellow-500">★</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">☆</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
openRouterModels.map(model => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</select>
|
||||
{selectedModel && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => handleSetDefaultModel(selectedModel)}
|
||||
disabled={loadingModels}
|
||||
title="Définir comme modèle par défaut"
|
||||
>
|
||||
{defaultModel === selectedModel ? (
|
||||
<span className="text-yellow-500">★</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">☆</span>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
Reference in new issue
Block a user