diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index ab99c65..1e81185 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -76,6 +76,7 @@ jobs: flutter build apk --release --flavor prod \ --build-name="${{ github.ref_name }}" \ --build-number="${{ github.run_number }}" \ + --dart-define=BACKEND_BASE_URL="${{ vars.BACKEND_BASE_URL || 'https://api.schuly.dev' }}" \ --dart-define=SENTRY_DSN="$SENTRY_DSN" \ --dart-define=SENTRY_RELEASE="${{ github.ref_name }}" \ --dart-define=SENTRY_ENVIRONMENT="production" @@ -88,6 +89,7 @@ jobs: flutter build appbundle --release --flavor prod \ --build-name="${{ github.ref_name }}" \ --build-number="${{ github.run_number }}" \ + --dart-define=BACKEND_BASE_URL="${{ vars.BACKEND_BASE_URL || 'https://api.schuly.dev' }}" \ --dart-define=SENTRY_DSN="$SENTRY_DSN" \ --dart-define=SENTRY_RELEASE="${{ github.ref_name }}" \ --dart-define=SENTRY_ENVIRONMENT="production" @@ -134,6 +136,7 @@ jobs: flutter build ios --release --no-codesign \ --build-name="${{ github.ref_name }}" \ --build-number="${{ github.run_number }}" \ + --dart-define=BACKEND_BASE_URL="${{ vars.BACKEND_BASE_URL || 'https://api.schuly.dev' }}" \ --dart-define=SENTRY_DSN="$SENTRY_DSN" \ --dart-define=SENTRY_RELEASE="${{ github.ref_name }}" \ --dart-define=SENTRY_ENVIRONMENT="production" diff --git a/lib/config/backend_config.dart b/lib/config/backend_config.dart index 57e5ee7..83bdf19 100644 --- a/lib/config/backend_config.dart +++ b/lib/config/backend_config.dart @@ -39,6 +39,20 @@ class BackendConfig { static String normalise(String? value) => (value ?? '').trim().replaceAll(RegExp(r'/+$'), ''); + /// True if [value] uses plaintext `http://` to a non-loopback host, so + /// credentials and tokens would travel in the clear. Used to warn before a + /// self-hoster saves an insecure custom backend. + static bool isInsecure(String? value) { + final uri = Uri.tryParse(normalise(value)); + if (uri == null || uri.scheme != 'http') return false; + final h = uri.host.toLowerCase(); + return h.isNotEmpty && + h != 'localhost' && + h != '127.0.0.1' && + h != '::1' && + !h.endsWith('.localhost'); + } + /// Probes [baseUrl] by fetching the anonymous `GET /api/app`. A reachable /// Schuly backend returns a JSON object with a `clientId`; on success this /// returns its reported `version` (or `'unknown'` if the field is missing). diff --git a/lib/ui/onboarding/onboarding_screen.dart b/lib/ui/onboarding/onboarding_screen.dart index 2afe848..1e21f5f 100644 --- a/lib/ui/onboarding/onboarding_screen.dart +++ b/lib/ui/onboarding/onboarding_screen.dart @@ -374,6 +374,14 @@ class _ServerPage extends StatelessWidget { hint: 'https://schuly.example.com', autocorrect: false, ), + if (BackendConfig.isInsecure(urlController.text)) ...[ + const SizedBox(height: 8), + Text( + 'Plaintext http:// - your login would be sent unencrypted. Use https:// unless this is a trusted local network.', + textAlign: TextAlign.center, + style: typography.xs.copyWith(color: colors.error), + ), + ], ], if (error != null) ...[ const SizedBox(height: 12), diff --git a/lib/ui/settings/settings_screen.dart b/lib/ui/settings/settings_screen.dart index 2e0f2cf..e1d7033 100644 --- a/lib/ui/settings/settings_screen.dart +++ b/lib/ui/settings/settings_screen.dart @@ -115,6 +115,13 @@ class _ServerDialogState extends State<_ServerDialog> { String? _error; bool _busy = false; + @override + void initState() { + super.initState(); + // Live-update the insecure-URL warning as the user types. + _urlCtrl.addListener(() => setState(() {})); + } + @override void dispose() { _urlCtrl.dispose(); @@ -230,6 +237,13 @@ class _ServerDialogState extends State<_ServerDialog> { hint: 'https://schuly.example.com', autocorrect: false, ), + if (BackendConfig.isInsecure(_urlCtrl.text)) ...[ + const SizedBox(height: 6), + Text( + 'Plaintext http:// - your login would be sent unencrypted. Use https:// unless this is a trusted local network.', + style: typography.xs.copyWith(color: colors.error), + ), + ], ], if (_error != null) ...[ const SizedBox(height: 8), diff --git a/package.json b/package.json index e41e8e2..eea09ae 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,15 @@ "description": "Task runner for the Schuly Flutter app. Use `bun run