From ec847ce8972f191586221206ba005d0e23dbf176 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sat, 24 Jan 2026 15:01:59 +0100 Subject: [PATCH] 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 --- ...26-01-24-supersync-cors-wildcard-design.md | 164 ------------------ ...ialog-get-and-enter-auth-code.component.ts | 4 +- .../file-based/dropbox/dropbox.ts | 22 +-- 3 files changed, 8 insertions(+), 182 deletions(-) delete mode 100644 docs/plans/2026-01-24-supersync-cors-wildcard-design.md diff --git a/docs/plans/2026-01-24-supersync-cors-wildcard-design.md b/docs/plans/2026-01-24-supersync-cors-wildcard-design.md deleted file mode 100644 index 39d5f9f27f..0000000000 --- a/docs/plans/2026-01-24-supersync-cors-wildcard-design.md +++ /dev/null @@ -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 diff --git a/src/app/imex/sync/dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component.ts b/src/app/imex/sync/dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component.ts index cf407806cc..2244e0e628 100644 --- a/src/app/imex/sync/dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component.ts +++ b/src/app/imex/sync/dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component.ts @@ -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() { diff --git a/src/app/op-log/sync-providers/file-based/dropbox/dropbox.ts b/src/app/op-log/sync-providers/file-based/dropbox/dropbox.ts index a91acbf3a1..510f740ee8 100644 --- a/src/app/op-log/sync-providers/file-based/dropbox/dropbox.ts +++ b/src/app/op-log/sync-providers/file-based/dropbox/dropbox.ts @@ -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