mirror of
https://github.com/we-promise/sure.git
synced 2026-05-28 06:54:56 +00:00
Mobile: custom proxy headers + small login UX fixes (#1748)
* Add mobile custom proxy headers * Clear login placeholders on focus Email/password fields ship with example values pre-filled. Tapping the field now clears the placeholder so users don't have to delete it manually. Skips clearing if the user has already edited the value. * Push Configuration as a route from Sign in Opening Configuration from the Sign in screen now uses Navigator.push instead of toggling a state flag, so Android back returns to Sign in instead of quitting the app. Saving the URL auto-pops the route. * Address PR review on custom proxy headers - Test Connection no longer leaves global ApiConfig headers mutated; unsaved edits are restored in a finally block after the probe. - _loadSavedUrl / _loadCustomHeaders wrap storage reads in try/catch and always finish initialization with sensible defaults. - Sanitization is now a single CustomProxyHeader.sanitize() reused by ApiConfig.setCustomProxyHeaders and CustomProxyHeadersService. - Brief comment on redactedValue explaining the length-obscuring design. * Harden custom proxy header validation and load path - validateValue now rejects ASCII control characters (CR/LF/tab/etc.) to prevent header-injection via crafted values. - loadHeaders moves the secure-storage read inside the try block so platform exceptions are caught the same way JSON parse errors are.
This commit is contained in:
145
mobile/lib/widgets/custom_proxy_headers_editor.dart
Normal file
145
mobile/lib/widgets/custom_proxy_headers_editor.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/custom_proxy_header.dart';
|
||||
|
||||
class CustomProxyHeadersEditor extends StatefulWidget {
|
||||
final List<CustomProxyHeader> initialHeaders;
|
||||
final ValueChanged<List<CustomProxyHeader>> onChanged;
|
||||
|
||||
const CustomProxyHeadersEditor({
|
||||
super.key,
|
||||
required this.initialHeaders,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomProxyHeadersEditor> createState() => _CustomProxyHeadersEditorState();
|
||||
}
|
||||
|
||||
class _CustomProxyHeadersEditorState extends State<CustomProxyHeadersEditor> {
|
||||
late List<_HeaderDraft> _drafts;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_drafts = widget.initialHeaders
|
||||
.map((header) => _HeaderDraft(name: header.name, value: header.value))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _notifyChanged() {
|
||||
widget.onChanged(
|
||||
_drafts
|
||||
.map((draft) => CustomProxyHeader(name: draft.name.text, value: draft.value.text))
|
||||
.where((header) => header.isComplete)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _addHeader() {
|
||||
setState(() => _drafts.add(_HeaderDraft()));
|
||||
}
|
||||
|
||||
void _removeHeader(int index) {
|
||||
setState(() {
|
||||
final draft = _drafts.removeAt(index);
|
||||
draft.dispose();
|
||||
});
|
||||
_notifyChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final draft in _drafts) {
|
||||
draft.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (var index = 0; index < _drafts.length; index++) ...[
|
||||
_HeaderRow(
|
||||
draft: _drafts[index],
|
||||
onChanged: _notifyChanged,
|
||||
onRemove: () => _removeHeader(index),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
OutlinedButton.icon(
|
||||
onPressed: _addHeader,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add header'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderRow extends StatelessWidget {
|
||||
final _HeaderDraft draft;
|
||||
final VoidCallback onChanged;
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const _HeaderRow({
|
||||
required this.draft,
|
||||
required this.onChanged,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: draft.name,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Header name',
|
||||
hintText: 'X-Auth-Token',
|
||||
),
|
||||
validator: (value) => CustomProxyHeader.validateName(value ?? ''),
|
||||
onChanged: (_) => onChanged(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: draft.value,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Header value',
|
||||
),
|
||||
obscureText: true,
|
||||
validator: (value) => CustomProxyHeader.validateValue(value ?? ''),
|
||||
onChanged: (_) => onChanged(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Remove header',
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: onRemove,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderDraft {
|
||||
final TextEditingController name;
|
||||
final TextEditingController value;
|
||||
|
||||
_HeaderDraft({String name = '', String value = ''})
|
||||
: name = TextEditingController(text: name),
|
||||
value = TextEditingController(text: value);
|
||||
|
||||
void dispose() {
|
||||
name.dispose();
|
||||
value.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user