add support for token validation on login page
This commit is contained in:
@@ -1,10 +1,37 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:mileograph_flutter/services/authservice.dart';
|
import 'package:mileograph_flutter/services/authservice.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class LoginScreen extends StatelessWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginScreen> createState() => _LoginScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginScreenState extends State<LoginScreen> {
|
||||||
|
bool _checkingSession = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _checkExistingSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkExistingSession() async {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
try {
|
||||||
|
final valid = await auth.validateStoredToken();
|
||||||
|
if (!valid) return;
|
||||||
|
await auth.tryRestoreSession();
|
||||||
|
if (!mounted) return;
|
||||||
|
context.go('/');
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _checkingSession = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -24,7 +51,7 @@ class LoginScreen extends StatelessWidget {
|
|||||||
color: Theme.of(context).textTheme.bodyLarge?.color,
|
color: Theme.of(context).textTheme.bodyLarge?.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextSpan(
|
const TextSpan(
|
||||||
text: "O",
|
text: "O",
|
||||||
style: TextStyle(color: Colors.red),
|
style: TextStyle(color: Colors.red),
|
||||||
),
|
),
|
||||||
@@ -35,18 +62,27 @@ class LoginScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontFamily: "Tomatoes",
|
fontFamily: "Tomatoes",
|
||||||
fontSize: 50,
|
fontSize: 50,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (_checkingSession)
|
||||||
const SizedBox(height: 50),
|
const Padding(
|
||||||
LoginPanel(),
|
padding: EdgeInsets.only(top: 12),
|
||||||
],
|
child: SizedBox(
|
||||||
),
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 50),
|
||||||
|
const LoginPanel(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:mileograph_flutter/components/calculator/calculator.dart';
|
import 'package:mileograph_flutter/components/calculator/calculator.dart';
|
||||||
import 'package:mileograph_flutter/components/pages/traction.dart';
|
import 'package:mileograph_flutter/components/pages/traction.dart';
|
||||||
@@ -444,10 +445,12 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
});
|
});
|
||||||
_startController.text = data['start'] ?? '';
|
_startController.text = data['start'] ?? '';
|
||||||
_endController.text = data['end'] ?? '';
|
_endController.text = data['end'] ?? '';
|
||||||
_headcodeController.text = data['headcode'] ?? '';
|
_headcodeController.text =
|
||||||
|
data['headcode'] is String ? data['headcode'].toUpperCase() : '';
|
||||||
_notesController.text = data['notes'] ?? '';
|
_notesController.text = data['notes'] ?? '';
|
||||||
_mileageController.text = data['mileage'] ?? '';
|
_mileageController.text = data['mileage'] ?? '';
|
||||||
_networkController.text = data['network'] ?? '';
|
_networkController.text =
|
||||||
|
data['network'] is String ? data['network'].toUpperCase() : '';
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Ignore corrupt draft data
|
// Ignore corrupt draft data
|
||||||
} finally {
|
} finally {
|
||||||
@@ -572,9 +575,11 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _headcodeController,
|
controller: _headcodeController,
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
inputFormatters: const [_UpperCaseTextFormatter()],
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Headcode',
|
labelText: 'Headcode',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
@@ -582,6 +587,8 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
),
|
),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _networkController,
|
controller: _networkController,
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
inputFormatters: const [_UpperCaseTextFormatter()],
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Network',
|
labelText: 'Network',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
@@ -817,6 +824,21 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _UpperCaseTextFormatter extends TextInputFormatter {
|
||||||
|
const _UpperCaseTextFormatter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue,
|
||||||
|
TextEditingValue newValue,
|
||||||
|
) {
|
||||||
|
return newValue.copyWith(
|
||||||
|
text: newValue.text.toUpperCase(),
|
||||||
|
selection: newValue.selection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _CalculatorPickerPage extends StatelessWidget {
|
class _CalculatorPickerPage extends StatelessWidget {
|
||||||
const _CalculatorPickerPage({required this.onResult});
|
const _CalculatorPickerPage({required this.onResult});
|
||||||
final ValueChanged<RouteResult> onResult;
|
final ValueChanged<RouteResult> onResult;
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ class AuthService extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> tryRestoreSession() async {
|
Future<void> tryRestoreSession() async {
|
||||||
if (_restoring || _user != null) return;
|
if (_restoring || _user != null) return;
|
||||||
_restoring = true;
|
_restoring = true;
|
||||||
try {
|
try {
|
||||||
// read token from secure storage (with fallback)
|
// read token from secure storage (with fallback)
|
||||||
final token = await _tokenStorage.getToken();
|
final token = await _tokenStorage.getToken();
|
||||||
if (token == null || token.isEmpty) return;
|
if (token == null || token.isEmpty) return;
|
||||||
@@ -100,6 +100,24 @@ class AuthService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> validateStoredToken() async {
|
||||||
|
final token = await _tokenStorage.getToken();
|
||||||
|
if (token == null || token.isEmpty) return false;
|
||||||
|
try {
|
||||||
|
await api.get(
|
||||||
|
'/validate',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'accept': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
await _clearToken();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _persistToken(String token) async {
|
Future<void> _persistToken(String token) async {
|
||||||
await _tokenStorage.setToken(token);
|
await _tokenStorage.setToken(token);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.1.2+1
|
version: 0.1.3+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user