mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-23 15:18:16 +02:00
Fix critique: Système de notes complètement refondu pour fonctionner
Problème majeur résolu: - L'éditeur de notes réinitialisait le contenu à chaque modification - Causé par dangerouslySetInnerHTML qui forçait React à synchroniser - Résultat: impossible d'éditer les notes correctement Solution: - Suppression de dangerouslySetInnerHTML du RichTextEditor - Conservation du useEffect avec deps vides pour initialisation unique - Utilisation du key prop pour forcer remount lors du changement de note - Éditeur maintenant complètement "uncontrolled" Migration complète Google Calendar vers OAuth 2.0: - Remplacement de l'API Key simple par OAuth 2.0 (Client ID + Secret) - Nouvelle table google_oauth_tokens pour stocker les tokens - Routes OAuth: * GET /api/calendar/auth-url - Génère l'URL d'authentification * GET /api/calendar/oauth-callback - Callback OAuth avec échange de code * GET /api/calendar/auth-status - Vérifier le statut de connexion * POST /api/calendar/disconnect - Déconnecter Google - Gestion automatique du rafraîchissement des tokens expirés - Interface utilisateur mise à jour: * Champs Client ID et Client Secret * Indicateur de statut de connexion (connecté/non connecté) * Bouton "Se connecter avec Google" ouvrant popup OAuth * Bouton "Déconnecter" * URI de redirection affichée pour configuration Google Cloud Console - Synchronisation maintenant avec calendrier principal de l'utilisateur - Accès complet aux données privées du calendrier Architecture: - CalendarService.ts: Nouvelles méthodes getAuthUrl(), getAuthStatus(), disconnect() - Settings: Ajout de google_client_id et google_client_secret - Popup OAuth avec communication window.postMessage - Gestion d'erreur avec re-authentification automatique si token expiré
This commit is contained in:
6 files changed
+444
-56
No files matched your search
@@ -179,6 +179,23 @@ function initDatabase() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Table google_oauth_tokens (tokens OAuth pour Google Calendar)
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS google_oauth_tokens (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
access_token TEXT NOT NULL,
|
||||||
|
refresh_token TEXT,
|
||||||
|
token_type TEXT DEFAULT 'Bearer',
|
||||||
|
expiry_date INTEGER,
|
||||||
|
scope TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(user_id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
// Créer les index pour la performance
|
// Créer les index pour la performance
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id)');
|
db.run('CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id)');
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
|
db.run('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
|
||||||
|
|||||||
+236
-17
@@ -1,14 +1,217 @@
|
|||||||
// Routes pour Google Calendar
|
// Routes pour Google Calendar avec OAuth 2.0
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { google } = require('googleapis');
|
const { google } = require('googleapis');
|
||||||
const { getAll, getOne, runQuery } = require('../config/database');
|
const { getAll, getOne, runQuery } = require('../config/database');
|
||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
||||||
const logger = require('../config/logger');
|
const logger = require('../config/logger');
|
||||||
|
|
||||||
// Toutes les routes nécessitent authentification
|
// Toutes les routes nécessitent authentification
|
||||||
router.use(authenticateToken);
|
router.use(authenticateToken);
|
||||||
|
|
||||||
|
// Scopes Google Calendar requis
|
||||||
|
const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Créer un client OAuth2
|
||||||
|
*/
|
||||||
|
async function getOAuth2Client() {
|
||||||
|
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'");
|
||||||
|
|
||||||
|
if (!clientId || !clientId.value || !clientSecret || !clientSecret.value) {
|
||||||
|
throw new Error('Client ID et Client Secret non configurés');
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUri = `${process.env.APP_URL || 'http://localhost:2222'}/api/calendar/oauth-callback`;
|
||||||
|
|
||||||
|
return new google.auth.OAuth2(
|
||||||
|
clientId.value,
|
||||||
|
clientSecret.value,
|
||||||
|
redirectUri
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/auth-url
|
||||||
|
* Générer l'URL d'authentification OAuth
|
||||||
|
*/
|
||||||
|
router.get('/auth-url', requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const oauth2Client = await getOAuth2Client();
|
||||||
|
|
||||||
|
const authUrl = oauth2Client.generateAuthUrl({
|
||||||
|
access_type: 'offline',
|
||||||
|
scope: SCOPES,
|
||||||
|
prompt: 'consent', // Force le prompt pour obtenir refresh_token
|
||||||
|
state: req.user.id.toString() // Pour identifier l'utilisateur au retour
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ authUrl });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la génération de l\'URL OAuth:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Erreur lors de la génération de l\'URL',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/oauth-callback
|
||||||
|
* Callback OAuth pour récupérer les tokens
|
||||||
|
*/
|
||||||
|
router.get('/oauth-callback', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { code, state } = req.query;
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
return res.status(400).send('Code d\'autorisation manquant');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = parseInt(state);
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).send('État utilisateur invalide');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauth2Client = await getOAuth2Client();
|
||||||
|
|
||||||
|
// Échanger le code contre des tokens
|
||||||
|
const { tokens } = await oauth2Client.getToken(code);
|
||||||
|
|
||||||
|
// Sauvegarder les tokens dans la base de données
|
||||||
|
const existing = await getOne('SELECT id FROM google_oauth_tokens WHERE user_id = ?', [userId]);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await runQuery(`
|
||||||
|
UPDATE google_oauth_tokens
|
||||||
|
SET access_token = ?, refresh_token = COALESCE(?, refresh_token),
|
||||||
|
token_type = ?, expiry_date = ?, scope = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE user_id = ?
|
||||||
|
`, [
|
||||||
|
tokens.access_token,
|
||||||
|
tokens.refresh_token,
|
||||||
|
tokens.token_type || 'Bearer',
|
||||||
|
tokens.expiry_date || null,
|
||||||
|
tokens.scope || SCOPES.join(' '),
|
||||||
|
userId
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
await runQuery(`
|
||||||
|
INSERT INTO google_oauth_tokens
|
||||||
|
(user_id, access_token, refresh_token, token_type, expiry_date, scope)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, [
|
||||||
|
userId,
|
||||||
|
tokens.access_token,
|
||||||
|
tokens.refresh_token || null,
|
||||||
|
tokens.token_type || 'Bearer',
|
||||||
|
tokens.expiry_date || null,
|
||||||
|
tokens.scope || SCOPES.join(' ')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Tokens OAuth sauvegardés pour l'utilisateur ${userId}`);
|
||||||
|
|
||||||
|
// Rediriger vers l'application avec un message de succès
|
||||||
|
res.send(`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.opener.postMessage({ type: 'google-auth-success' }, '*');
|
||||||
|
window.close();
|
||||||
|
</script>
|
||||||
|
<h1>Authentification réussie!</h1>
|
||||||
|
<p>Vous pouvez fermer cette fenêtre et retourner à l'application.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors du callback OAuth:', error);
|
||||||
|
res.status(500).send(`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Erreur d'authentification</h1>
|
||||||
|
<p>${error.message}</p>
|
||||||
|
<script>
|
||||||
|
window.opener.postMessage({ type: 'google-auth-error', error: '${error.message}' }, '*');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/auth-status
|
||||||
|
* Vérifier si l'utilisateur est authentifié avec Google
|
||||||
|
*/
|
||||||
|
router.get('/auth-status', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const tokens = await getOne(
|
||||||
|
'SELECT access_token, expiry_date FROM google_oauth_tokens WHERE user_id = ?',
|
||||||
|
[req.user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAuthenticated = !!tokens;
|
||||||
|
const isExpired = tokens && tokens.expiry_date && tokens.expiry_date < Date.now();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
isAuthenticated,
|
||||||
|
isExpired,
|
||||||
|
needsReauth: !isAuthenticated || isExpired
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la vérification du statut OAuth:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer et rafraîchir les tokens OAuth si nécessaire
|
||||||
|
*/
|
||||||
|
async function getValidTokens(userId) {
|
||||||
|
const tokenData = await getOne(
|
||||||
|
'SELECT * FROM google_oauth_tokens WHERE user_id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!tokenData) {
|
||||||
|
throw new Error('Tokens OAuth non trouvés. Veuillez vous authentifier.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oauth2Client = await getOAuth2Client();
|
||||||
|
oauth2Client.setCredentials({
|
||||||
|
access_token: tokenData.access_token,
|
||||||
|
refresh_token: tokenData.refresh_token,
|
||||||
|
token_type: tokenData.token_type,
|
||||||
|
expiry_date: tokenData.expiry_date,
|
||||||
|
scope: tokenData.scope
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rafraîchir le token si expiré
|
||||||
|
if (tokenData.expiry_date && tokenData.expiry_date < Date.now()) {
|
||||||
|
try {
|
||||||
|
const { credentials } = await oauth2Client.refreshAccessToken();
|
||||||
|
|
||||||
|
// Mettre à jour les tokens dans la base de données
|
||||||
|
await runQuery(`
|
||||||
|
UPDATE google_oauth_tokens
|
||||||
|
SET access_token = ?, expiry_date = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE user_id = ?
|
||||||
|
`, [credentials.access_token, credentials.expiry_date, userId]);
|
||||||
|
|
||||||
|
oauth2Client.setCredentials(credentials);
|
||||||
|
logger.info(`Token OAuth rafraîchi pour l'utilisateur ${userId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors du rafraîchissement du token:', error);
|
||||||
|
throw new Error('Erreur lors du rafraîchissement du token. Veuillez vous reconnecter.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return oauth2Client;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/calendar/events
|
* GET /api/calendar/events
|
||||||
* Récupérer les événements du calendrier
|
* Récupérer les événements du calendrier
|
||||||
@@ -38,24 +241,15 @@ router.get('/events', async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/calendar/sync
|
* POST /api/calendar/sync
|
||||||
* Synchroniser avec Google Calendar
|
* Synchroniser avec Google Calendar via OAuth
|
||||||
*/
|
*/
|
||||||
router.post('/sync', async (req, res) => {
|
router.post('/sync', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Récupérer les paramètres Google Calendar depuis settings
|
// Obtenir un client OAuth valide
|
||||||
const apiKey = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_api_key'");
|
const oauth2Client = await getValidTokens(req.user.id);
|
||||||
const calendarId = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_id'");
|
|
||||||
|
|
||||||
if (!apiKey || !apiKey.value) {
|
// Créer un client Google Calendar avec OAuth
|
||||||
return res.status(400).json({ error: 'Clé API Google Calendar non configurée' });
|
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
||||||
}
|
|
||||||
|
|
||||||
if (!calendarId || !calendarId.value) {
|
|
||||||
return res.status(400).json({ error: 'ID du calendrier non configuré' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer un client Google Calendar
|
|
||||||
const calendar = google.calendar({ version: 'v3', auth: apiKey.value });
|
|
||||||
|
|
||||||
// Récupérer les événements des 30 prochains jours
|
// Récupérer les événements des 30 prochains jours
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -63,7 +257,7 @@ router.post('/sync', async (req, res) => {
|
|||||||
futureDate.setDate(futureDate.getDate() + 30);
|
futureDate.setDate(futureDate.getDate() + 30);
|
||||||
|
|
||||||
const response = await calendar.events.list({
|
const response = await calendar.events.list({
|
||||||
calendarId: calendarId.value,
|
calendarId: 'primary', // Calendrier principal de l'utilisateur
|
||||||
timeMin: now.toISOString(),
|
timeMin: now.toISOString(),
|
||||||
timeMax: futureDate.toISOString(),
|
timeMax: futureDate.toISOString(),
|
||||||
maxResults: 50,
|
maxResults: 50,
|
||||||
@@ -130,6 +324,15 @@ router.post('/sync', async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la synchronisation avec Google Calendar:', error);
|
logger.error('Erreur lors de la synchronisation avec Google Calendar:', error);
|
||||||
|
|
||||||
|
// Si l'erreur est liée à l'authentification, renvoyer un statut 401
|
||||||
|
if (error.message.includes('authentifier') || error.message.includes('reconnecter')) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: error.message,
|
||||||
|
needsReauth: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: 'Erreur lors de la synchronisation',
|
error: 'Erreur lors de la synchronisation',
|
||||||
details: error.message
|
details: error.message
|
||||||
@@ -137,6 +340,22 @@ router.post('/sync', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/calendar/disconnect
|
||||||
|
* Déconnecter Google Calendar (supprimer les tokens)
|
||||||
|
*/
|
||||||
|
router.post('/disconnect', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await runQuery('DELETE FROM google_oauth_tokens WHERE user_id = ?', [req.user.id]);
|
||||||
|
logger.info(`Tokens OAuth supprimés pour l'utilisateur ${req.user.username}`);
|
||||||
|
|
||||||
|
res.json({ message: 'Déconnecté avec succès' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la déconnexion:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/calendar/events/:id
|
* DELETE /api/calendar/events/:id
|
||||||
* Supprimer un événement local
|
* Supprimer un événement local
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
dir="ltr"
|
dir="ltr"
|
||||||
className="min-h-[400px] outline-none prose prose-sm max-w-none dark:prose-invert"
|
className="min-h-[400px] outline-none prose prose-sm max-w-none dark:prose-invert"
|
||||||
dangerouslySetInnerHTML={{ __html: content }}
|
|
||||||
data-placeholder={placeholder || 'Commencez à écrire...'}
|
data-placeholder={placeholder || 'Commencez à écrire...'}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
|
|||||||
+154
-38
@@ -54,6 +54,11 @@ const Index = () => {
|
|||||||
const [noteTags, setNoteTags] = useState<Tag[]>([]);
|
const [noteTags, setNoteTags] = useState<Tag[]>([]);
|
||||||
const [openRouterModels, setOpenRouterModels] = useState<OpenRouterModel[]>([]);
|
const [openRouterModels, setOpenRouterModels] = useState<OpenRouterModel[]>([]);
|
||||||
const [loadingModels, setLoadingModels] = useState(false);
|
const [loadingModels, setLoadingModels] = useState(false);
|
||||||
|
const [calendarAuthStatus, setCalendarAuthStatus] = useState({
|
||||||
|
isAuthenticated: false,
|
||||||
|
isExpired: false,
|
||||||
|
needsReauth: true
|
||||||
|
});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Modal states
|
// Modal states
|
||||||
@@ -189,6 +194,75 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
}, [showAdmin, adminTab, settings.openrouter_api_key]);
|
}, [showAdmin, adminTab, settings.openrouter_api_key]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showAdmin && adminTab === 'calendar') {
|
||||||
|
checkCalendarAuthStatus();
|
||||||
|
}
|
||||||
|
}, [showAdmin, adminTab]);
|
||||||
|
|
||||||
|
const checkCalendarAuthStatus = async () => {
|
||||||
|
try {
|
||||||
|
const status = await CalendarService.getAuthStatus();
|
||||||
|
setCalendarAuthStatus(status);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la vérification du statut OAuth:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleConnect = async () => {
|
||||||
|
try {
|
||||||
|
const authUrl = await CalendarService.getAuthUrl();
|
||||||
|
|
||||||
|
// Ouvrir une popup pour l'authentification OAuth
|
||||||
|
const width = 600;
|
||||||
|
const height = 700;
|
||||||
|
const left = window.screen.width / 2 - width / 2;
|
||||||
|
const top = window.screen.height / 2 - height / 2;
|
||||||
|
|
||||||
|
const popup = window.open(
|
||||||
|
authUrl,
|
||||||
|
'GoogleAuth',
|
||||||
|
`width=${width},height=${height},top=${top},left=${left}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Écouter le message de la popup
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
if (event.data.type === 'google-auth-success') {
|
||||||
|
showSuccess('Authentification Google réussie');
|
||||||
|
checkCalendarAuthStatus();
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
} else if (event.data.type === 'google-auth-error') {
|
||||||
|
showError(`Erreur d'authentification: ${event.data.error}`);
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
|
// Fermer le listener si la popup est fermée manuellement
|
||||||
|
const checkPopup = setInterval(() => {
|
||||||
|
if (popup && popup.closed) {
|
||||||
|
clearInterval(checkPopup);
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
showError('Erreur lors de la génération de l\'URL d\'authentification');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleDisconnect = async () => {
|
||||||
|
try {
|
||||||
|
const success = await CalendarService.disconnect();
|
||||||
|
if (success) {
|
||||||
|
showSuccess('Déconnecté de Google Calendar');
|
||||||
|
checkCalendarAuthStatus();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError('Erreur lors de la déconnexion');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
AuthService.logout();
|
AuthService.logout();
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
@@ -1271,64 +1345,106 @@ const Index = () => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CalendarIcon className="h-5 w-5" />
|
<CalendarIcon className="h-5 w-5" />
|
||||||
Configuration Google Calendar
|
Configuration Google Calendar OAuth 2.0
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="calendar-api-key">Clé API Google Calendar</Label>
|
<Label htmlFor="google-client-id">Client ID Google</Label>
|
||||||
<Input
|
<Input
|
||||||
id="calendar-api-key"
|
id="google-client-id"
|
||||||
type="password"
|
type="text"
|
||||||
placeholder="AIza..."
|
placeholder="123456789-abcdef.apps.googleusercontent.com"
|
||||||
value={settings.google_calendar_api_key || ''}
|
value={settings.google_client_id || ''}
|
||||||
onChange={(e) => setSettings({ ...settings, google_calendar_api_key: e.target.value })}
|
onChange={(e) => setSettings({ ...settings, google_client_id: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Créez une clé API 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>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="calendar-id">ID du calendrier</Label>
|
<Label htmlFor="google-client-secret">Client Secret Google</Label>
|
||||||
<Input
|
<Input
|
||||||
id="calendar-id"
|
id="google-client-secret"
|
||||||
placeholder="votre-email@gmail.com ou id-calendrier"
|
type="password"
|
||||||
value={settings.google_calendar_id || ''}
|
placeholder="GOCSPX-..."
|
||||||
onChange={(e) => setSettings({ ...settings, google_calendar_id: e.target.value })}
|
value={settings.google_client_secret || ''}
|
||||||
|
onChange={(e) => setSettings({ ...settings, google_client_secret: e.target.value })}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Trouvez l'ID du calendrier dans les paramètres de votre calendrier Google.<br />
|
Créez un projet OAuth 2.0 sur{' '}
|
||||||
Pour votre calendrier principal, utilisez votre adresse email.
|
<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.
|
||||||
|
<br />
|
||||||
|
<strong>URI de redirection autorisée:</strong> {window.location.origin}/api/calendar/oauth-callback
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
||||||
<CalendarIcon className="h-4 w-4" />
|
<Key className="h-4 w-4" />
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
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. Vérifiez votre configuration.");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
Tester la synchronisation
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{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</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 text-yellow-600">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-yellow-600"></div>
|
||||||
|
<span>Non connecté</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{calendarAuthStatus.needsReauth ? (
|
||||||
|
<Button onClick={handleGoogleConnect} className="flex items-center gap-2">
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
Se connecter avec Google
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const result = await CalendarService.sync();
|
||||||
|
showSuccess(`${result.syncedCount} événements synchronisés`);
|
||||||
|
await loadCalendarEvents();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.data?.needsReauth) {
|
||||||
|
showError("Session expirée. Veuillez vous reconnecter.");
|
||||||
|
checkCalendarAuthStatus();
|
||||||
|
} else {
|
||||||
|
showError("Erreur lors de la synchronisation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
Synchroniser
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleGoogleDisconnect}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
Déconnecter
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -37,6 +37,40 @@ class CalendarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir l'URL d'authentification OAuth Google
|
||||||
|
*/
|
||||||
|
async getAuthUrl(): Promise<string> {
|
||||||
|
const response = await axios.get(`${this.baseURL}/auth-url`, this.getAuthHeader());
|
||||||
|
return response.data.authUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier le statut d'authentification Google
|
||||||
|
*/
|
||||||
|
async getAuthStatus(): Promise<{ isAuthenticated: boolean; isExpired: boolean; needsReauth: boolean }> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.baseURL}/auth-status`, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la vérification du statut OAuth:", error);
|
||||||
|
return { isAuthenticated: false, isExpired: false, needsReauth: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déconnecter Google Calendar
|
||||||
|
*/
|
||||||
|
async disconnect(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.post(`${this.baseURL}/disconnect`, {}, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la déconnexion:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchroniser avec Google Calendar
|
* Synchroniser avec Google Calendar
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export interface Settings {
|
|||||||
ai_model?: string;
|
ai_model?: string;
|
||||||
rss_refresh_interval?: number;
|
rss_refresh_interval?: number;
|
||||||
theme?: 'light' | 'dark' | 'auto';
|
theme?: 'light' | 'dark' | 'auto';
|
||||||
|
google_client_id?: string;
|
||||||
|
google_client_secret?: string;
|
||||||
|
// Legacy fields (kept for backward compatibility)
|
||||||
google_calendar_api_key?: string;
|
google_calendar_api_key?: string;
|
||||||
google_calendar_id?: string;
|
google_calendar_id?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user