feat: Update PWA service worker for authentication

- Add authentication pages to static cache (/auth/login, /auth/register, /auth/error)
- Implement special handling for authentication API requests (network-only with offline error)
- Add authentication page caching strategy (cache-first with network fallback)
- Prevent caching of authentication errors (401, 403)
- Enhance offline page with online detection and auto-redirect
- Add authentication notice to offline page
- Update cache version to v2 for proper invalidation
- Create PWA_AUTH_GUIDE.md documentation

Task: 19. Update PWA service worker for authentication
Requirements: 4.4
This commit is contained in:
2025-12-02 01:34:28 +00:00
parent 534796edd7
commit 0e609af924
4 changed files with 327 additions and 26 deletions

View File

@@ -264,7 +264,7 @@
- [ ] 18. Checkpoint - Ensure all tests pass - [ ] 18. Checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise. - Ensure all tests pass, ask the user if questions arise.
- [ ] 19. Update PWA service worker for authentication - [x] 19. Update PWA service worker for authentication
- Handle authentication errors in offline mode - Handle authentication errors in offline mode
- Cache authentication-related pages appropriately - Cache authentication-related pages appropriately
- Update manifest if needed - Update manifest if needed

189
PWA_AUTH_GUIDE.md Normal file
View File

@@ -0,0 +1,189 @@
# PWA Authentication Guide
## Overview
The Ultimate Lists App PWA has been updated to handle authentication properly in offline scenarios. This guide explains how the service worker manages authentication-related requests and what users can expect when offline.
## Service Worker Updates
### Cache Version
The service worker cache has been updated to version 2 (`ultimate-lists-v2`) to ensure proper cache invalidation and updates.
### Authentication Pages Caching
The following authentication-related pages are now cached during service worker installation:
- `/auth/login` - Login page
- `/auth/register` - Registration page
- `/auth/error` - Authentication error page
This allows users to view these pages even when offline, though actual authentication requires an internet connection.
### Authentication Request Handling
#### API Authentication Requests (`/api/auth/*`)
Authentication API calls (login, register, session validation) always require a network connection. When offline:
- The service worker returns a 503 Service Unavailable response
- A JSON error message explains that authentication requires internet connectivity
- The error message: "Authentication requires an internet connection. Please check your connection and try again."
#### Authentication Pages (`/auth/*`)
Authentication pages use a cache-first strategy with network fallback:
1. Try to fetch the latest version from the network
2. Cache successful responses for offline access
3. If offline, serve the cached version
4. If no cached version exists, redirect to the offline page
### Authentication Error Handling
The service worker now properly handles authentication errors (401, 403):
- Authentication errors are **not cached**
- This prevents stale authentication states from being served
- Users always get fresh authentication status from the server
### Offline Page Enhancements
The offline page (`/app/offline/page.tsx`) has been enhanced with:
1. **Online Detection**: Automatically detects when the connection is restored
2. **Auto-redirect**: Redirects to the home page when back online
3. **Authentication Notice**: Displays a clear message that login/registration requires internet
4. **Visual Feedback**: Shows different states for offline vs. back online
## User Experience
### When Online
- All authentication features work normally
- Login, registration, and session management function as expected
- Pages are cached for faster subsequent loads
### When Offline
#### Authenticated Users
- Can view cached lists and tasks
- Can make changes that will sync when back online
- Session remains valid (until expiration)
- Cannot perform new authentication actions
#### Unauthenticated Users
- Can view cached authentication pages (login/register forms)
- Cannot actually log in or register (requires internet)
- See clear error messages explaining the need for connectivity
- Are directed to the offline page with authentication guidance
## Testing Offline Behavior
### Manual Testing
1. **Test Offline Login Attempt**:
- Open the app while online
- Navigate to `/auth/login`
- Disconnect from the internet (or use DevTools offline mode)
- Try to submit login credentials
- Verify error message appears
2. **Test Cached Authentication Pages**:
- Visit `/auth/login` while online
- Disconnect from the internet
- Refresh the page
- Verify the login page loads from cache
3. **Test Online Detection**:
- Navigate to `/offline` while offline
- Reconnect to the internet
- Verify automatic redirect to home page
### Chrome DevTools Testing
1. Open Chrome DevTools (F12)
2. Go to Application tab → Service Workers
3. Check "Offline" to simulate offline mode
4. Test authentication flows
5. Verify cache contents in Application → Cache Storage
## Technical Implementation
### Service Worker Strategy
```javascript
// Authentication API calls - Network only
if (url.pathname.startsWith('/api/auth/')) {
// Always try network, return 503 if offline
}
// Authentication pages - Cache-first with network fallback
if (url.pathname.startsWith('/auth/')) {
// Try network, fall back to cache, then offline page
}
```
### Error Response Format
When authentication API calls fail offline:
```json
{
"error": "Authentication requires an internet connection. Please check your connection and try again."
}
```
### Cache Strategy Summary
| Resource Type | Strategy | Offline Behavior |
|--------------|----------|------------------|
| Authentication API | Network only | Return 503 error |
| Authentication Pages | Cache-first | Serve cached version |
| Static Assets | Cache-first | Serve cached version |
| API Responses | Stale-while-revalidate | Serve cached, update in background |
| Regular Pages | Stale-while-revalidate | Serve cached or offline page |
## Security Considerations
1. **No Credential Caching**: User credentials are never cached by the service worker
2. **Session Validation**: Sessions are validated server-side on each request
3. **Error State Handling**: Authentication errors (401, 403) are not cached
4. **Secure Token Storage**: Session tokens are managed by NextAuth.js, not the service worker
## Future Enhancements
Potential improvements for authentication in offline scenarios:
- Queue authentication attempts for when connection is restored
- Show estimated time until session expiration
- Provide offline-specific UI hints for authenticated users
- Implement background sync for authentication state updates
## Troubleshooting
### Service Worker Not Updating
If changes aren't reflected:
1. Unregister the old service worker in DevTools
2. Clear cache storage
3. Hard refresh the page (Ctrl+Shift+R)
### Authentication Not Working Offline
This is expected behavior. Authentication requires:
- Active internet connection
- Server communication for credential validation
- Session token generation and validation
### Cached Pages Showing Old Content
The service worker updates cached pages when online. To force an update:
1. Ensure you're online
2. The service worker checks for updates every minute
3. Or manually update in DevTools → Application → Service Workers → Update

View File

@@ -1,8 +1,30 @@
'use client'; 'use client';
import { WifiOff } from 'lucide-react'; import { WifiOff } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function OfflinePage() { export default function OfflinePage() {
const [isOnline, setIsOnline] = useState(false);
const router = useRouter();
useEffect(() => {
// Check if we're back online
const handleOnline = () => {
setIsOnline(true);
// Automatically redirect after a short delay
setTimeout(() => {
router.push('/');
}, 1000);
};
window.addEventListener('online', handleOnline);
return () => {
window.removeEventListener('online', handleOnline);
};
}, [router]);
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4"> <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
<div className="text-center max-w-md"> <div className="text-center max-w-md">
@@ -13,36 +35,52 @@ export default function OfflinePage() {
</div> </div>
<h1 className="text-3xl font-bold text-gray-900 mb-4"> <h1 className="text-3xl font-bold text-gray-900 mb-4">
You're Offline {isOnline ? "You're Back Online!" : "You're Offline"}
</h1> </h1>
<p className="text-gray-600 mb-8"> {isOnline ? (
It looks like you've lost your internet connection. Don't worry, your data is safe and will sync when you're back online. <p className="text-gray-600 mb-8">
</p> Your connection has been restored. Redirecting...
</p>
) : (
<>
<p className="text-gray-600 mb-8">
It looks like you've lost your internet connection. Don't worry, your data is safe and will sync when you're back online.
</p>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 text-left"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 text-left mb-6">
<h2 className="font-semibold text-gray-900 mb-3">What you can do:</h2> <h2 className="font-semibold text-gray-900 mb-3">What you can do:</h2>
<ul className="space-y-2 text-gray-600"> <ul className="space-y-2 text-gray-600">
<li className="flex items-start"> <li className="flex items-start">
<span className="text-blue-600 mr-2">•</span> <span className="text-blue-600 mr-2">•</span>
<span>View your cached lists and tasks</span> <span>View your cached lists and tasks</span>
</li> </li>
<li className="flex items-start"> <li className="flex items-start">
<span className="text-blue-600 mr-2">•</span> <span className="text-blue-600 mr-2">•</span>
<span>Make changes that will sync later</span> <span>Make changes that will sync later</span>
</li> </li>
<li className="flex items-start"> <li className="flex items-start">
<span className="text-blue-600 mr-2">•</span> <span className="text-blue-600 mr-2">•</span>
<span>Check your connection and try again</span> <span>Check your connection and try again</span>
</li> </li>
</ul> </ul>
</div> </div>
<div className="bg-amber-50 rounded-lg border border-amber-200 p-4 text-left mb-6">
<h3 className="font-semibold text-amber-900 mb-2 text-sm">Note about authentication:</h3>
<p className="text-amber-800 text-sm">
Login and registration require an internet connection. If you need to sign in, please connect to the internet first.
</p>
</div>
</>
)}
<button <button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="mt-8 bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors" className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isOnline}
> >
Try Again {isOnline ? 'Redirecting...' : 'Try Again'}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,13 +1,16 @@
// Service Worker for Ultimate Lists App // Service Worker for Ultimate Lists App
// Implements caching strategies for offline functionality // Implements caching strategies for offline functionality
const CACHE_NAME = 'ultimate-lists-v1'; const CACHE_NAME = 'ultimate-lists-v2';
const RUNTIME_CACHE = 'ultimate-lists-runtime-v1'; const RUNTIME_CACHE = 'ultimate-lists-runtime-v2';
// Static assets to cache on install // Static assets to cache on install
const STATIC_ASSETS = [ const STATIC_ASSETS = [
'/', '/',
'/offline', '/offline',
'/auth/login',
'/auth/register',
'/auth/error',
'/manifest.json', '/manifest.json',
'/icon-192.svg', '/icon-192.svg',
'/icon-512.svg', '/icon-512.svg',
@@ -58,6 +61,12 @@ self.addEventListener('fetch', (event) => {
return; return;
} }
// Handle authentication-related requests
if (isAuthRequest(url)) {
event.respondWith(handleAuthRequest(request));
return;
}
// Cache-first strategy for static assets // Cache-first strategy for static assets
if (isStaticAsset(url)) { if (isStaticAsset(url)) {
event.respondWith(cacheFirst(request)); event.respondWith(cacheFirst(request));
@@ -90,6 +99,58 @@ function isPageRequest(url) {
return url.origin === self.location.origin && !url.pathname.includes('.'); return url.origin === self.location.origin && !url.pathname.includes('.');
} }
// Helper: Check if request is authentication-related
function isAuthRequest(url) {
return url.pathname.startsWith('/auth/') ||
url.pathname.startsWith('/api/auth/');
}
// Strategy: Handle authentication requests
async function handleAuthRequest(request) {
const url = new URL(request.url);
// For authentication API calls, always try network first
if (url.pathname.startsWith('/api/auth/')) {
try {
const response = await fetch(request);
return response;
} catch (error) {
console.error('Authentication API request failed (offline):', error);
// Return a JSON error response for API calls
return new Response(
JSON.stringify({
error: 'Authentication requires an internet connection. Please check your connection and try again.'
}),
{
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'application/json' }
}
);
}
}
// For authentication pages, use cache-first with network fallback
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (error) {
console.error('Authentication page request failed:', error);
// Return cached version if available
if (cached) {
return cached;
}
// Otherwise redirect to offline page
return caches.match('/offline');
}
}
// Strategy: Cache-first (for static assets) // Strategy: Cache-first (for static assets)
async function cacheFirst(request) { async function cacheFirst(request) {
const cache = await caches.open(CACHE_NAME); const cache = await caches.open(CACHE_NAME);
@@ -117,6 +178,12 @@ async function staleWhileRevalidate(request) {
const cached = await cache.match(request); const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => { const fetchPromise = fetch(request).then((response) => {
// Handle authentication errors (401, 403)
if (response.status === 401 || response.status === 403) {
// Don't cache authentication errors
return response;
}
if (response.ok) { if (response.ok) {
cache.put(request, response.clone()); cache.put(request, response.clone());
} }
@@ -141,6 +208,13 @@ async function staleWhileRevalidate(request) {
async function networkFirst(request) { async function networkFirst(request) {
try { try {
const response = await fetch(request); const response = await fetch(request);
// Handle authentication errors (401, 403)
if (response.status === 401 || response.status === 403) {
// Don't cache authentication errors
return response;
}
if (response.ok) { if (response.ok) {
const cache = await caches.open(RUNTIME_CACHE); const cache = await caches.open(RUNTIME_CACHE);
cache.put(request, response.clone()); cache.put(request, response.clone());