mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(sync): improve Dropbox PKCE error handling and messaging
Dropbox Sync Improvements: - Change PKCE alerts to thrown errors for better error handling - Add provider ID to sync auth error logs for debugging - Improve PKCE error message to guide users to HTTPS requirement - Add unit tests for PKCE code generation Testing Improvements: - Enhance wait-for-supersync.sh to check both health and test endpoints - Match E2E fixture behavior for more reliable test startup detection These changes improve error diagnostics and provide clearer guidance when WebCrypto APIs are unavailable (HTTP contexts).
This commit is contained in:
parent
5027b31431
commit
301a8abdd7
5 changed files with 125 additions and 10 deletions
|
|
@ -1,20 +1,65 @@
|
|||
#!/bin/bash
|
||||
# Wait for SuperSync server to be ready at http://localhost:1901
|
||||
# Retries for up to 90 seconds (accounts for PostgreSQL + SuperSync startup)
|
||||
# Checks both health endpoint AND test mode endpoint to match fixture behavior
|
||||
|
||||
echo "Waiting for SuperSync server on http://localhost:1901..."
|
||||
echo "Checking both /health and /api/test/create-user endpoints..."
|
||||
|
||||
MAX_WAIT=90
|
||||
elapsed=0
|
||||
until curl -s http://localhost:1901/health > /dev/null 2>&1; do
|
||||
|
||||
# Function to check if server is fully healthy (matches fixture logic)
|
||||
check_server() {
|
||||
# Check 1: Basic health endpoint
|
||||
if ! curl -sf http://localhost:1901/health > /dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check 2: Test mode endpoint (verify TEST_MODE is enabled)
|
||||
# Create a dummy user to verify test endpoints are available
|
||||
local test_email="health-check-$(date +%s)@test.local"
|
||||
local response_code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-X POST http://localhost:1901/api/test/create-user \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"HealthCheck123!\"}" \
|
||||
2>/dev/null)
|
||||
|
||||
# Accept 201 (created), 409 (already exists), or 400 (validation error)
|
||||
# Reject 404 (test mode disabled) or connection errors
|
||||
if [ "$response_code" = "404" ] || [ -z "$response_code" ]; then
|
||||
echo "WARNING: Health endpoint OK but test mode endpoint returned: $response_code"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
until check_server; do
|
||||
if [ $elapsed -ge $MAX_WAIT ]; then
|
||||
echo "Timeout: SuperSync server did not start within ${MAX_WAIT}s"
|
||||
echo "--- SuperSync logs ---"
|
||||
docker compose -f docker-compose.yaml -f docker-compose.supersync.yaml logs supersync
|
||||
echo ""
|
||||
echo "ERROR: SuperSync server did not start within ${MAX_WAIT}s"
|
||||
echo ""
|
||||
echo "=== Diagnostics ==="
|
||||
echo "1. Container status:"
|
||||
docker ps -a | grep -E "(supersync|db|NAMES)"
|
||||
echo ""
|
||||
echo "2. Port mappings:"
|
||||
docker ps | grep supersync | awk '{print $NF " " $(NF-1)}'
|
||||
echo ""
|
||||
echo "3. Database health:"
|
||||
docker compose -f docker-compose.e2e.yaml -f docker-compose.yaml -f docker-compose.supersync.yaml exec -T db pg_isready -U supersync 2>&1 || echo "Database not ready"
|
||||
echo ""
|
||||
echo "4. SuperSync logs (last 50 lines):"
|
||||
docker compose -f docker-compose.e2e.yaml -f docker-compose.yaml -f docker-compose.supersync.yaml logs --tail=50 supersync
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
# Show progress every 10 seconds
|
||||
if [ $((elapsed % 10)) -eq 0 ]; then
|
||||
echo "Still waiting... (${elapsed}s / ${MAX_WAIT}s)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "SuperSync server is ready!"
|
||||
echo "SuperSync server is ready! (Both health and test endpoints responding)"
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ export class SyncWrapperService {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
SyncLog.err(error);
|
||||
SyncLog.err(`Failed to configure auth for provider ${providerId}:`, error);
|
||||
this._snackService.open({
|
||||
// TODO don't limit snack to dropbox
|
||||
msg: T.F.DROPBOX.S.UNABLE_TO_GENERATE_PKCE_CHALLENGE,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import { generatePKCECodes } from './generate-pkce-codes';
|
||||
|
||||
describe('generatePKCECodes', () => {
|
||||
let originalCrypto: Crypto;
|
||||
|
||||
beforeEach(() => {
|
||||
originalCrypto = window.crypto;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original crypto
|
||||
Object.defineProperty(window, 'crypto', {
|
||||
value: originalCrypto,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when crypto.getRandomValues is unavailable', async () => {
|
||||
// Mock crypto without getRandomValues
|
||||
Object.defineProperty(window, 'crypto', {
|
||||
value: {} as Crypto,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
await expectAsync(generatePKCECodes(128)).toBeRejectedWithError(
|
||||
/WebCrypto API.*getRandomValues/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when crypto.subtle is unavailable', async () => {
|
||||
// Mock crypto with getRandomValues but without subtle
|
||||
Object.defineProperty(window, 'crypto', {
|
||||
value: {
|
||||
getRandomValues: (array: Uint32Array) => {
|
||||
// Fill with dummy values
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = Math.floor(Math.random() * 0xffffffff);
|
||||
}
|
||||
return array;
|
||||
},
|
||||
subtle: undefined,
|
||||
} as unknown as Crypto,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
await expectAsync(generatePKCECodes(128)).toBeRejectedWithError(
|
||||
/WebCrypto API.*subtle/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('should successfully generate PKCE codes when WebCrypto is available', async () => {
|
||||
// Use real WebCrypto API (already available in test environment)
|
||||
const result = await generatePKCECodes(128);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.codeVerifier).toBeDefined();
|
||||
expect(result.codeChallenge).toBeDefined();
|
||||
expect(typeof result.codeVerifier).toBe('string');
|
||||
expect(typeof result.codeChallenge).toBe('string');
|
||||
expect(result.codeVerifier.length).toBeGreaterThan(0);
|
||||
expect(result.codeChallenge.length).toBeGreaterThan(0);
|
||||
// Code challenge should be different from verifier (it's hashed)
|
||||
expect(result.codeChallenge).not.toBe(result.codeVerifier);
|
||||
});
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
const generateRandomString = (length: number): string => {
|
||||
const array = new Uint32Array(length / 2);
|
||||
if (!window.crypto?.getRandomValues) {
|
||||
alert(
|
||||
throw new Error(
|
||||
'WebCrypto API (getRandomValues) not supported in your browser. Please update to the latest version or use a different one',
|
||||
);
|
||||
}
|
||||
|
|
@ -21,7 +21,9 @@ const sha256 = (plain: string): Promise<ArrayBuffer> => {
|
|||
// NOTE: crypto.subtle is supposed to be undefined in insecure contexts
|
||||
// @see https://www.chromium.org/blink/webcrypto
|
||||
if (window.crypto?.subtle === undefined) {
|
||||
alert('WebCrypto API (subtle.digest) is only supported in secure contexts.');
|
||||
throw new Error(
|
||||
'WebCrypto API (subtle.digest) is only supported in secure contexts.',
|
||||
);
|
||||
}
|
||||
|
||||
return window.crypto.subtle.digest('SHA-256', data);
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@
|
|||
"S": {
|
||||
"OFFLINE": "Dropbox: Unable to sync, because offline",
|
||||
"SYNC_ERROR": "Dropbox: Error while syncing",
|
||||
"UNABLE_TO_GENERATE_PKCE_CHALLENGE": "Dropbox: Unable to generate PKCE challenge."
|
||||
"UNABLE_TO_GENERATE_PKCE_CHALLENGE": "Dropbox sync requires a secure connection (HTTPS). Please ensure you're using the app over HTTPS or try the stable version."
|
||||
}
|
||||
},
|
||||
"D_RATE": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue