Stirling-PDF/frontend/src/desktop/auth/UseSession.tsx
ConnorYoh 4c0c9b28ef
V2 Tauri integration (#3854)
# 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>
2025-11-05 11:44:59 +00:00

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