mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
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:
parent
d3b85dbd7e
commit
ec847ce897
3 changed files with 8 additions and 182 deletions
|
|
@ -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
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue