mirror of
https://github.com/we-promise/sure.git
synced 2026-05-25 13:34:58 +00:00
* 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.
146 lines
3.6 KiB
Dart
146 lines
3.6 KiB
Dart
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();
|
|
}
|
|
}
|