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:
@@ -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
189
PWA_AUTH_GUIDE.md
Normal 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
|
||||||
@@ -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>
|
||||||
|
|||||||
78
public/sw.js
78
public/sw.js
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user