Integrate offline support and improve error handling across app
- Add `OfflineContext` to manage offline state and interactions within components. - Refactor session logic in `ScribeControllerBar` and `page.tsx` to handle offline scenarios (e.g., check connectivity before enabling GPT features). - Enhance offline PIN setup and verification with better flow and error messaging. - Optimize database IPC handlers to initialize and sync data in offline mode. - Refactor code to clean up redundant logs and ensure stricter typings. - Improve consistency and structure in handling online and offline operations for smoother user experience.
This commit is contained in:
94
app/page.tsx
94
app/page.tsx
@@ -14,7 +14,6 @@ import {SessionContext} from '@/context/SessionContext';
|
|||||||
import {SessionProps} from "@/lib/models/Session";
|
import {SessionProps} from "@/lib/models/Session";
|
||||||
import User, {UserProps} from "@/lib/models/User";
|
import User, {UserProps} from "@/lib/models/User";
|
||||||
import {BookProps} from "@/lib/models/Book";
|
import {BookProps} from "@/lib/models/Book";
|
||||||
// Removed Next.js router imports for Electron
|
|
||||||
import ScribeTopBar from "@/components/ScribeTopBar";
|
import ScribeTopBar from "@/components/ScribeTopBar";
|
||||||
import ScribeControllerBar from "@/components/ScribeControllerBar";
|
import ScribeControllerBar from "@/components/ScribeControllerBar";
|
||||||
import ScribeLeftBar from "@/components/leftbar/ScribeLeftBar";
|
import ScribeLeftBar from "@/components/leftbar/ScribeLeftBar";
|
||||||
@@ -27,7 +26,6 @@ import {faBookMedical, faFeather} from "@fortawesome/free-solid-svg-icons";
|
|||||||
import TermsOfUse from "@/components/TermsOfUse";
|
import TermsOfUse from "@/components/TermsOfUse";
|
||||||
import frMessages from '@/lib/locales/fr.json';
|
import frMessages from '@/lib/locales/fr.json';
|
||||||
import enMessages from '@/lib/locales/en.json';
|
import enMessages from '@/lib/locales/en.json';
|
||||||
// Removed Next.js Image import for Electron
|
|
||||||
import {NextIntlClientProvider, useTranslations} from "next-intl";
|
import {NextIntlClientProvider, useTranslations} from "next-intl";
|
||||||
import {LangContext} from "@/context/LangContext";
|
import {LangContext} from "@/context/LangContext";
|
||||||
import {AIUsageContext} from "@/context/AIUsageContext";
|
import {AIUsageContext} from "@/context/AIUsageContext";
|
||||||
@@ -45,7 +43,7 @@ function ScribeContent() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const {lang: locale} = useContext(LangContext);
|
const {lang: locale} = useContext(LangContext);
|
||||||
const {errorMessage} = useContext(AlertContext);
|
const {errorMessage} = useContext(AlertContext);
|
||||||
const {initializeDatabase} = useContext(OfflineContext);
|
const {initializeDatabase, setOfflineMode, isCurrentlyOffline} = useContext(OfflineContext);
|
||||||
const editor: Editor | null = useEditor({
|
const editor: Editor | null = useEditor({
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit,
|
||||||
@@ -57,8 +55,7 @@ function ScribeContent() {
|
|||||||
injectCSS: false,
|
injectCSS: false,
|
||||||
immediatelyRender: false,
|
immediatelyRender: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Router removed for Electron - using window.location instead
|
|
||||||
const [session, setSession] = useState<SessionProps>({user: null, accessToken: '', isConnected: false});
|
const [session, setSession] = useState<SessionProps>({user: null, accessToken: '', isConnected: false});
|
||||||
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
|
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
|
||||||
const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
|
const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
|
||||||
@@ -68,8 +65,6 @@ function ScribeContent() {
|
|||||||
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const [sessionAttempts, setSessionAttempts] = useState<number>(0)
|
|
||||||
|
|
||||||
const [isTermsAccepted, setIsTermsAccepted] = useState<boolean>(false);
|
const [isTermsAccepted, setIsTermsAccepted] = useState<boolean>(false);
|
||||||
const [homeStepsGuide, setHomeStepsGuide] = useState<boolean>(false);
|
const [homeStepsGuide, setHomeStepsGuide] = useState<boolean>(false);
|
||||||
const [showPinSetup, setShowPinSetup] = useState<boolean>(false);
|
const [showPinSetup, setShowPinSetup] = useState<boolean>(false);
|
||||||
@@ -151,12 +146,7 @@ function ScribeContent() {
|
|||||||
setIsTermsAccepted(session.user?.termsAccepted ?? false);
|
setIsTermsAccepted(session.user?.termsAccepted ?? false);
|
||||||
setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic'));
|
setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic'));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} else {
|
|
||||||
if (sessionAttempts > 2) {
|
|
||||||
// Redirect handled by checkAuthentification
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setSessionAttempts(sessionAttempts + 1);
|
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
useEffect((): void => {
|
useEffect((): void => {
|
||||||
@@ -166,54 +156,50 @@ function ScribeContent() {
|
|||||||
}, [currentBook]);
|
}, [currentBook]);
|
||||||
|
|
||||||
// Check for PIN setup after successful connection
|
// Check for PIN setup after successful connection
|
||||||
useEffect(() => {
|
useEffect(():void => {
|
||||||
async function checkPinSetup() {
|
async function checkPinSetup() {
|
||||||
if (session.isConnected && window.electron) {
|
if (session.isConnected && window.electron) {
|
||||||
try {
|
try {
|
||||||
const offlineStatus = await window.electron.offlineModeGet();
|
const offlineStatus = await window.electron.offlineModeGet();
|
||||||
console.log('[Page] Session connected, offline status:', offlineStatus);
|
|
||||||
|
|
||||||
if (!offlineStatus.hasPin) {
|
if (!offlineStatus.hasPin) {
|
||||||
console.log('[Page] No PIN configured, will show setup dialog');
|
setTimeout(():void => {
|
||||||
// Show PIN setup dialog after a short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[Page] Showing PIN setup dialog');
|
console.log('[Page] Showing PIN setup dialog');
|
||||||
setShowPinSetup(true);
|
setShowPinSetup(true);
|
||||||
}, 2000); // 2 seconds delay after page load
|
}, 2000);
|
||||||
|
}
|
||||||
|
} catch (e:unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
} else {
|
||||||
|
errorMessage('Unknown error occurred while checking offline mode')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('[Page] Error checking offline mode:', error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPinSetup();
|
checkPinSetup().then();
|
||||||
}, [session.isConnected]); // Run when session connection status changes
|
}, [session.isConnected]);
|
||||||
|
|
||||||
async function handlePinVerifySuccess(userId: string): Promise<void> {
|
async function handlePinVerifySuccess(userId: string): Promise<void> {
|
||||||
console.log('[OfflinePin] PIN verified successfully for user:', userId);
|
console.log('[OfflinePin] PIN verified successfully for user:', userId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Initialize database with user's encryption key
|
|
||||||
if (window.electron) {
|
if (window.electron) {
|
||||||
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
|
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
|
||||||
if (encryptionKey) {
|
if (encryptionKey) {
|
||||||
await window.electron.dbInitialize(userId, encryptionKey);
|
await window.electron.dbInitialize(userId, encryptionKey);
|
||||||
|
|
||||||
// Load user from local DB
|
const localUser:UserProps = await window.electron.invoke('db:user:info');
|
||||||
const localUser = await window.electron.invoke('db:user:info');
|
if (localUser && localUser.id) {
|
||||||
if (localUser && localUser.success) {
|
|
||||||
// Use local data and continue in offline mode
|
|
||||||
setSession({
|
setSession({
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
user: localUser.data,
|
user: localUser,
|
||||||
accessToken: 'offline', // Special offline token
|
accessToken: 'offline', // Special offline token
|
||||||
});
|
});
|
||||||
setShowPinVerify(false);
|
setShowPinVerify(false);
|
||||||
setCurrentCredits(localUser.data.creditsBalance || 0);
|
setCurrentCredits(localUser.creditsBalance || 0);
|
||||||
setAmountSpent(localUser.data.aiUsage || 0);
|
setAmountSpent(localUser.aiUsage || 0);
|
||||||
|
|
||||||
console.log('[OfflinePin] Running in offline mode');
|
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t("homePage.errors.localDataError"));
|
errorMessage(t("homePage.errors.localDataError"));
|
||||||
if (window.electron) {
|
if (window.electron) {
|
||||||
@@ -282,29 +268,18 @@ function ScribeContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('user: ' , user);
|
|
||||||
|
|
||||||
// Initialize user in Electron (sets userId and creates/gets encryption key)
|
|
||||||
if (window.electron && user.id) {
|
if (window.electron && user.id) {
|
||||||
try {
|
try {
|
||||||
const initResult = await window.electron.initUser(user.id);
|
const initResult = await window.electron.initUser(user.id);
|
||||||
if (!initResult.success) {
|
if (!initResult.success) {
|
||||||
console.error('[Page] Failed to initialize user:', initResult.error);
|
console.error('[Page] Failed to initialize user:', initResult.error);
|
||||||
} else {
|
} else {
|
||||||
console.log('[Page] User initialized successfully, key created:', initResult.keyCreated);
|
|
||||||
|
|
||||||
// Check if PIN is configured for offline mode
|
|
||||||
try {
|
try {
|
||||||
const offlineStatus = await window.electron.offlineModeGet();
|
const offlineStatus = await window.electron.offlineModeGet();
|
||||||
console.log('[Page] Offline status:', offlineStatus);
|
|
||||||
if (!offlineStatus.hasPin) {
|
if (!offlineStatus.hasPin) {
|
||||||
// First login or no PIN configured yet
|
setTimeout(():void => {
|
||||||
// Show PIN setup dialog after a short delay
|
|
||||||
console.log('[Page] No PIN configured, will show setup dialog');
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('[Page] Showing PIN setup dialog');
|
|
||||||
setShowPinSetup(true);
|
setShowPinSetup(true);
|
||||||
}, 2000); // 2 seconds delay after successful login
|
}, 2000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Page] Error checking offline mode:', error);
|
console.error('[Page] Error checking offline mode:', error);
|
||||||
@@ -321,15 +296,10 @@ function ScribeContent() {
|
|||||||
});
|
});
|
||||||
setCurrentCredits(user.creditsBalance)
|
setCurrentCredits(user.creditsBalance)
|
||||||
setAmountSpent(user.aiUsage)
|
setAmountSpent(user.aiUsage)
|
||||||
|
|
||||||
// Initialiser la DB locale en Electron
|
|
||||||
if (window.electron && user.id) {
|
if (window.electron && user.id) {
|
||||||
try {
|
try {
|
||||||
const dbInitialized = await initializeDatabase(user.id);
|
const dbInitialized:boolean = await initializeDatabase(user.id);
|
||||||
if (dbInitialized) {
|
if (dbInitialized) {
|
||||||
console.log('Database initialized successfully');
|
|
||||||
|
|
||||||
// Sync user to local DB (only if not exists)
|
|
||||||
try {
|
try {
|
||||||
await window.electron.invoke('db:user:sync', {
|
await window.electron.invoke('db:user:sync', {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -341,7 +311,6 @@ function ScribeContent() {
|
|||||||
console.log('User synced to local DB');
|
console.log('User synced to local DB');
|
||||||
} catch (syncError) {
|
} catch (syncError) {
|
||||||
console.error('Failed to sync user to local DB:', syncError);
|
console.error('Failed to sync user to local DB:', syncError);
|
||||||
// Non-blocking error, continue anyway
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -349,13 +318,12 @@ function ScribeContent() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
console.log('[Auth] Server error, checking offline mode...');
|
|
||||||
if (window.electron) {
|
if (window.electron) {
|
||||||
try {
|
try {
|
||||||
const offlineStatus = await window.electron.offlineModeGet();
|
const offlineStatus = await window.electron.offlineModeGet();
|
||||||
|
|
||||||
if (offlineStatus.hasPin && offlineStatus.lastUserId) {
|
if (offlineStatus.hasPin && offlineStatus.lastUserId) {
|
||||||
console.log('[Auth] Server unreachable but PIN configured, showing PIN verification');
|
setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false}));
|
||||||
setShowPinVerify(true);
|
setShowPinVerify(true);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -369,8 +337,7 @@ function ScribeContent() {
|
|||||||
console.error('[Auth] Error checking offline mode:', offlineError);
|
console.error('[Auth] Error checking offline mode:', offlineError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not in offline mode or no PIN configured, show error and logout
|
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(e.message);
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
@@ -383,7 +350,7 @@ function ScribeContent() {
|
|||||||
const offlineStatus = await window.electron.offlineModeGet();
|
const offlineStatus = await window.electron.offlineModeGet();
|
||||||
|
|
||||||
if (offlineStatus.hasPin && offlineStatus.lastUserId) {
|
if (offlineStatus.hasPin && offlineStatus.lastUserId) {
|
||||||
console.log('[Auth] No token but PIN configured, showing PIN verification for offline mode');
|
setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false}));
|
||||||
setShowPinVerify(true);
|
setShowPinVerify(true);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
@@ -428,7 +395,12 @@ function ScribeContent() {
|
|||||||
async function getLastChapter(): Promise<void> {
|
async function getLastChapter(): Promise<void> {
|
||||||
if (session?.accessToken) {
|
if (session?.accessToken) {
|
||||||
try {
|
try {
|
||||||
const response: ChapterProps | null = await System.authGetQueryToServer<ChapterProps | null>(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
|
let response: ChapterProps | null
|
||||||
|
if (isCurrentlyOffline()){
|
||||||
|
response = await window.electron.invoke('db:chapter:last', currentBook?.bookId)
|
||||||
|
} else {
|
||||||
|
response = await System.authGetQueryToServer<ChapterProps | null>(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
|
||||||
|
}
|
||||||
if (response) {
|
if (response) {
|
||||||
setCurrentChapter(response)
|
setCurrentChapter(response)
|
||||||
} else {
|
} else {
|
||||||
@@ -489,12 +461,12 @@ function ScribeContent() {
|
|||||||
</EditorContext.Provider>
|
</EditorContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
homeStepsGuide &&
|
homeStepsGuide && !isCurrentlyOffline() &&
|
||||||
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
|
<GuideTour stepId={0} steps={homeSteps} onComplete={handleHomeTour}
|
||||||
onClose={(): void => setHomeStepsGuide(false)}/>
|
onClose={(): void => setHomeStepsGuide(false)}/>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!isTermsAccepted && <TermsOfUse onAccept={handleTermsAcceptance}/>
|
!isTermsAccepted && !isCurrentlyOffline() && <TermsOfUse onAccept={handleTermsAcceptance}/>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
showPinSetup && window.electron && (
|
showPinSetup && window.electron && (
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {useTranslations} from "next-intl";
|
|||||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||||
import CreditCounter from "@/components/CreditMeters";
|
import CreditCounter from "@/components/CreditMeters";
|
||||||
import QuillSense from "@/lib/models/QuillSense";
|
import QuillSense from "@/lib/models/QuillSense";
|
||||||
|
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||||
|
|
||||||
export default function ScribeControllerBar() {
|
export default function ScribeControllerBar() {
|
||||||
const {chapter, setChapter} = useContext(ChapterContext);
|
const {chapter, setChapter} = useContext(ChapterContext);
|
||||||
@@ -24,12 +25,13 @@ export default function ScribeControllerBar() {
|
|||||||
const {errorMessage} = useContext(AlertContext)
|
const {errorMessage} = useContext(AlertContext)
|
||||||
const {session} = useContext(SessionContext);
|
const {session} = useContext(SessionContext);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const {lang, setLang} = useContext<LangContextProps>(LangContext)
|
const {lang, setLang} = useContext<LangContextProps>(LangContext);
|
||||||
|
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext)
|
||||||
|
|
||||||
const isGPTEnabled: boolean = QuillSense.isOpenAIEnabled(session);
|
const isGPTEnabled: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
|
||||||
const isGemini: boolean = QuillSense.isOpenAIEnabled(session);
|
const isGemini: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
|
||||||
const isAnthropic: boolean = QuillSense.isOpenAIEnabled(session);
|
const isAnthropic: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
|
||||||
const isSubTierTwo: boolean = QuillSense.getSubLevel(session) >= 2;
|
const isSubTierTwo: boolean = !isCurrentlyOffline() && QuillSense.getSubLevel(session) >= 2;
|
||||||
const hasAccess: boolean = (isGPTEnabled || isAnthropic || isGemini) || isSubTierTwo;
|
const hasAccess: boolean = (isGPTEnabled || isAnthropic || isGemini) || isSubTierTwo;
|
||||||
|
|
||||||
const [showSettingPanel, setShowSettingPanel] = useState<boolean>(false);
|
const [showSettingPanel, setShowSettingPanel] = useState<boolean>(false);
|
||||||
|
|||||||
@@ -146,13 +146,14 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
|||||||
publicationDate: publicationDate,
|
publicationDate: publicationDate,
|
||||||
desiredWordCount: wordCount,
|
desiredWordCount: wordCount,
|
||||||
}, token, lang);
|
}, token, lang);
|
||||||
if (!bookId) {
|
|
||||||
throw new Error(t('addNewBookForm.error.addingBook'));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Offline - call local database
|
|
||||||
bookId = await window.electron.invoke<string>('db:book:create', bookData);
|
bookId = await window.electron.invoke<string>('db:book:create', bookData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!bookId) {
|
||||||
|
errorMessage(t('addNewBookForm.error.addingBook'))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const book: BookProps = {
|
const book: BookProps = {
|
||||||
bookId: bookId,
|
bookId: bookId,
|
||||||
|
|||||||
@@ -172,12 +172,14 @@ export default function BookList() {
|
|||||||
|
|
||||||
async function getBook(bookId: string): Promise<void> {
|
async function getBook(bookId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const bookResponse: BookListProps = await System.authGetQueryToServer<BookListProps>(
|
let bookResponse: BookListProps|null = null;
|
||||||
`book/basic-information`,
|
if (isCurrentlyOffline()){
|
||||||
accessToken,
|
bookResponse = await window.electron.invoke('db:book:bookBasicInformation', bookId)
|
||||||
lang,
|
} else {
|
||||||
{id: bookId}
|
bookResponse = await System.authGetQueryToServer<BookListProps>(`book/basic-information`, accessToken, lang, {
|
||||||
);
|
id: bookId
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!bookResponse) {
|
if (!bookResponse) {
|
||||||
errorMessage(t("bookList.errorBookDetails"));
|
errorMessage(t("bookList.errorBookDetails"));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -398,7 +398,6 @@ export default function TextEditor() {
|
|||||||
const parsedContent = JSON.parse(chapter.chapterContent.content);
|
const parsedContent = JSON.parse(chapter.chapterContent.content);
|
||||||
editor.commands.setContent(parsedContent);
|
editor.commands.setContent(parsedContent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur lors du parsing du contenu:', error);
|
|
||||||
editor.commands.setContent({
|
editor.commands.setContent({
|
||||||
type: "doc",
|
type: "doc",
|
||||||
content: [{type: "paragraph", content: []}]
|
content: [{type: "paragraph", content: []}]
|
||||||
|
|||||||
2
electron.d.ts
vendored
2
electron.d.ts
vendored
@@ -11,7 +11,7 @@ export interface IElectronAPI {
|
|||||||
platform: NodeJS.Platform;
|
platform: NodeJS.Platform;
|
||||||
|
|
||||||
// Generic invoke method - use this for all IPC calls
|
// Generic invoke method - use this for all IPC calls
|
||||||
invoke: <T = any>(channel: string, ...args: any[]) => Promise<T>;
|
invoke: <T>(channel: string, ...args: any[]) => Promise<T>;
|
||||||
|
|
||||||
// Token management (shortcuts for convenience)
|
// Token management (shortcuts for convenience)
|
||||||
getToken: () => Promise<string | null>;
|
getToken: () => Promise<string | null>;
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export default class BookRepo {
|
|||||||
let result:RunResult
|
let result:RunResult
|
||||||
try {
|
try {
|
||||||
const db: Database = System.getDb();
|
const db: Database = System.getDb();
|
||||||
result = db.run('INSERT INTO erit_books (book_id,type,author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, erit_books.desired_word_count) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [bookId, type, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, serie, publicationDate ? publicationDate : null, desiredWordCount]);
|
result = db.run('INSERT INTO erit_books (book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [bookId, type, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, serie, publicationDate ? publicationDate : null, desiredWordCount]);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
console.error(`DB Error: ${err.message}`);
|
console.error(`DB Error: ${err.message}`);
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ ipcMain.handle('db:book:books', createHandler<void, BookProps[]>(
|
|||||||
// GET /book/:id - Get single book
|
// GET /book/:id - Get single book
|
||||||
ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>(
|
ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>(
|
||||||
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<BookProps> {
|
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<BookProps> {
|
||||||
return await Book.getBook(bookId, userId);
|
return await Book.getBook(userId, bookId);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
|
|||||||
import { createHandler } from '../database/LocalSystem.js';
|
import { createHandler } from '../database/LocalSystem.js';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { getSecureStorage } from '../storage/SecureStorage.js';
|
import { getSecureStorage } from '../storage/SecureStorage.js';
|
||||||
|
import { getDatabaseService } from '../database/database.service.js';
|
||||||
|
|
||||||
interface SetPinData {
|
interface SetPinData {
|
||||||
pin: string;
|
pin: string;
|
||||||
@@ -62,6 +63,17 @@ ipcMain.handle('offline:pin:verify', async (_event, data: VerifyPinData) => {
|
|||||||
if (isValid) {
|
if (isValid) {
|
||||||
// Set userId for session
|
// Set userId for session
|
||||||
storage.set('userId', lastUserId);
|
storage.set('userId', lastUserId);
|
||||||
|
|
||||||
|
// Initialize database for offline use
|
||||||
|
const encryptionKey = storage.get<string>(`encryptionKey-${lastUserId}`);
|
||||||
|
if (encryptionKey) {
|
||||||
|
const db = getDatabaseService();
|
||||||
|
db.initialize(lastUserId, encryptionKey);
|
||||||
|
} else {
|
||||||
|
console.error('[Offline] No encryption key found for user');
|
||||||
|
return { success: false, error: 'No encryption key found' };
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[Offline] PIN verified, user authenticated locally');
|
console.log('[Offline] PIN verified, user authenticated locally');
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
106
electron/main.ts
106
electron/main.ts
@@ -1,12 +1,11 @@
|
|||||||
import { app, BrowserWindow, ipcMain, nativeImage, protocol, safeStorage } from 'electron';
|
import {app, BrowserWindow, ipcMain, nativeImage, protocol, safeStorage} from 'electron';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as url from 'url';
|
import {fileURLToPath} from 'url';
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { getDatabaseService } from './database/database.service.js';
|
import {DatabaseService, getDatabaseService} from './database/database.service.js';
|
||||||
import { getSecureStorage } from './storage/SecureStorage.js';
|
import SecureStorage, {getSecureStorage} from './storage/SecureStorage.js';
|
||||||
import { getUserEncryptionKey, setUserEncryptionKey, hasUserEncryptionKey } from './database/keyManager.js';
|
import {getUserEncryptionKey, hasUserEncryptionKey, setUserEncryptionKey} from './database/keyManager.js';
|
||||||
import { generateUserEncryptionKey } from './database/encryption.js';
|
import {generateUserEncryptionKey} from './database/encryption.js';
|
||||||
|
|
||||||
// Import IPC handlers
|
// Import IPC handlers
|
||||||
import './ipc/book.ipc.js';
|
import './ipc/book.ipc.js';
|
||||||
@@ -58,7 +57,6 @@ function createLoginWindow(): void {
|
|||||||
width: 500,
|
width: 500,
|
||||||
height: 900,
|
height: 900,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
// Ne pas définir icon sur macOS - utilise l'icône de l'app bundle
|
|
||||||
...(process.platform !== 'darwin' && { icon: iconPath }),
|
...(process.platform !== 'darwin' && { icon: iconPath }),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: preloadPath,
|
preload: preloadPath,
|
||||||
@@ -91,7 +89,6 @@ function createMainWindow(): void {
|
|||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
// Ne pas définir icon sur macOS - utilise l'icône de l'app bundle
|
|
||||||
...(process.platform !== 'darwin' && { icon: iconPath }),
|
...(process.platform !== 'darwin' && { icon: iconPath }),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: preloadPath,
|
preload: preloadPath,
|
||||||
@@ -121,11 +118,8 @@ function createMainWindow(): void {
|
|||||||
|
|
||||||
// IPC Handlers pour la gestion du token (OS-encrypted storage)
|
// IPC Handlers pour la gestion du token (OS-encrypted storage)
|
||||||
ipcMain.handle('get-token', () => {
|
ipcMain.handle('get-token', () => {
|
||||||
const storage = getSecureStorage();
|
const storage:SecureStorage = getSecureStorage();
|
||||||
const token = storage.get('authToken', null);
|
return storage.get('authToken', null);
|
||||||
console.log('[GetToken] Token requested, exists:', !!token);
|
|
||||||
console.log('[GetToken] Storage has authToken:', storage.has('authToken'));
|
|
||||||
return token;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('set-token', (_event, token: string) => {
|
ipcMain.handle('set-token', (_event, token: string) => {
|
||||||
@@ -154,11 +148,9 @@ ipcMain.handle('set-lang', (_event, lang: 'fr' | 'en') => {
|
|||||||
|
|
||||||
// IPC Handler pour initialiser l'utilisateur après récupération depuis le serveur
|
// IPC Handler pour initialiser l'utilisateur après récupération depuis le serveur
|
||||||
ipcMain.handle('init-user', async (_event, userId: string) => {
|
ipcMain.handle('init-user', async (_event, userId: string) => {
|
||||||
console.log('[InitUser] Initializing user:', userId);
|
const storage:SecureStorage = getSecureStorage();
|
||||||
|
|
||||||
const storage = getSecureStorage();
|
|
||||||
storage.set('userId', userId);
|
storage.set('userId', userId);
|
||||||
storage.set('lastUserId', userId); // Save for offline mode
|
storage.set('lastUserId', userId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let encryptionKey: string | null = null;
|
let encryptionKey: string | null = null;
|
||||||
@@ -166,22 +158,16 @@ ipcMain.handle('init-user', async (_event, userId: string) => {
|
|||||||
if (!hasUserEncryptionKey(userId)) {
|
if (!hasUserEncryptionKey(userId)) {
|
||||||
encryptionKey = generateUserEncryptionKey(userId);
|
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) {
|
if (!encryptionKey) {
|
||||||
console.error('[InitUser] CRITICAL: Generated key is undefined, blocking operation');
|
|
||||||
throw new Error('Failed to generate encryption key');
|
throw new Error('Failed to generate encryption key');
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserEncryptionKey(userId, encryptionKey);
|
setUserEncryptionKey(userId, encryptionKey);
|
||||||
|
|
||||||
// Verify the key was saved
|
const savedKey:string = getUserEncryptionKey(userId);
|
||||||
const savedKey = getUserEncryptionKey(userId);
|
|
||||||
console.log('[InitUser] Key verification after save:', savedKey ? `${savedKey.substring(0, 10)}...` : 'UNDEFINED');
|
console.log('[InitUser] Key verification after save:', savedKey ? `${savedKey.substring(0, 10)}...` : 'UNDEFINED');
|
||||||
|
|
||||||
if (!savedKey) {
|
if (!savedKey) {
|
||||||
console.error('[InitUser] CRITICAL: Key was not saved correctly, blocking operation');
|
|
||||||
throw new Error('Failed to save encryption key');
|
throw new Error('Failed to save encryption key');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -195,9 +181,7 @@ ipcMain.handle('init-user', async (_event, userId: string) => {
|
|||||||
setUserEncryptionKey(userId, encryptionKey);
|
setUserEncryptionKey(userId, encryptionKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save userId and lastUserId to disk now that we have everything
|
|
||||||
// This is the ONLY additional save after login
|
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
storage.save();
|
storage.save();
|
||||||
console.log('[InitUser] User ID and lastUserId saved to disk (encrypted)');
|
console.log('[InitUser] User ID and lastUserId saved to disk (encrypted)');
|
||||||
@@ -219,28 +203,21 @@ ipcMain.on('login-success', async (_event, token: string) => {
|
|||||||
console.log('[Login] Received token, setting in storage');
|
console.log('[Login] Received token, setting in storage');
|
||||||
const storage = getSecureStorage();
|
const storage = getSecureStorage();
|
||||||
storage.set('authToken', token);
|
storage.set('authToken', token);
|
||||||
console.log('[Login] Token set in cache, has authToken:', storage.has('authToken'));
|
|
||||||
// Note: userId will be set later when we get user info from server
|
|
||||||
|
|
||||||
if (loginWindow) {
|
if (loginWindow) {
|
||||||
loginWindow.close();
|
loginWindow.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
|
|
||||||
// Save AFTER mainWindow is created (fixes macOS safeStorage issue)
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
storage.save();
|
storage.save();
|
||||||
console.log('[Login] Auth token saved to disk (encrypted)');
|
|
||||||
} else {
|
} else {
|
||||||
console.error('[Login] Encryption still not available after window creation');
|
|
||||||
// Try one more time after another delay
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (safeStorage.isEncryptionAvailable()) {
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
storage.save();
|
storage.save();
|
||||||
console.log('[Login] Auth token saved to disk (encrypted) - second attempt');
|
|
||||||
} else {
|
} else {
|
||||||
console.error('[Login] CRITICAL: Cannot encrypt credentials');
|
console.error('[Login] CRITICAL: Cannot encrypt credentials');
|
||||||
}
|
}
|
||||||
@@ -252,31 +229,21 @@ ipcMain.on('login-success', async (_event, token: string) => {
|
|||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('logout', () => {
|
ipcMain.on('logout', ():void => {
|
||||||
try {
|
try {
|
||||||
const storage = getSecureStorage();
|
const storage:SecureStorage = getSecureStorage();
|
||||||
|
|
||||||
// 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('authToken');
|
||||||
storage.delete('userId');
|
storage.delete('userId');
|
||||||
storage.delete('userLang');
|
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();
|
storage.save();
|
||||||
console.log('[Logout] Cleared auth data from disk');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Logout] Error clearing storage:', error);
|
console.error('[Logout] Error clearing storage:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const db = getDatabaseService();
|
const db:DatabaseService = getDatabaseService();
|
||||||
db.close();
|
db.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Logout] Error closing database:', error);
|
console.error('[Logout] Error closing database:', error);
|
||||||
@@ -306,17 +273,10 @@ ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise<boole
|
|||||||
const { default: UserRepo } = await import('./database/repositories/user.repository.js');
|
const { default: UserRepo } = await import('./database/repositories/user.repository.js');
|
||||||
|
|
||||||
const lang: 'fr' | 'en' = 'fr';
|
const lang: 'fr' | 'en' = 'fr';
|
||||||
|
|
||||||
// Check if user already exists in local DB
|
|
||||||
try {
|
try {
|
||||||
UserRepo.fetchUserInfos(data.userId, lang);
|
UserRepo.fetchUserInfos(data.userId, lang);
|
||||||
// User exists, no need to sync
|
|
||||||
console.log(`[DB] User ${data.userId} already exists in local DB, skipping sync`);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// User doesn't exist, create it
|
|
||||||
console.log(`[DB] User ${data.userId} not found, creating in local DB`);
|
|
||||||
|
|
||||||
await User.addUser(
|
await User.addUser(
|
||||||
data.userId,
|
data.userId,
|
||||||
data.firstName,
|
data.firstName,
|
||||||
@@ -326,7 +286,6 @@ ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise<boole
|
|||||||
'',
|
'',
|
||||||
lang
|
lang
|
||||||
);
|
);
|
||||||
console.log(`[DB] User ${data.userId} synced successfully`);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -335,14 +294,12 @@ ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise<boole
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== DATABASE IPC HANDLERS ==========
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate user encryption key
|
* Generate user encryption key
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('generate-encryption-key', async (_event, userId: string) => {
|
ipcMain.handle('generate-encryption-key', async (_event, userId: string) => {
|
||||||
try {
|
try {
|
||||||
const key = generateUserEncryptionKey(userId);
|
const key:string = generateUserEncryptionKey(userId);
|
||||||
return { success: true, key };
|
return { success: true, key };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to generate encryption key:', error);
|
console.error('Failed to generate encryption key:', error);
|
||||||
@@ -357,16 +314,15 @@ ipcMain.handle('generate-encryption-key', async (_event, userId: string) => {
|
|||||||
* Get or generate user encryption key (OS-encrypted storage)
|
* Get or generate user encryption key (OS-encrypted storage)
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('get-user-encryption-key', (_event, userId: string) => {
|
ipcMain.handle('get-user-encryption-key', (_event, userId: string) => {
|
||||||
const storage = getSecureStorage();
|
const storage:SecureStorage = getSecureStorage();
|
||||||
const key = storage.get(`encryptionKey-${userId}`, null);
|
return storage.get(`encryptionKey-${userId}`, null);
|
||||||
return key;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store user encryption key (OS-encrypted storage)
|
* Store user encryption key (OS-encrypted storage)
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => {
|
ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => {
|
||||||
const storage = getSecureStorage();
|
const storage:SecureStorage = getSecureStorage();
|
||||||
storage.set(`encryptionKey-${userId}`, encryptionKey);
|
storage.set(`encryptionKey-${userId}`, encryptionKey);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -376,7 +332,7 @@ ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey
|
|||||||
*/
|
*/
|
||||||
ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) => {
|
ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabaseService();
|
const db:DatabaseService = getDatabaseService();
|
||||||
db.initialize(userId, encryptionKey);
|
db.initialize(userId, encryptionKey);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -388,28 +344,21 @@ ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// NOTE: All database IPC handlers have been moved to ./ipc/book.ipc.ts
|
|
||||||
// and use the new createHandler() pattern with auto userId/lang injection
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Enregistrer le protocole custom app:// pour servir les fichiers depuis out/
|
|
||||||
if (!isDev) {
|
if (!isDev) {
|
||||||
const outPath = path.join(process.resourcesPath, 'app.asar.unpacked/out');
|
const outPath = path.join(process.resourcesPath, 'app.asar.unpacked/out');
|
||||||
|
|
||||||
protocol.handle('app', async (request) => {
|
protocol.handle('app', async (request) => {
|
||||||
// Enlever app:// et ./
|
let filePath:string = request.url.replace('app://', '').replace(/^\.\//, '');
|
||||||
let filePath = request.url.replace('app://', '').replace(/^\.\//, '');
|
const fullPath:string = path.normalize(path.join(outPath, filePath));
|
||||||
const fullPath = path.normalize(path.join(outPath, filePath));
|
|
||||||
|
|
||||||
// Vérifier que le chemin est bien dans out/ (sécurité)
|
|
||||||
if (!fullPath.startsWith(outPath)) {
|
if (!fullPath.startsWith(outPath)) {
|
||||||
console.error('Security: Attempted to access file outside out/:', fullPath);
|
|
||||||
return new Response('Forbidden', { status: 403 });
|
return new Response('Forbidden', { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fs.promises.readFile(fullPath);
|
const data = await fs.promises.readFile(fullPath);
|
||||||
const ext = path.extname(fullPath).toLowerCase();
|
const ext:string = path.extname(fullPath).toLowerCase();
|
||||||
const mimeTypes: Record<string, string> = {
|
const mimeTypes: Record<string, string> = {
|
||||||
'.html': 'text/html',
|
'.html': 'text/html',
|
||||||
'.css': 'text/css',
|
'.css': 'text/css',
|
||||||
@@ -428,7 +377,6 @@ app.whenReady().then(() => {
|
|||||||
headers: { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' }
|
headers: { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' }
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load:', fullPath, error);
|
|
||||||
return new Response('Not found', { status: 404 });
|
return new Response('Not found', { status: 404 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user