Migrate from electron-store to OS-level secure storage (getSecureStorage)
- Replace `electron-store` with OS-level encrypted storage for secure token, userId, and language management in `LocalSystem` and `keyManager`. - Add `init-user` IPC handler to initialize user data and manage encryption keys. - Update login process to handle encrypted storage saving with fallback for macOS issues. - Add offline warning component to `login/page.tsx` to handle first-time sync requirements. - Remove `electron-store` and associated dependencies from `package.json` and `package-lock.json`.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import {useContext} from 'react';
|
||||
import {useContext, useEffect, useState} from 'react';
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEnvelope} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faEnvelope, faWifi, faCloudArrowUp} from "@fortawesome/free-solid-svg-icons";
|
||||
import LoginForm from "@/app/login/login/LoginForm";
|
||||
import SocialForm from "@/app/login/login/SocialForm";
|
||||
import {useTranslations} from "next-intl";
|
||||
@@ -11,13 +11,67 @@ import System from "@/lib/models/System";
|
||||
export default function LoginPage() {
|
||||
const t = useTranslations();
|
||||
const {lang, setLang} = useContext(LangContext);
|
||||
|
||||
const [showOfflineWarning, setShowOfflineWarning] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = lang === 'fr' ? 'en' : 'fr';
|
||||
setLang(newLang);
|
||||
System.setCookie('lang', newLang, 365);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function checkFirstConnectionAndNetwork() {
|
||||
// Check if we're in Electron
|
||||
if (!window.electron) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if token exists (first connection)
|
||||
const token = await window.electron.getToken();
|
||||
const hasToken = !!token;
|
||||
|
||||
// Check network status
|
||||
const online = navigator.onLine;
|
||||
setIsOnline(online);
|
||||
|
||||
// Show warning if first connection AND offline
|
||||
if (!hasToken && !online) {
|
||||
setShowOfflineWarning(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking first connection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
checkFirstConnectionAndNetwork();
|
||||
|
||||
// Listen for online/offline events
|
||||
const handleOnline = () => {
|
||||
setIsOnline(true);
|
||||
setShowOfflineWarning(false);
|
||||
};
|
||||
const handleOffline = async () => {
|
||||
setIsOnline(false);
|
||||
// Check if token exists
|
||||
if (window.electron) {
|
||||
const token = await window.electron.getToken();
|
||||
if (!token) {
|
||||
setShowOfflineWarning(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-background text-textPrimary p-4">
|
||||
<div className="w-full max-w-md px-4 py-10">
|
||||
@@ -32,6 +86,29 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Offline warning notification */}
|
||||
{showOfflineWarning && (
|
||||
<div className="mb-6 bg-gradient-to-r from-orange-500/20 to-amber-500/20 border border-orange-500/40 rounded-xl p-4 shadow-lg shadow-orange-500/10">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<div className="relative">
|
||||
<FontAwesomeIcon icon={faWifi} className="w-5 h-5 text-orange-400" />
|
||||
<div className="absolute inset-0 w-6 h-0.5 bg-orange-400 rotate-45 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-orange-200 mb-1 flex items-center gap-2">
|
||||
<FontAwesomeIcon icon={faCloudArrowUp} className="w-4 h-4" />
|
||||
{t('loginPage.offlineWarning.title')}
|
||||
</h3>
|
||||
<p className="text-sm text-orange-300/90 leading-relaxed">
|
||||
{t('loginPage.offlineWarning.message')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="bg-tertiary rounded-2xl overflow-hidden shadow-xl shadow-primary/10 w-full relative">
|
||||
<button
|
||||
|
||||
14
app/page.tsx
14
app/page.tsx
@@ -212,6 +212,20 @@ function ScribeContent() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize user in Electron (sets userId and creates/gets encryption key)
|
||||
if (window.electron && user.id) {
|
||||
try {
|
||||
const initResult = await window.electron.initUser(user.id);
|
||||
if (!initResult.success) {
|
||||
console.error('[Page] Failed to initialize user:', initResult.error);
|
||||
} else {
|
||||
console.log('[Page] User initialized successfully, key created:', initResult.keyCreated);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Page] Error initializing user:', error);
|
||||
}
|
||||
}
|
||||
setSession({
|
||||
isConnected: true,
|
||||
user: user,
|
||||
|
||||
5
electron.d.ts
vendored
5
electron.d.ts
vendored
@@ -23,9 +23,12 @@ export interface IElectronAPI {
|
||||
setLang: (lang: 'fr' | 'en') => Promise<void>;
|
||||
|
||||
// Auth events (one-way communication)
|
||||
loginSuccess: (token: string, userId: string) => void;
|
||||
loginSuccess: (token: string) => void;
|
||||
logout: () => void;
|
||||
|
||||
// User initialization (after getting user info from server)
|
||||
initUser: (userId: string) => Promise<{ success: boolean; keyCreated?: boolean; error?: string }>;
|
||||
|
||||
// Encryption key management (shortcuts for convenience)
|
||||
generateEncryptionKey: (userId: string) => Promise<string>;
|
||||
getUserEncryptionKey: (userId: string) => Promise<string | null>;
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
import type { IpcMainInvokeEvent } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { getSecureStorage } from '../storage/SecureStorage.js';
|
||||
|
||||
// ============================================================
|
||||
// SESSION MANAGEMENT - Auto-inject userId and lang
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Electron store instance for session management
|
||||
* - userId: Set during login via 'login-success' event
|
||||
* - userLang: Set via 'set-lang' handler
|
||||
*/
|
||||
const store = new Store({
|
||||
encryptionKey: 'eritors-scribe-secure-key'
|
||||
});
|
||||
|
||||
/**
|
||||
* Get userId from electron-store
|
||||
* Get userId from secure storage (OS-encrypted)
|
||||
* Set during login via 'login-success' event
|
||||
*/
|
||||
function getUserIdFromSession(): string | null {
|
||||
return store.get('userId', null) as string | null;
|
||||
const storage = getSecureStorage();
|
||||
return storage.get<string>('userId', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lang from electron-store
|
||||
* Get lang from secure storage
|
||||
* Set via 'set-lang' handler, defaults to 'fr'
|
||||
*/
|
||||
function getLangFromSession(): 'fr' | 'en' {
|
||||
return store.get('userLang', 'fr') as 'fr' | 'en';
|
||||
const storage = getSecureStorage();
|
||||
return storage.get<'fr' | 'en'>('userLang', 'fr') as 'fr' | 'en';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import Store from 'electron-store';
|
||||
import { getSecureStorage } from '../storage/SecureStorage.js';
|
||||
|
||||
/**
|
||||
* Key Manager - Manages user encryption keys stored in electron-store
|
||||
* Key Manager - Manages user encryption keys using OS-level secure storage
|
||||
* - macOS: Keychain
|
||||
* - Windows: DPAPI
|
||||
* - Linux: gnome-libsecret/kwallet
|
||||
*/
|
||||
|
||||
const store = new Store({
|
||||
encryptionKey: 'eritors-scribe-secure-key'
|
||||
});
|
||||
|
||||
/**
|
||||
* Get user encryption key from secure store
|
||||
* Get user encryption key from secure storage
|
||||
* @param userId - User ID
|
||||
* @returns User's encryption key or null if not found
|
||||
* @returns User's encryption key
|
||||
* @throws Error if encryption key not found
|
||||
*/
|
||||
export function getUserEncryptionKey(userId: string): string {
|
||||
const key: string | undefined = store.get(`encryptionKey-${userId}`) as string | undefined;
|
||||
if (key === undefined) {
|
||||
const storage = getSecureStorage();
|
||||
const key = storage.get<string>(`encryptionKey-${userId}`);
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error(`Unknown encryptionKey`);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user encryption key in secure store
|
||||
* Set user encryption key in secure storage (OS-encrypted)
|
||||
* @param userId - User ID
|
||||
* @param encryptionKey - Encryption key to store
|
||||
*/
|
||||
export function setUserEncryptionKey(userId: string, encryptionKey: string): void {
|
||||
store.set(`encryptionKey-${userId}`, encryptionKey);
|
||||
const storage = getSecureStorage();
|
||||
storage.set(`encryptionKey-${userId}`, encryptionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +38,8 @@ export function setUserEncryptionKey(userId: string, encryptionKey: string): voi
|
||||
* @returns True if key exists
|
||||
*/
|
||||
export function hasUserEncryptionKey(userId: string): boolean {
|
||||
return store.has(`encryptionKey-${userId}`);
|
||||
const storage = getSecureStorage();
|
||||
return storage.has(`encryptionKey-${userId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,5 +47,6 @@ export function hasUserEncryptionKey(userId: string): boolean {
|
||||
* @param userId - User ID
|
||||
*/
|
||||
export function deleteUserEncryptionKey(userId: string): void {
|
||||
store.delete(`encryptionKey-${userId}`);
|
||||
const storage = getSecureStorage();
|
||||
storage.delete(`encryptionKey-${userId}`);
|
||||
}
|
||||
|
||||
180
electron/main.ts
180
electron/main.ts
@@ -1,10 +1,10 @@
|
||||
import { app, BrowserWindow, ipcMain, nativeImage, protocol } from 'electron';
|
||||
import { app, BrowserWindow, ipcMain, nativeImage, protocol, safeStorage } from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import { fileURLToPath } from 'url';
|
||||
import Store from 'electron-store';
|
||||
import * as fs from 'fs';
|
||||
import { getDatabaseService } from './database/database.service.js';
|
||||
import { getSecureStorage } from './storage/SecureStorage.js';
|
||||
|
||||
// Import IPC handlers
|
||||
import './ipc/book.ipc.js';
|
||||
@@ -47,11 +47,6 @@ const iconPath = isDev
|
||||
? path.join(process.resourcesPath, 'icon.icns') // macOS utilise .icns
|
||||
: path.join(process.resourcesPath, 'app.asar/build/icon.png'); // Windows/Linux utilisent .png
|
||||
|
||||
// Store sécurisé pour le token
|
||||
const store = new Store({
|
||||
encryptionKey: 'eritors-scribe-secure-key' // En production, utiliser une clé générée
|
||||
});
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let loginWindow: BrowserWindow | null = null;
|
||||
|
||||
@@ -121,53 +116,169 @@ function createMainWindow(): void {
|
||||
});
|
||||
}
|
||||
|
||||
// IPC Handlers pour la gestion du token
|
||||
// IPC Handlers pour la gestion du token (OS-encrypted storage)
|
||||
ipcMain.handle('get-token', () => {
|
||||
return store.get('authToken', null);
|
||||
const storage = getSecureStorage();
|
||||
return storage.get('authToken', null);
|
||||
});
|
||||
|
||||
ipcMain.handle('set-token', (_event, token: string) => {
|
||||
store.set('authToken', token);
|
||||
const storage = getSecureStorage();
|
||||
storage.set('authToken', token);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('remove-token', () => {
|
||||
store.delete('authToken');
|
||||
const storage = getSecureStorage();
|
||||
storage.delete('authToken');
|
||||
return true;
|
||||
});
|
||||
|
||||
// IPC Handlers pour la gestion de la langue
|
||||
ipcMain.handle('get-lang', () => {
|
||||
return store.get('userLang', 'fr');
|
||||
const storage = getSecureStorage();
|
||||
return storage.get('userLang', 'fr');
|
||||
});
|
||||
|
||||
ipcMain.handle('set-lang', (_event, lang: 'fr' | 'en') => {
|
||||
store.set('userLang', lang);
|
||||
const storage = getSecureStorage();
|
||||
storage.set('userLang', lang);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.on('login-success', (_event, token: string, userId: string) => {
|
||||
store.set('authToken', token);
|
||||
store.set('userId', userId);
|
||||
// IPC Handler pour initialiser l'utilisateur après récupération depuis le serveur
|
||||
ipcMain.handle('init-user', async (_event, userId: string) => {
|
||||
console.log('[InitUser] Initializing user:', userId);
|
||||
|
||||
const storage = getSecureStorage();
|
||||
storage.set('userId', userId);
|
||||
|
||||
try {
|
||||
const { getUserEncryptionKey, setUserEncryptionKey, hasUserEncryptionKey } = await import('./database/keyManager.js');
|
||||
|
||||
let encryptionKey: string | null = null;
|
||||
|
||||
if (!hasUserEncryptionKey(userId)) {
|
||||
const { generateUserEncryptionKey } = await import('./database/encryption.js');
|
||||
encryptionKey = generateUserEncryptionKey(userId);
|
||||
|
||||
console.log('[InitUser] Generated new encryption key for user');
|
||||
console.log('[InitUser] Key generated:', encryptionKey ? `${encryptionKey.substring(0, 10)}...` : 'UNDEFINED');
|
||||
|
||||
if (!encryptionKey) {
|
||||
console.error('[InitUser] CRITICAL: Generated key is undefined, blocking operation');
|
||||
throw new Error('Failed to generate encryption key');
|
||||
}
|
||||
|
||||
setUserEncryptionKey(userId, encryptionKey);
|
||||
|
||||
// Verify the key was saved
|
||||
const savedKey = getUserEncryptionKey(userId);
|
||||
console.log('[InitUser] Key verification after save:', savedKey ? `${savedKey.substring(0, 10)}...` : 'UNDEFINED');
|
||||
|
||||
if (!savedKey) {
|
||||
console.error('[InitUser] CRITICAL: Key was not saved correctly, blocking operation');
|
||||
throw new Error('Failed to save encryption key');
|
||||
}
|
||||
} else {
|
||||
encryptionKey = getUserEncryptionKey(userId);
|
||||
console.log('[InitUser] Using existing encryption key:', encryptionKey ? `${encryptionKey.substring(0, 10)}...` : 'UNDEFINED');
|
||||
|
||||
if (!encryptionKey) {
|
||||
console.error('[InitUser] CRITICAL: Existing key is undefined, regenerating');
|
||||
const { generateUserEncryptionKey } = await import('./database/encryption.js');
|
||||
encryptionKey = generateUserEncryptionKey(userId);
|
||||
setUserEncryptionKey(userId, encryptionKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Save userId to disk now that we have everything
|
||||
// This is the ONLY additional save after login
|
||||
if (safeStorage.isEncryptionAvailable()) {
|
||||
storage.save();
|
||||
console.log('[InitUser] User ID saved to disk (encrypted)');
|
||||
} else {
|
||||
console.error('[InitUser] WARNING: Cannot save user ID - encryption not available');
|
||||
}
|
||||
|
||||
return { success: true, keyCreated: !hasUserEncryptionKey(userId) };
|
||||
} catch (error) {
|
||||
console.error('[InitUser] Error managing encryption key:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('login-success', async (_event, token: string) => {
|
||||
const storage = getSecureStorage();
|
||||
storage.set('authToken', token);
|
||||
// Note: userId will be set later when we get user info from server
|
||||
|
||||
if (loginWindow) {
|
||||
loginWindow.close();
|
||||
}
|
||||
|
||||
createMainWindow();
|
||||
|
||||
// Save AFTER mainWindow is created (fixes macOS safeStorage issue)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
if (safeStorage.isEncryptionAvailable()) {
|
||||
storage.save();
|
||||
console.log('[Login] Auth token saved to disk (encrypted)');
|
||||
} else {
|
||||
console.error('[Login] Encryption still not available after window creation');
|
||||
// Try one more time after another delay
|
||||
setTimeout(() => {
|
||||
if (safeStorage.isEncryptionAvailable()) {
|
||||
storage.save();
|
||||
console.log('[Login] Auth token saved to disk (encrypted) - second attempt');
|
||||
} else {
|
||||
console.error('[Login] CRITICAL: Cannot encrypt credentials');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Login] Error saving auth data:', error);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
|
||||
ipcMain.on('logout', () => {
|
||||
store.delete('authToken');
|
||||
store.delete('userId');
|
||||
store.delete('userLang');
|
||||
try {
|
||||
const storage = getSecureStorage();
|
||||
|
||||
// Close database connection
|
||||
const db = getDatabaseService();
|
||||
db.close();
|
||||
// Debug: Check what's in storage before deletion
|
||||
console.log('[Logout] Before deletion - authToken exists:', storage.has('authToken'));
|
||||
console.log('[Logout] Before deletion - userId exists:', storage.has('userId'));
|
||||
|
||||
storage.delete('authToken');
|
||||
storage.delete('userId');
|
||||
storage.delete('userLang');
|
||||
|
||||
// Debug: Check what's in storage after deletion
|
||||
console.log('[Logout] After deletion - authToken exists:', storage.has('authToken'));
|
||||
console.log('[Logout] After deletion - userId exists:', storage.has('userId'));
|
||||
|
||||
// IMPORTANT: Save to disk to persist the deletions
|
||||
storage.save();
|
||||
console.log('[Logout] Cleared auth data from disk');
|
||||
} catch (error) {
|
||||
console.error('[Logout] Error clearing storage:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
const db = getDatabaseService();
|
||||
db.close();
|
||||
} catch (error) {
|
||||
console.error('[Logout] Error closing database:', error);
|
||||
}
|
||||
|
||||
if (mainWindow) {
|
||||
mainWindow.close();
|
||||
mainWindow = null;
|
||||
}
|
||||
|
||||
createLoginWindow();
|
||||
@@ -240,18 +351,20 @@ ipcMain.handle('generate-encryption-key', async (_event, userId: string) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Get or generate user encryption key
|
||||
* Get or generate user encryption key (OS-encrypted storage)
|
||||
*/
|
||||
ipcMain.handle('get-user-encryption-key', (_event, userId: string) => {
|
||||
const key = store.get(`encryptionKey-${userId}`, null);
|
||||
const storage = getSecureStorage();
|
||||
const key = storage.get(`encryptionKey-${userId}`, null);
|
||||
return key;
|
||||
});
|
||||
|
||||
/**
|
||||
* Store user encryption key
|
||||
* Store user encryption key (OS-encrypted storage)
|
||||
*/
|
||||
ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => {
|
||||
store.set(`encryptionKey-${userId}`, encryptionKey);
|
||||
const storage = getSecureStorage();
|
||||
storage.set(`encryptionKey-${userId}`, encryptionKey);
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -324,9 +437,15 @@ app.whenReady().then(() => {
|
||||
app.dock.setIcon(icon);
|
||||
}
|
||||
|
||||
// Vérifier si un token existe
|
||||
const token = store.get('authToken');
|
||||
console.log('Token exists:', !!token);
|
||||
// Vérifier si un token existe (OS-encrypted storage)
|
||||
const storage = getSecureStorage();
|
||||
const token = storage.get('authToken');
|
||||
const userId = storage.get('userId');
|
||||
console.log('[Startup] Token exists:', !!token);
|
||||
console.log('[Startup] UserId exists:', !!userId);
|
||||
if (token) {
|
||||
console.log('[Startup] Token value:', token.substring(0, 20) + '...');
|
||||
}
|
||||
|
||||
if (token) {
|
||||
// Token existe, ouvrir la fenêtre principale
|
||||
@@ -338,7 +457,8 @@ app.whenReady().then(() => {
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
const token = store.get('authToken');
|
||||
const storage = getSecureStorage();
|
||||
const token = storage.get('authToken');
|
||||
if (token) {
|
||||
createMainWindow();
|
||||
} else {
|
||||
|
||||
@@ -21,9 +21,12 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
setLang: (lang: 'fr' | 'en') => ipcRenderer.invoke('set-lang', lang),
|
||||
|
||||
// Auth events (use send for one-way communication)
|
||||
loginSuccess: (token: string, userId: string) => ipcRenderer.send('login-success', token, userId),
|
||||
loginSuccess: (token: string) => ipcRenderer.send('login-success', token),
|
||||
logout: () => ipcRenderer.send('logout'),
|
||||
|
||||
// User initialization (after getting user info from server)
|
||||
initUser: (userId: string) => ipcRenderer.invoke('init-user', userId),
|
||||
|
||||
// Encryption key management (shortcuts for convenience)
|
||||
generateEncryptionKey: (userId: string) => ipcRenderer.invoke('generate-encryption-key', userId),
|
||||
getUserEncryptionKey: (userId: string) => ipcRenderer.invoke('get-user-encryption-key', userId),
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"orSocial": "or continue with",
|
||||
"noAccount": "Don't have an account yet?",
|
||||
"createAccount": "Create one here",
|
||||
"backToLogin": "Back to login"
|
||||
"backToLogin": "Back to login",
|
||||
"offlineWarning": {
|
||||
"title": "First sync required",
|
||||
"message": "An Internet connection is required for your first login to sync your data."
|
||||
}
|
||||
},
|
||||
"loginForm": {
|
||||
"error": {
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"orSocial": "ou continuez avec",
|
||||
"noAccount": "Pas encore de compte?",
|
||||
"createAccount": "Créez-en un ici",
|
||||
"backToLogin": "Retour à la connexion"
|
||||
"backToLogin": "Retour à la connexion",
|
||||
"offlineWarning": {
|
||||
"title": "Première synchronisation requise",
|
||||
"message": "Une connexion Internet est nécessaire pour votre première connexion afin de synchroniser vos données."
|
||||
}
|
||||
},
|
||||
"loginForm": {
|
||||
"error": {
|
||||
|
||||
294
package-lock.json
generated
294
package-lock.json
generated
@@ -26,7 +26,6 @@
|
||||
"antd": "^5.28.1",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"axios": "^1.13.2",
|
||||
"electron-store": "^11.0.2",
|
||||
"i18next": "^25.6.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"next": "^16.0.3",
|
||||
@@ -42,7 +41,6 @@
|
||||
"tailwindcss": "^4.1.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/electron-store": "^1.3.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.10.1",
|
||||
@@ -4245,16 +4243,6 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/electron-store": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/electron-store/-/electron-store-1.3.1.tgz",
|
||||
"integrity": "sha512-RvEAlIWcy7ATEMeyw481SdnuceN6Pd2Qh5KSW5NohwtY1t1uP0MmC3Cvoszd+ueGLqTKCpRwhCJY4qdER5QQVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -4516,45 +4504,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
||||
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
@@ -4846,16 +4795,6 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/atomically": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz",
|
||||
"integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"stubborn-fs": "^2.0.0",
|
||||
"when-exit": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.22",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
|
||||
@@ -5579,75 +5518,6 @@
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/conf": {
|
||||
"version": "15.0.2",
|
||||
"resolved": "https://registry.npmjs.org/conf/-/conf-15.0.2.tgz",
|
||||
"integrity": "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"atomically": "^2.0.3",
|
||||
"debounce-fn": "^6.0.0",
|
||||
"dot-prop": "^10.0.0",
|
||||
"env-paths": "^3.0.0",
|
||||
"json-schema-typed": "^8.0.1",
|
||||
"semver": "^7.7.2",
|
||||
"uint8array-extras": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/conf/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/conf/node_modules/env-paths": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
|
||||
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/conf/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/conf/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/config-file-ts": {
|
||||
"version": "0.2.8-rc1",
|
||||
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz",
|
||||
@@ -5833,21 +5703,6 @@
|
||||
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debounce-fn": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz",
|
||||
"integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-function": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -6101,36 +5956,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-prop": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz",
|
||||
"integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"type-fest": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-prop/node_modules/type-fest": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.2.0.tgz",
|
||||
"integrity": "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"dependencies": {
|
||||
"tagged-tag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -6439,37 +6264,6 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-store": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/electron-store/-/electron-store-11.0.2.tgz",
|
||||
"integrity": "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"conf": "^15.0.2",
|
||||
"type-fest": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-store/node_modules/type-fest": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.2.0.tgz",
|
||||
"integrity": "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"dependencies": {
|
||||
"tagged-tag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.254",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz",
|
||||
@@ -6811,22 +6605,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
@@ -7849,12 +7627,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-typed": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz",
|
||||
"integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
@@ -8474,18 +8246,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-function": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
@@ -10442,15 +10202,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resedit": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz",
|
||||
@@ -11102,21 +10853,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/stubborn-fs": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz",
|
||||
"integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"stubborn-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/stubborn-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz",
|
||||
"integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
|
||||
@@ -11184,18 +10920,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tagged-tag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
||||
@@ -11501,18 +11225,6 @@
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uint8array-extras": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
|
||||
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
@@ -11805,12 +11517,6 @@
|
||||
"defaults": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/when-exit": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz",
|
||||
"integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@types/electron-store": "^1.3.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.10.1",
|
||||
@@ -52,7 +51,6 @@
|
||||
"antd": "^5.28.1",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"axios": "^1.13.2",
|
||||
"electron-store": "^11.0.2",
|
||||
"i18next": "^25.6.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"next": "^16.0.3",
|
||||
|
||||
Reference in New Issue
Block a user