fix(sync): use manual code entry for Dropbox auth on all platforms

- Force dialog to show input field instead of OAuth redirect spinner
- Remove platform-specific OAuth redirect URI logic
- Ensures reliable copy/paste auth flow on Android, web, and Electron
This commit is contained in:
Johannes Millan 2026-01-24 15:01:59 +01:00
parent d3b85dbd7e
commit ec847ce897
3 changed files with 8 additions and 182 deletions

View file

@ -1,164 +0,0 @@
# SuperSync CORS Wildcard Support Design
**Date:** 2026-01-24
**Status:** Approved
**Author:** Claude Code
## Overview
Add wildcard pattern support to SuperSync server CORS configuration to allow preview deployment domains like `https://*.super-productivity-preview.pages.dev` while maintaining security.
## Problem Statement
Currently, CORS origins only support exact string matching. Preview deployments use dynamic subdomains (e.g., `https://f5382282.super-productivity-preview.pages.dev`) that cannot be whitelisted without manually updating the environment configuration for each deployment.
## Design Decision
Use **wildcard syntax** instead of arbitrary RegExp parsing for security and simplicity.
### Why Wildcards Over RegExp?
- **Security**: @fastify/cors documentation warns that RegExp patterns can enable DoS attacks through catastrophic backtracking
- **Simplicity**: `https://*.example.com` is clearer than `/^https:\/\/[^\/]+\.example\.com$/`
- **Validation**: We can strictly validate wildcard patterns and reject dangerous ones
- **Sufficient**: Covers the preview deployment use case without exposing attack surface
## Implementation Design
### 1. Wildcard Syntax
**Supported:**
- `https://*.example.com` - any subdomain of example.com
- `https://*.super-productivity-preview.pages.dev` - preview deployments
- `http://*.localhost:4200` - local dev with subdomains
**Rejected patterns:**
- `https://*` - too broad
- `https://example.*` - TLD wildcards
- `https://*/path` - path wildcards
- `*://example.com` - protocol wildcards
- Multiple wildcards in one origin
### 2. Parsing Function
```typescript
function parseCorsOrigin(origin: string): CorsOrigin {
const trimmed = origin.trim();
// No wildcard - return as-is for exact match
if (!trimmed.includes('*')) {
return trimmed;
}
// Validate wildcard count
const wildcardCount = (trimmed.match(/\*/g) || []).length;
if (wildcardCount > 1) {
throw new Error(`Invalid CORS origin "${trimmed}": multiple wildcards not allowed`);
}
// Only allow subdomain wildcards: https://*.example.com
const subdomainWildcardPattern = /^(https?):\/\/\*\.([a-z0-9.-]+)(:\d+)?$/i;
const match = trimmed.match(subdomainWildcardPattern);
if (!match) {
throw new Error(
`Invalid CORS origin "${trimmed}": wildcard only allowed as subdomain (e.g., https://*.example.com)`,
);
}
const [, protocol, domain, port] = match;
// Convert to safe RegExp
const escapedDomain = domain.replace(/\./g, '\\.');
const portPart = port ? port.replace('.', '\\.') : '';
const pattern = `^${protocol}:\\/\\/[^\\/]+\\.${escapedDomain}${portPart}$`;
return new RegExp(pattern);
}
```
### 3. Configuration Integration
**Update `config.ts`:**
- Add `parseCorsOrigin()` helper
- Call when parsing `CORS_ORIGINS` env var
- Update default origins to include preview pattern
**Default origins:**
```typescript
const DEFAULT_CORS_ORIGINS: CorsOrigin[] = [
'https://app.super-productivity.com',
/^https:\/\/[^\/]+\.super-productivity-preview\.pages\.dev$/,
];
```
**Environment variable example:**
```bash
CORS_ORIGINS=https://app.super-productivity.com,https://*.super-productivity-preview.pages.dev,http://localhost:4200
```
### 4. Error Handling
- Errors thrown during config loading (server won't start with invalid config)
- Clear, actionable error messages
- Examples in error output
## Testing Strategy
### Unit Tests (`config.spec.ts`)
Test `parseCorsOrigin()` function:
- Valid wildcards convert to correct RegExp
- Exact matches pass through unchanged
- Invalid patterns throw descriptive errors
- Edge cases: multiple wildcards, wrong positions, missing protocol
### Integration Tests
- Server starts with wildcard origins
- CORS headers returned correctly for matching origins
- Non-matching origins rejected
### Manual Testing
- Test with actual preview URL in browser
- Verify CORS headers in DevTools
- Test preflight OPTIONS requests
## Security Considerations
1. **DoS Prevention**: Only allowing simple subdomain wildcards prevents catastrophic backtracking
2. **No Arbitrary RegExp**: Users cannot inject complex patterns from environment
3. **Strict Validation**: Invalid patterns fail fast at startup
4. **Production Safety**: Wildcard in production still requires explicit domain specification
## Migration Path
**No breaking changes:**
- Existing exact-match origins continue working
- New wildcard syntax is additive
- Default config includes preview domain pattern
**Documentation updates:**
- Update `.env.example` with wildcard examples
- Update README with CORS configuration section
- Add comments explaining wildcard syntax
## Implementation Checklist
- [ ] Add `parseCorsOrigin()` to `config.ts`
- [ ] Update CORS origin parsing in `loadConfigFromEnv()`
- [ ] Update `DEFAULT_CORS_ORIGINS` to include preview pattern
- [ ] Update `.env.example` with documentation
- [ ] Write unit tests for `parseCorsOrigin()`
- [ ] Write integration tests for CORS with wildcards
- [ ] Manual testing with preview deployment
- [ ] Update README if needed

View file

@ -13,7 +13,6 @@ import { MatFormField, MatLabel, MatPrefix } from '@angular/material/form-field'
import { MatInput } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
import { TranslatePipe } from '@ngx-translate/core';
import { IS_NATIVE_PLATFORM } from '../../../util/is-native-platform';
import { OAuthCallbackHandlerService } from '../oauth-callback-handler.service';
import { Subscription } from 'rxjs';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
@ -54,7 +53,8 @@ export class DialogGetAndEnterAuthCodeComponent implements OnDestroy {
T: typeof T = T;
token?: string;
readonly isNativePlatform = IS_NATIVE_PLATFORM;
// Always use manual code entry flow (show input field)
readonly isNativePlatform = false;
private _authCodeSub?: Subscription;
constructor() {

View file

@ -14,7 +14,6 @@ import { DropboxApi } from './dropbox-api';
import { generatePKCECodes } from './generate-pkce-codes';
import { SyncCredentialStore } from '../../credential-store.service';
import { SyncProviderPrivateCfgBase } from '../../../core/types/sync.types';
import { IS_NATIVE_PLATFORM } from '../../../../util/is-native-platform';
const DROPBOX_AUTH_URL = 'https://www.dropbox.com/oauth2/authorize' as const;
const PATH_NOT_FOUND_ERROR = 'path/not_found' as const;
@ -306,25 +305,16 @@ export class Dropbox implements SyncProviderServiceInterface<SyncProviderId.Drop
}
/**
* Gets the appropriate OAuth redirect URI based on the current platform.
* Gets the OAuth redirect URI.
*
* Mobile platforms (iOS/Android/Android WebView):
* Returns custom URI scheme to enable automatic redirect back to app.
* Dropbox will redirect to: com.super-productivity.app://oauth-callback?code=xxx
* Always returns null to use Dropbox's manual code entry flow.
* User must copy the code from Dropbox's page and paste it manually.
* This works reliably across all platforms (web, Electron, Android, iOS).
*
* Web/Electron platforms:
* Returns null to use Dropbox's manual code entry flow.
* User must copy the code from Dropbox's page and paste it manually.
*
* @returns The redirect URI for mobile platforms, null for web/Electron
* @returns null for manual code entry flow
*/
private _getRedirectUri(): string | null {
if (IS_NATIVE_PLATFORM) {
return 'com.super-productivity.app://oauth-callback';
} else {
// Web/Electron: Use manual code entry (no redirect_uri)
return null;
}
return null;
}
/**