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:
natreex
2025-11-19 19:10:12 -05:00
parent 71d13e2b12
commit dde4683c38
11 changed files with 287 additions and 361 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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>;

View File

@@ -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';
}
// ============================================================

View File

@@ -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}`);
}

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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": {

View File

@@ -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
View File

@@ -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",

View File

@@ -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",