mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
# Description of Changes Please provide a summary of the changes, including: ## Add PDF File Association Support for Tauri App ### 🎯 **Features Added** - PDF file association configuration in Tauri - Command line argument detection for opened files - Automatic file loading when app is launched via "Open with" - Cross-platform support (Windows/macOS) ### 🔧 **Technical Changes** - Added `fileAssociations` in `tauri.conf.json` for PDF files - New `get_opened_file` Tauri command to detect file arguments - `fileOpenService` with Tauri fs plugin integration - `useOpenedFile` hook for React integration - Improved backend health logging during startup (reduced noise) ### 🧪 **Testing** See * https://v2.tauri.app/start/prerequisites/ * [DesktopApplicationDevelopmentGuide.md](DesktopApplicationDevelopmentGuide.md) ```bash # Test file association during development: cd frontend npm install cargo tauri dev --no-watch -- -- "path/to/file.pdf" ``` For production testing: 1. Build: npm run tauri build 2. Install the built app 3. Right-click PDF → "Open with" → Stirling-PDF 🚀 User Experience - Users can now double-click PDF files to open them directly in Stirling-PDF - Files automatically load in the viewer when opened via file association - Seamless integration with OS file handling --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com> Co-authored-by: James Brunton <james@stirlingpdf.com> Co-authored-by: James Brunton <jbrunton96@gmail.com>
235 lines
6.1 KiB
TypeScript
235 lines
6.1 KiB
TypeScript
import { createContext, useContext, useEffect, useState, ReactNode, useCallback } from 'react';
|
|
import apiClient from '@app/services/apiClient';
|
|
import { springAuth } from '@app/auth/springAuthClient';
|
|
import type { Session, User, AuthError, AuthChangeEvent } from '@app/auth/springAuthClient';
|
|
|
|
/**
|
|
* Auth Context Type
|
|
* Simplified version without SaaS-specific features (credits, subscriptions)
|
|
*/
|
|
interface AuthContextType {
|
|
session: Session | null;
|
|
user: User | null;
|
|
loading: boolean;
|
|
error: AuthError | null;
|
|
signOut: () => Promise<void>;
|
|
refreshSession: () => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType>({
|
|
session: null,
|
|
user: null,
|
|
loading: true,
|
|
error: null,
|
|
signOut: async () => {},
|
|
refreshSession: async () => {},
|
|
});
|
|
|
|
/**
|
|
* Auth Provider Component
|
|
*
|
|
* Manages authentication state and provides it to the entire app.
|
|
* Integrates with Spring Security + JWT backend.
|
|
*/
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [session, setSession] = useState<Session | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<AuthError | null>(null);
|
|
|
|
/**
|
|
* Refresh current session
|
|
*/
|
|
const refreshSession = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
console.debug('[Auth] Refreshing session...');
|
|
|
|
const { data, error } = await springAuth.refreshSession();
|
|
|
|
if (error) {
|
|
console.error('[Auth] Session refresh error:', error);
|
|
setError(error);
|
|
setSession(null);
|
|
} else {
|
|
console.debug('[Auth] Session refreshed successfully');
|
|
setSession(data.session);
|
|
}
|
|
} catch (err) {
|
|
console.error('[Auth] Unexpected error during session refresh:', err);
|
|
setError(err as AuthError);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Sign out user
|
|
*/
|
|
const signOut = useCallback(async () => {
|
|
try {
|
|
setError(null);
|
|
console.debug('[Auth] Signing out...');
|
|
|
|
const { error } = await springAuth.signOut();
|
|
|
|
if (error) {
|
|
console.error('[Auth] Sign out error:', error);
|
|
setError(error);
|
|
} else {
|
|
console.debug('[Auth] Signed out successfully');
|
|
setSession(null);
|
|
}
|
|
} catch (err) {
|
|
console.error('[Auth] Unexpected error during sign out:', err);
|
|
setError(err as AuthError);
|
|
}
|
|
}, []);
|
|
|
|
/**
|
|
* Initialize auth on mount
|
|
*/
|
|
useEffect(() => {
|
|
let mounted = true;
|
|
|
|
const initializeAuth = async () => {
|
|
try {
|
|
console.debug('[Auth] Initializing auth...');
|
|
|
|
// First check if login is enabled
|
|
const configResponse = await apiClient.get('/api/v1/config/app-config');
|
|
if (configResponse.status === 200) {
|
|
const config = configResponse.data;
|
|
|
|
// If login is disabled, skip authentication entirely
|
|
if (config.enableLogin === false) {
|
|
console.debug('[Auth] Login disabled - skipping authentication');
|
|
if (mounted) {
|
|
setSession(null);
|
|
setLoading(false);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Login is enabled, proceed with normal auth check
|
|
const { data, error } = await springAuth.getSession();
|
|
|
|
if (!mounted) return;
|
|
|
|
if (error) {
|
|
console.error('[Auth] Initial session error:', error);
|
|
setError(error);
|
|
} else {
|
|
console.debug('[Auth] Initial session loaded:', {
|
|
hasSession: !!data.session,
|
|
userId: data.session?.user?.id,
|
|
email: data.session?.user?.email,
|
|
});
|
|
setSession(data.session);
|
|
}
|
|
} catch (err) {
|
|
console.error('[Auth] Unexpected error during auth initialization:', err);
|
|
if (mounted) {
|
|
setError(err as AuthError);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
initializeAuth();
|
|
|
|
// Subscribe to auth state changes
|
|
const { data: { subscription } } = springAuth.onAuthStateChange(
|
|
async (event: AuthChangeEvent, newSession: Session | null) => {
|
|
if (!mounted) return;
|
|
|
|
console.debug('[Auth] Auth state change:', {
|
|
event,
|
|
hasSession: !!newSession,
|
|
userId: newSession?.user?.id,
|
|
email: newSession?.user?.email,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
// Schedule state update
|
|
setTimeout(() => {
|
|
if (mounted) {
|
|
setSession(newSession);
|
|
setError(null);
|
|
|
|
// Handle specific events
|
|
if (event === 'SIGNED_OUT') {
|
|
console.debug('[Auth] User signed out, clearing session');
|
|
} else if (event === 'SIGNED_IN') {
|
|
console.debug('[Auth] User signed in successfully');
|
|
} else if (event === 'TOKEN_REFRESHED') {
|
|
console.debug('[Auth] Token refreshed');
|
|
} else if (event === 'USER_UPDATED') {
|
|
console.debug('[Auth] User updated');
|
|
}
|
|
}
|
|
}, 0);
|
|
}
|
|
);
|
|
|
|
return () => {
|
|
mounted = false;
|
|
subscription.unsubscribe();
|
|
};
|
|
}, []);
|
|
|
|
const value: AuthContextType = {
|
|
session,
|
|
user: session?.user ?? null,
|
|
loading,
|
|
error,
|
|
signOut,
|
|
refreshSession,
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={value}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook to access auth context
|
|
* Must be used within AuthProvider
|
|
*/
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
|
|
if (context === undefined) {
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
/**
|
|
* Debug hook to expose auth state for debugging
|
|
* Can be used in development to monitor auth state
|
|
*/
|
|
export function useAuthDebug() {
|
|
const auth = useAuth();
|
|
|
|
useEffect(() => {
|
|
console.debug('[Auth Debug] Current auth state:', {
|
|
hasSession: !!auth.session,
|
|
hasUser: !!auth.user,
|
|
loading: auth.loading,
|
|
hasError: !!auth.error,
|
|
userId: auth.user?.id,
|
|
email: auth.user?.email,
|
|
});
|
|
}, [auth.session, auth.user, auth.loading, auth.error]);
|
|
|
|
return auth;
|
|
}
|