mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-22 22:58:15 +02:00
Merge pull request #32 from R0m1k3/claude/fix-npm-ci-build-error-011CV3c2K4ETHdBRQUs7tkJf
Feature: Support multi-images, Google Calendar et correction RTL
This commit is contained in:
10 files changed
+946
-5
No files matched your search
@@ -74,6 +74,18 @@ function initDatabase() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Table note_images (images attachées aux notes)
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS note_images (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
note_id INTEGER NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
original_name TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
// Table note_files (fichiers attachés aux notes)
|
// Table note_files (fichiers attachés aux notes)
|
||||||
db.run(`
|
db.run(`
|
||||||
CREATE TABLE IF NOT EXISTS note_files (
|
CREATE TABLE IF NOT EXISTS note_files (
|
||||||
@@ -137,14 +149,35 @@ function initDatabase() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Table calendar_events (événements Google Calendar)
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
google_event_id TEXT UNIQUE NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
start_time DATETIME NOT NULL,
|
||||||
|
end_time DATETIME NOT NULL,
|
||||||
|
location TEXT,
|
||||||
|
color_id TEXT,
|
||||||
|
html_link TEXT,
|
||||||
|
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
// 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)');
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_global_todos_user ON global_todos(user_id)');
|
db.run('CREATE INDEX IF NOT EXISTS idx_global_todos_user ON global_todos(user_id)');
|
||||||
|
db.run('CREATE INDEX IF NOT EXISTS idx_note_images ON note_images(note_id)');
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_note_files ON note_files(note_id)');
|
db.run('CREATE INDEX IF NOT EXISTS idx_note_files ON note_files(note_id)');
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key)');
|
db.run('CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key)');
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_rss_articles_feed ON rss_articles(feed_id)');
|
db.run('CREATE INDEX IF NOT EXISTS idx_rss_articles_feed ON rss_articles(feed_id)');
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_rss_articles_date ON rss_articles(pub_date)');
|
db.run('CREATE INDEX IF NOT EXISTS idx_rss_articles_date ON rss_articles(pub_date)');
|
||||||
|
db.run('CREATE INDEX IF NOT EXISTS idx_calendar_events_user ON calendar_events(user_id)');
|
||||||
|
db.run('CREATE INDEX IF NOT EXISTS idx_calendar_events_start ON calendar_events(start_time)');
|
||||||
|
|
||||||
logger.info('✓ Tables de base de données créées avec succès');
|
logger.info('✓ Tables de base de données créées avec succès');
|
||||||
|
|
||||||
|
|||||||
Generated
+352
@@ -49,6 +49,7 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^7.1.5",
|
"express-rate-limit": "^7.1.5",
|
||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
|
"googleapis": "^166.0.0",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@@ -4338,6 +4339,15 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bignumber.js": {
|
||||||
|
"version": "9.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||||
|
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -5121,6 +5131,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/data-uri-to-buffer": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||||
@@ -5822,6 +5841,12 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/extend": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -5895,6 +5920,29 @@
|
|||||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fetch-blob": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-domexception": "^1.0.0",
|
||||||
|
"web-streams-polyfill": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20 || >= 14.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -6067,6 +6115,18 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/formdata-polyfill": {
|
||||||
|
"version": "4.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
|
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fetch-blob": "^3.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -6167,6 +6227,143 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gaxios": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.1",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"rimraf": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gaxios/node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gaxios/node_modules/brace-expansion": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gaxios/node_modules/glob": {
|
||||||
|
"version": "10.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||||
|
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"foreground-child": "^3.1.0",
|
||||||
|
"jackspeak": "^3.1.2",
|
||||||
|
"minimatch": "^9.0.4",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"package-json-from-dist": "^1.0.0",
|
||||||
|
"path-scurry": "^1.11.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"glob": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gaxios/node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gaxios/node_modules/minimatch": {
|
||||||
|
"version": "9.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
|
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gaxios/node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gaxios/node_modules/node-fetch": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"data-uri-to-buffer": "^4.0.0",
|
||||||
|
"fetch-blob": "^3.1.4",
|
||||||
|
"formdata-polyfill": "^4.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/node-fetch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gaxios/node_modules/rimraf": {
|
||||||
|
"version": "5.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
|
||||||
|
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^10.3.7"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rimraf": "dist/esm/bin.mjs"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gcp-metadata": {
|
||||||
|
"version": "8.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
||||||
|
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"gaxios": "^7.0.0",
|
||||||
|
"google-logging-utils": "^1.0.0",
|
||||||
|
"json-bigint": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -6275,6 +6472,83 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/google-auth-library": {
|
||||||
|
"version": "10.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
|
||||||
|
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.0",
|
||||||
|
"ecdsa-sig-formatter": "^1.0.11",
|
||||||
|
"gaxios": "^7.0.0",
|
||||||
|
"gcp-metadata": "^8.0.0",
|
||||||
|
"google-logging-utils": "^1.0.0",
|
||||||
|
"gtoken": "^8.0.0",
|
||||||
|
"jws": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/google-auth-library/node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/google-auth-library/node_modules/jws": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.0",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/google-logging-utils": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-YsFPGVgDFf4IzSwbwIR0iaFJQFmR5Jp7V1WuYSjuRgAm9yWqsMhKE9YPlL+wvFLnc/wMiFV4SQUD9Y/JMpxIxQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/googleapis": {
|
||||||
|
"version": "166.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-166.0.0.tgz",
|
||||||
|
"integrity": "sha512-Xz7yG+ihVvYMwSULKIMeTp6vDX95rrSbsaR3xUmgYjwQdAkchMQ5ndFeG5QZwcBOIyvJFvG7ZLIZE7mKW2X/hw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"google-auth-library": "^10.2.0",
|
||||||
|
"googleapis-common": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/googleapis-common": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-66if47It7y+Sab3HMkwEXx1kCq9qUC9px8ZXoj1CMrmLmUw81GpbnsNlXnlyZyGbGPGcj+tDD9XsZ23m7GLaJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"gaxios": "^7.0.0-rc.4",
|
||||||
|
"google-auth-library": "^10.1.0",
|
||||||
|
"qs": "^6.7.0",
|
||||||
|
"url-template": "^2.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -6301,6 +6575,40 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/gtoken": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"gaxios": "^7.0.0",
|
||||||
|
"jws": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gtoken/node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gtoken/node_modules/jws": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.0",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -6733,6 +7041,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/json-bigint": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bignumber.js": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-buffer": {
|
"node_modules/json-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
@@ -7426,6 +7743,26 @@
|
|||||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-domexception": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
|
"deprecated": "Use your platform's native DOMException instead",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
@@ -9833,6 +10170,12 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/url-template": {
|
||||||
|
"version": "2.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
|
||||||
|
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
|
||||||
|
"license": "BSD"
|
||||||
|
},
|
||||||
"node_modules/use-callback-ref": {
|
"node_modules/use-callback-ref": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||||
@@ -10056,6 +10399,15 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^7.1.5",
|
"express-rate-limit": "^7.1.5",
|
||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
|
"googleapis": "^166.0.0",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"input-otp": "^1.4.1",
|
"input-otp": "^1.4.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// Routes pour Google Calendar
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { google } = require('googleapis');
|
||||||
|
const { getAll, getOne, runQuery } = require('../config/database');
|
||||||
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
|
const logger = require('../config/logger');
|
||||||
|
|
||||||
|
// Toutes les routes nécessitent authentification
|
||||||
|
router.use(authenticateToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/calendar/events
|
||||||
|
* Récupérer les événements du calendrier
|
||||||
|
*/
|
||||||
|
router.get('/events', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const limit = parseInt(req.query.limit) || 10;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Récupérer les événements à venir depuis la base de données
|
||||||
|
const events = await getAll(`
|
||||||
|
SELECT
|
||||||
|
id, google_event_id, title, description,
|
||||||
|
start_time, end_time, location, html_link
|
||||||
|
FROM calendar_events
|
||||||
|
WHERE user_id = ? AND start_time >= ?
|
||||||
|
ORDER BY start_time ASC
|
||||||
|
LIMIT ?
|
||||||
|
`, [req.user.id, now, limit]);
|
||||||
|
|
||||||
|
res.json(events || []);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la récupération des événements:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/calendar/sync
|
||||||
|
* Synchroniser avec Google Calendar
|
||||||
|
*/
|
||||||
|
router.post('/sync', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Récupérer les paramètres Google Calendar depuis settings
|
||||||
|
const apiKey = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_api_key'");
|
||||||
|
const calendarId = await getOne("SELECT value FROM settings WHERE key = 'google_calendar_id'");
|
||||||
|
|
||||||
|
if (!apiKey || !apiKey.value) {
|
||||||
|
return res.status(400).json({ error: 'Clé API Google Calendar non configurée' });
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
const now = new Date();
|
||||||
|
const futureDate = new Date();
|
||||||
|
futureDate.setDate(futureDate.getDate() + 30);
|
||||||
|
|
||||||
|
const response = await calendar.events.list({
|
||||||
|
calendarId: calendarId.value,
|
||||||
|
timeMin: now.toISOString(),
|
||||||
|
timeMax: futureDate.toISOString(),
|
||||||
|
maxResults: 50,
|
||||||
|
singleEvents: true,
|
||||||
|
orderBy: 'startTime',
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = response.data.items || [];
|
||||||
|
let syncedCount = 0;
|
||||||
|
|
||||||
|
// Sauvegarder chaque événement dans la base de données
|
||||||
|
for (const event of events) {
|
||||||
|
const startTime = event.start.dateTime || event.start.date;
|
||||||
|
const endTime = event.end.dateTime || event.end.date;
|
||||||
|
|
||||||
|
// Vérifier si l'événement existe déjà
|
||||||
|
const existing = await getOne(
|
||||||
|
'SELECT id FROM calendar_events WHERE google_event_id = ? AND user_id = ?',
|
||||||
|
[event.id, req.user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Mettre à jour l'événement existant
|
||||||
|
await runQuery(`
|
||||||
|
UPDATE calendar_events
|
||||||
|
SET title = ?, description = ?, start_time = ?, end_time = ?,
|
||||||
|
location = ?, html_link = ?, synced_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`, [
|
||||||
|
event.summary || 'Sans titre',
|
||||||
|
event.description || '',
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
event.location || '',
|
||||||
|
event.htmlLink || '',
|
||||||
|
existing.id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Créer un nouvel événement
|
||||||
|
await runQuery(`
|
||||||
|
INSERT INTO calendar_events
|
||||||
|
(google_event_id, user_id, title, description, start_time, end_time, location, html_link)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`, [
|
||||||
|
event.id,
|
||||||
|
req.user.id,
|
||||||
|
event.summary || 'Sans titre',
|
||||||
|
event.description || '',
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
event.location || '',
|
||||||
|
event.htmlLink || ''
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
syncedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${syncedCount} événements synchronisés pour l'utilisateur ${req.user.username}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Synchronisation réussie',
|
||||||
|
syncedCount,
|
||||||
|
events: events.length
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la synchronisation avec Google Calendar:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Erreur lors de la synchronisation',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/calendar/events/:id
|
||||||
|
* Supprimer un événement local
|
||||||
|
*/
|
||||||
|
router.delete('/events/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const event = await getOne(
|
||||||
|
'SELECT id FROM calendar_events WHERE id = ? AND user_id = ?',
|
||||||
|
[req.params.id, req.user.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
return res.status(404).json({ error: 'Événement non trouvé' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await runQuery('DELETE FROM calendar_events WHERE id = ?', [req.params.id]);
|
||||||
|
|
||||||
|
logger.info(`Événement ${req.params.id} supprimé`);
|
||||||
|
|
||||||
|
res.json({ message: 'Événement supprimé avec succès' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la suppression de l\'événement:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
+171
-1
@@ -58,7 +58,7 @@ router.get('/', async (req, res) => {
|
|||||||
ORDER BY n.updated_at DESC
|
ORDER BY n.updated_at DESC
|
||||||
`, [req.user.id, archivedFilter]);
|
`, [req.user.id, archivedFilter]);
|
||||||
|
|
||||||
// Charger les todos et fichiers pour chaque note
|
// Charger les todos, images et fichiers pour chaque note
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
const todos = await getAll(`
|
const todos = await getAll(`
|
||||||
SELECT id, text, completed, position
|
SELECT id, text, completed, position
|
||||||
@@ -68,6 +68,14 @@ router.get('/', async (req, res) => {
|
|||||||
`, [note.id]);
|
`, [note.id]);
|
||||||
note.todos = todos || [];
|
note.todos = todos || [];
|
||||||
|
|
||||||
|
const images = await getAll(`
|
||||||
|
SELECT id, filename, original_name, created_at
|
||||||
|
FROM note_images
|
||||||
|
WHERE note_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`, [note.id]);
|
||||||
|
note.images = images || [];
|
||||||
|
|
||||||
const files = await getAll(`
|
const files = await getAll(`
|
||||||
SELECT id, filename, original_name, file_size, mime_type, created_at
|
SELECT id, filename, original_name, file_size, mime_type, created_at
|
||||||
FROM note_files
|
FROM note_files
|
||||||
@@ -111,6 +119,26 @@ router.get('/:id', async (req, res) => {
|
|||||||
|
|
||||||
note.todos = todos;
|
note.todos = todos;
|
||||||
|
|
||||||
|
// Récupérer les images de la note
|
||||||
|
const images = await getAll(`
|
||||||
|
SELECT id, filename, original_name, created_at
|
||||||
|
FROM note_images
|
||||||
|
WHERE note_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`, [req.params.id]);
|
||||||
|
|
||||||
|
note.images = images || [];
|
||||||
|
|
||||||
|
// Récupérer les fichiers de la note
|
||||||
|
const files = await getAll(`
|
||||||
|
SELECT id, filename, original_name, file_size, mime_type, created_at
|
||||||
|
FROM note_files
|
||||||
|
WHERE note_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`, [req.params.id]);
|
||||||
|
|
||||||
|
note.files = files || [];
|
||||||
|
|
||||||
res.json(note);
|
res.json(note);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Erreur lors de la récupération de la note:', error);
|
logger.error('Erreur lors de la récupération de la note:', error);
|
||||||
@@ -487,6 +515,148 @@ const searchNotes = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==================== IMAGE ATTACHMENTS ====================
|
||||||
|
|
||||||
|
// Configuration multer pour les images multiples
|
||||||
|
const imageStorage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
const imagesDir = path.join(__dirname, '../public/uploads/images');
|
||||||
|
// Créer le dossier s'il n'existe pas
|
||||||
|
if (!fs.existsSync(imagesDir)) {
|
||||||
|
fs.mkdirSync(imagesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
cb(null, imagesDir);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${path.extname(file.originalname)}`;
|
||||||
|
cb(null, uniqueName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageUpload = multer({
|
||||||
|
storage: imageStorage,
|
||||||
|
limits: { fileSize: parseInt(process.env.MAX_FILE_SIZE) || 5 * 1024 * 1024 }, // 5MB
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
||||||
|
if (allowedTypes.includes(file.mimetype)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Type de fichier non autorisé. Utilisez JPEG, PNG, WebP ou GIF.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/notes/:id/images
|
||||||
|
* Ajouter une image à une note
|
||||||
|
*/
|
||||||
|
router.post('/:id/images', imageUpload.single('image'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'Aucune image fournie' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que la note appartient à l'utilisateur
|
||||||
|
const note = await getOne('SELECT id FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
|
||||||
|
if (!note) {
|
||||||
|
// Supprimer le fichier uploadé
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
return res.status(404).json({ error: 'Note non trouvée' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enregistrer l'image dans la base de données
|
||||||
|
const result = await runQuery(`
|
||||||
|
INSERT INTO note_images (note_id, filename, original_name)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`, [req.params.id, req.file.filename, req.file.originalname]);
|
||||||
|
|
||||||
|
// Mettre à jour la date de modification de la note
|
||||||
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.params.id]);
|
||||||
|
|
||||||
|
logger.info(`Image ajoutée à la note ${req.params.id}: ${req.file.filename}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Image ajoutée avec succès',
|
||||||
|
image: {
|
||||||
|
id: result.id,
|
||||||
|
filename: req.file.filename,
|
||||||
|
original_name: req.file.originalname
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de l\'ajout de l\'image:', error);
|
||||||
|
if (req.file && fs.existsSync(req.file.path)) {
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
}
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/notes/:id/images
|
||||||
|
* Liste les images d'une note
|
||||||
|
*/
|
||||||
|
router.get('/:id/images', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Vérifier que la note appartient à l'utilisateur
|
||||||
|
const note = await getOne('SELECT id FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
|
||||||
|
if (!note) {
|
||||||
|
return res.status(404).json({ error: 'Note non trouvée' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = await getAll(`
|
||||||
|
SELECT id, filename, original_name, created_at
|
||||||
|
FROM note_images
|
||||||
|
WHERE note_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`, [req.params.id]);
|
||||||
|
|
||||||
|
res.json(images);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la récupération des images:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/notes/:noteId/images/:imageId
|
||||||
|
* Supprimer une image d'une note
|
||||||
|
*/
|
||||||
|
router.delete('/:noteId/images/:imageId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Vérifier que l'image appartient à une note de l'utilisateur
|
||||||
|
const image = await getOne(`
|
||||||
|
SELECT ni.id, ni.filename, ni.note_id
|
||||||
|
FROM note_images ni
|
||||||
|
JOIN notes n ON ni.note_id = n.id
|
||||||
|
WHERE ni.id = ? AND ni.note_id = ? AND n.user_id = ?
|
||||||
|
`, [req.params.imageId, req.params.noteId, req.user.id]);
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
return res.status(404).json({ error: 'Image non trouvée' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer le fichier physique
|
||||||
|
const imagePath = path.join(__dirname, '../public/uploads/images', image.filename);
|
||||||
|
if (fs.existsSync(imagePath)) {
|
||||||
|
fs.unlinkSync(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer l'entrée de la base de données
|
||||||
|
await runQuery('DELETE FROM note_images WHERE id = ?', [req.params.imageId]);
|
||||||
|
|
||||||
|
// Mettre à jour la date de modification de la note
|
||||||
|
await runQuery('UPDATE notes SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.params.noteId]);
|
||||||
|
|
||||||
|
logger.info(`Image ${image.filename} supprimée de la note ${req.params.noteId}`);
|
||||||
|
|
||||||
|
res.json({ message: 'Image supprimée avec succès' });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de la suppression de l\'image:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== FILE ATTACHMENTS ====================
|
// ==================== FILE ATTACHMENTS ====================
|
||||||
|
|
||||||
// Configuration multer pour tous types de fichiers
|
// Configuration multer pour tous types de fichiers
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ app.use('/api/notes', require('./routes/notes.routes'));
|
|||||||
app.use('/api/todos', require('./routes/todos.routes'));
|
app.use('/api/todos', require('./routes/todos.routes'));
|
||||||
app.use('/api/settings', require('./routes/settings.routes'));
|
app.use('/api/settings', require('./routes/settings.routes'));
|
||||||
app.use('/api/rss', require('./routes/rss.routes'));
|
app.use('/api/rss', require('./routes/rss.routes'));
|
||||||
|
app.use('/api/calendar', require('./routes/calendar.routes'));
|
||||||
|
|
||||||
// Route de recherche
|
// Route de recherche
|
||||||
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
||||||
|
|||||||
@@ -158,12 +158,15 @@ export function RichTextEditor({ content, onChange, placeholder }: RichTextEdito
|
|||||||
contentEditable
|
contentEditable
|
||||||
onInput={updateContent}
|
onInput={updateContent}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
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 }}
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
data-placeholder={placeholder || 'Commencez à écrire...'}
|
data-placeholder={placeholder || 'Commencez à écrire...'}
|
||||||
style={{
|
style={{
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
lineHeight: '1.6'
|
lineHeight: '1.6',
|
||||||
|
direction: 'ltr',
|
||||||
|
textAlign: 'left'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+154
-3
@@ -13,7 +13,7 @@ import { RichTextEditor } from "@/components/RichTextEditor";
|
|||||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||||
import {
|
import {
|
||||||
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
|
PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2,
|
||||||
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit
|
Image as ImageIcon, CheckSquare, FileText, Rss, ExternalLink, RefreshCw, Key, Zap, Paperclip, X, Edit, Calendar as CalendarIcon
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import AuthService from "@/services/AuthService";
|
import AuthService from "@/services/AuthService";
|
||||||
import AdminService from "@/services/AdminService";
|
import AdminService from "@/services/AdminService";
|
||||||
@@ -21,6 +21,7 @@ import NotesService, { Note } from "@/services/NotesService";
|
|||||||
import TodosService, { Todo } from "@/services/TodosService";
|
import TodosService, { Todo } from "@/services/TodosService";
|
||||||
import RssService, { RssFeed, RssArticle } from "@/services/RssService";
|
import RssService, { RssFeed, RssArticle } from "@/services/RssService";
|
||||||
import SettingsService, { Settings as AppSettings } from "@/services/SettingsService";
|
import SettingsService, { Settings as AppSettings } from "@/services/SettingsService";
|
||||||
|
import CalendarService, { CalendarEvent } from "@/services/CalendarService";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { showError, showSuccess } from "@/utils/toast";
|
import { showError, showSuccess } from "@/utils/toast";
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ const Index = () => {
|
|||||||
const [todos, setTodos] = useState<Todo[]>([]);
|
const [todos, setTodos] = useState<Todo[]>([]);
|
||||||
const [rssFeeds, setRssFeeds] = useState<RssFeed[]>([]);
|
const [rssFeeds, setRssFeeds] = useState<RssFeed[]>([]);
|
||||||
const [rssArticles, setRssArticles] = useState<RssArticle[]>([]);
|
const [rssArticles, setRssArticles] = useState<RssArticle[]>([]);
|
||||||
|
const [calendarEvents, setCalendarEvents] = useState<CalendarEvent[]>([]);
|
||||||
const [settings, setSettings] = useState<AppSettings>({});
|
const [settings, setSettings] = useState<AppSettings>({});
|
||||||
const [adminTab, setAdminTab] = useState("users");
|
const [adminTab, setAdminTab] = useState("users");
|
||||||
const [showArchived, setShowArchived] = useState(false);
|
const [showArchived, setShowArchived] = useState(false);
|
||||||
@@ -53,6 +55,7 @@ const Index = () => {
|
|||||||
loadTodos();
|
loadTodos();
|
||||||
loadRssFeeds();
|
loadRssFeeds();
|
||||||
loadRssArticles();
|
loadRssArticles();
|
||||||
|
loadCalendarEvents();
|
||||||
} else {
|
} else {
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
}
|
}
|
||||||
@@ -102,6 +105,17 @@ const Index = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadCalendarEvents = async () => {
|
||||||
|
try {
|
||||||
|
const events = await CalendarService.getEvents(10);
|
||||||
|
if (Array.isArray(events)) {
|
||||||
|
setCalendarEvents(events);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du chargement des événements:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
if (user?.is_admin) {
|
if (user?.is_admin) {
|
||||||
try {
|
try {
|
||||||
@@ -898,11 +912,80 @@ const Index = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Calendar Box */}
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-xl flex items-center gap-2">
|
||||||
|
<CalendarIcon className="h-6 w-6" />
|
||||||
|
Rendez-vous
|
||||||
|
</CardTitle>
|
||||||
|
<Button size="lg" 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");
|
||||||
|
}
|
||||||
|
}} className="gap-2">
|
||||||
|
<RefreshCw className="h-5 w-5" />
|
||||||
|
Synchroniser
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-3 max-h-[480px] overflow-y-auto">
|
||||||
|
{calendarEvents.length > 0 ? (
|
||||||
|
calendarEvents.map(event => {
|
||||||
|
const startDate = new Date(event.start_time);
|
||||||
|
const endDate = new Date(event.end_time);
|
||||||
|
const isToday = startDate.toDateString() === new Date().toDateString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className="p-3 border rounded-lg hover:bg-accent/50 transition-colors cursor-pointer"
|
||||||
|
onClick={() => event.html_link && window.open(event.html_link, '_blank')}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="font-medium line-clamp-2 mb-1">{event.title}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{isToday ? "Aujourd'hui" : startDate.toLocaleDateString('fr-FR')} à {startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</p>
|
||||||
|
{event.location && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
📍 {event.location}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ExternalLink className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground py-8">
|
||||||
|
Aucun rendez-vous à venir
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={() => setShowAdmin(true)}
|
||||||
|
>
|
||||||
|
Configurer Google Calendar
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin Modal */}
|
|
||||||
{/* Admin Modal */}
|
{/* Admin Modal */}
|
||||||
<Dialog open={showAdmin} onOpenChange={setShowAdmin}>
|
<Dialog open={showAdmin} onOpenChange={setShowAdmin}>
|
||||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
@@ -914,9 +997,10 @@ const Index = () => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Tabs value={adminTab} onValueChange={setAdminTab}>
|
<Tabs value={adminTab} onValueChange={setAdminTab}>
|
||||||
<TabsList className="grid grid-cols-4 w-full">
|
<TabsList className="grid grid-cols-5 w-full">
|
||||||
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
<TabsTrigger value="users">Utilisateurs</TabsTrigger>
|
||||||
<TabsTrigger value="rss">Flux RSS</TabsTrigger>
|
<TabsTrigger value="rss">Flux RSS</TabsTrigger>
|
||||||
|
<TabsTrigger value="calendar">Google Calendar</TabsTrigger>
|
||||||
<TabsTrigger value="openrouter">OpenRouter</TabsTrigger>
|
<TabsTrigger value="openrouter">OpenRouter</TabsTrigger>
|
||||||
<TabsTrigger value="settings">Paramètres</TabsTrigger>
|
<TabsTrigger value="settings">Paramètres</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -1003,6 +1087,73 @@ const Index = () => {
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="calendar" className="mt-4 space-y-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CalendarIcon className="h-5 w-5" />
|
||||||
|
Configuration Google Calendar
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="calendar-api-key">Clé API Google Calendar</Label>
|
||||||
|
<Input
|
||||||
|
id="calendar-api-key"
|
||||||
|
type="password"
|
||||||
|
placeholder="AIza..."
|
||||||
|
value={settings.google_calendar_api_key || ''}
|
||||||
|
onChange={(e) => setSettings({ ...settings, google_calendar_api_key: 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 className="space-y-2">
|
||||||
|
<Label htmlFor="calendar-id">ID du calendrier</Label>
|
||||||
|
<Input
|
||||||
|
id="calendar-id"
|
||||||
|
placeholder="votre-email@gmail.com ou id-calendrier"
|
||||||
|
value={settings.google_calendar_id || ''}
|
||||||
|
onChange={(e) => setSettings({ ...settings, google_calendar_id: e.target.value })}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Trouvez l'ID du calendrier dans les paramètres de votre calendrier Google.<br />
|
||||||
|
Pour votre calendrier principal, utilisez votre adresse email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleSaveSettings} className="flex items-center gap-2">
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
Enregistrer
|
||||||
|
</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>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="openrouter" className="mt-4 space-y-4">
|
<TabsContent value="openrouter" className="mt-4 space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import AuthService from "./AuthService";
|
||||||
|
|
||||||
|
export interface CalendarEvent {
|
||||||
|
id: number;
|
||||||
|
google_event_id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
location?: string;
|
||||||
|
html_link?: string;
|
||||||
|
synced_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CalendarService {
|
||||||
|
private baseURL = "/api/calendar";
|
||||||
|
|
||||||
|
private getAuthHeader() {
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${AuthService.getToken()}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer les événements du calendrier
|
||||||
|
*/
|
||||||
|
async getEvents(limit: number = 10): Promise<CalendarEvent[]> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${this.baseURL}/events?limit=${limit}`, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des événements:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchroniser avec Google Calendar
|
||||||
|
*/
|
||||||
|
async sync(): Promise<{ syncedCount: number; events: number }> {
|
||||||
|
const response = await axios.post(`${this.baseURL}/sync`, {}, this.getAuthHeader());
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprimer un événement local
|
||||||
|
*/
|
||||||
|
async deleteEvent(eventId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await axios.delete(`${this.baseURL}/events/${eventId}`, this.getAuthHeader());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la suppression de l'événement:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new CalendarService();
|
||||||
@@ -5,6 +5,8 @@ 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_calendar_api_key?: string;
|
||||||
|
google_calendar_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsService {
|
class SettingsService {
|
||||||
|
|||||||
Reference in new issue
Block a user