ft/adds auto logout and persons_page

This commit is contained in:
itsscb 2023-11-05 22:41:38 +01:00
parent ef7f6c093f
commit 604c2bdd27
6 changed files with 478 additions and 109 deletions

View File

@ -5,9 +5,11 @@ import 'package:app/pb/account.pb.dart';
import 'package:app/pb/account_info.pb.dart';
import 'package:app/pb/person.pb.dart';
import 'package:app/data/database.dart';
import 'package:app/pb/rpc_create_account.pb.dart';
import 'package:app/pb/rpc_get_account.pb.dart';
import 'package:app/pb/rpc_get_account_info.pb.dart';
import 'package:app/pb/rpc_get_person.pb.dart';
import 'package:app/pb/rpc_list_persons.pb.dart';
import 'package:app/pb/rpc_login.pb.dart';
import 'package:app/pb/rpc_refresh_token.pb.dart';
import 'package:app/pb/service_df.pbgrpc.dart';
@ -87,20 +89,48 @@ class BackendService {
}
if (session.accessTokenExpiresAt == null) {
await logout();
return false;
}
if (session.refreshTokenExpiresAt == null) {
await logout();
return false;
}
if (session.refreshTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) {
await logout();
return false;
}
if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) {
Session s = await BackendService.refreshToken(session);
if (s == session) {
return false;
}
}
return true;
}
static Future<bool> createAccount(
{required String email, required String password}) async {
try {
await BackendService.client.createAccount(CreateAccountRequest(
email: email,
password: password,
));
return await login(email: email, password: password);
} on SocketException {
throw FetchDataException('Keine Internet Verbindung');
} on GrpcError catch (err) {
throw FetchDataException(err.message);
} catch (err) {
throw InternalException(err.toString());
}
}
Future<Account> getAccount() async {
Session? session = await _isLoggedIn();
if (session == null) {
@ -177,6 +207,34 @@ class BackendService {
}
}
Future<List<Person>> listPersons() async {
Session session = await Session.session;
if (session.accessTokenExpiresAt == null) {
throw UnauthorizedException('Keine Siztung gefunden');
}
if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) {
session = await refreshToken(session);
if (session.accessTokenExpiresAt == null) {
throw UnauthorizedException('Sitzung ist abgelaufen');
}
}
try {
final ListPersonsResponse response = await _client.listPersons(
ListPersonsRequest(
accountId: session.accountId,
),
options: CallOptions(
metadata: {'Authorization': 'Bearer ${session.accessToken}'}));
return response.persons;
} on SocketException {
throw FetchDataException('Keine Internet Verbindung');
} on GrpcError catch (err) {
throw FetchDataException(err.message);
} catch (err) {
throw InternalException(err.toString());
}
}
// Future<List<Person>> listPersons() async {
// if (_session.accessToken == null) {
// refreshToken();
@ -221,10 +279,11 @@ class BackendService {
}
}
Future<Session> refreshToken(Session session) async {
static Future<Session> refreshToken(Session session) async {
try {
final RefreshTokenResponse response = await _client.refreshToken(
RefreshTokenRequest(refreshToken: session.refreshToken));
final RefreshTokenResponse response = await BackendService.client
.refreshToken(
RefreshTokenRequest(refreshToken: session.refreshToken));
session.accessToken = response.accessToken;
session.accessTokenExpiresAt = response.accessTokenExpiresAt;
session = await Session.updateToken(session);

View File

@ -0,0 +1,29 @@
import 'package:app/model/apis/api_response.dart';
import 'package:app/model/services/backend_service.dart';
import 'package:app/pb/account.pb.dart';
import 'package:app/pb/person.pb.dart';
import 'package:flutter/material.dart';
class PersonsViewModel with ChangeNotifier {
PersonsViewModel() {
listPersons();
}
ApiResponse _apiResponse = ApiResponse.initial('Keine Daten');
final BackendService _service = BackendService();
ApiResponse get response {
return _apiResponse;
}
void listPersons() async {
_apiResponse = ApiResponse.loading('Bereite alles vor');
try {
_apiResponse =
ApiResponse.completed(await _service.listPersons(), 'done');
} catch (e) {
_apiResponse = ApiResponse.error(e.toString());
}
notifyListeners();
}
}

View File

@ -1,8 +1,8 @@
import 'dart:io';
import 'package:app/model/apis/api_response.dart';
import 'package:app/model/services/backend_service.dart';
import 'package:app/model/view_model/account_vm.dart';
import 'package:app/pages/login_overlay.dart';
import 'package:app/pages/persons_page.dart';
import 'package:app/widgets/background.dart';
import 'package:app/widgets/bottom_navigation.dart';
import 'package:app/widgets/bottom_navigation_item.dart';
@ -28,6 +28,12 @@ class _HomePageState extends State<HomePage> {
void _init() async {
_setLoading(true);
_loggedin = await BackendService.isLoggedIn;
// if (!_loggedin) {
// await BackendService.logout();
// Navigator.of(context).pushAndRemoveUntil(
// MaterialPageRoute(builder: (builder) => HomePage()),
// (route) => false);
// }
_setLoading(false);
}
@ -37,6 +43,17 @@ class _HomePageState extends State<HomePage> {
});
}
void _checkResponse(ApiResponse response) async {
print('${response.message}');
if (response.status == Status.ERROR &&
response.message!.contains('unauthorized')) {
await BackendService.logout();
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (builder) => const HomePage()),
(route) => false);
}
}
bool _loading = true;
bool _loggedin = false;
@override
@ -45,114 +62,143 @@ class _HomePageState extends State<HomePage> {
child: ChangeNotifierProvider<AccountViewModel>(
create: (context) => AccountViewModel(),
child: Consumer<AccountViewModel>(
builder: (context, value, child) => Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
// flexibleSpace: Image.asset(
// 'lib/assets/logo_300x200.png',
// // height: 400,
// ),
),
drawer: SideDrawer(
children: [
const Spacer(
flex: 3,
),
SideDrawerItem(
onPressed: () {},
icon: Icons.question_answer,
color: Colors.white,
label: 'About',
),
SideDrawerItem(
onPressed: () {},
icon: Icons.privacy_tip,
color: Colors.white,
label: 'Datenschutz',
),
SideDrawerItem(
onPressed: () {},
icon: Icons.apartment,
color: Colors.white,
label: 'Impressum',
),
const Spacer(
flex: 1,
),
if (_loggedin || value.response.data != null)
builder: (context, value, child) {
_checkResponse(value.response);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
// flexibleSpace: Image.asset(
// 'lib/assets/logo_300x200.png',
// // height: 400,
// ),
),
drawer: SideDrawer(
children: [
const Spacer(
flex: 3,
),
SideDrawerItem(
onPressed: () {},
icon: Icons.logout,
icon: Icons.question_answer,
color: Colors.white,
label: 'Logout',
label: 'About',
),
],
),
bottomNavigationBar: BottomNavigation(
children: [
if (!_loggedin) ...[
BottomNavigationItem(
SideDrawerItem(
onPressed: () {},
icon: Icons.person_add_alt,
icon: Icons.privacy_tip,
color: Colors.white,
label: 'Registrieren',
label: 'Datenschutz',
),
BottomNavigationItem(
onPressed: () async {
_loggedin = await showLogin(context);
},
icon: Icons.login,
color: Colors.white,
label: 'Login',
),
] else
BottomNavigationItem(
SideDrawerItem(
onPressed: () {},
icon: Icons.person_search,
icon: Icons.apartment,
color: Colors.white,
label: 'Personen',
label: 'Impressum',
),
BottomNavigationItem(
onPressed: () {},
icon: Icons.dashboard,
color: Colors.white,
label: 'Dashboard',
),
...[]
],
),
body: Padding(
padding: const EdgeInsets.fromLTRB(16, 40, 16, 16),
child: Center(
child: _loading
? const CircularProgressIndicator(
color: Colors.grey,
)
: Column(
children: [
Image.asset(
'lib/assets/logo_300x200.png',
),
const SizedBox(
height: 40,
),
Text(
'Digitale Spuren auf Knopfdruck entfernen'
.toUpperCase(),
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'sans-serif',
fontSize: 24,
height: 1.6,
fontWeight: FontWeight.normal,
letterSpacing: 6,
),
),
],
),
const Spacer(
flex: 1,
),
if (_loggedin && value.response.data != null)
SideDrawerItem(
onPressed: () async {
setState(() {
_loading = true;
});
await BackendService.logout();
// ignore: use_build_context_synchronously
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (builder) => const HomePage()),
(route) => false);
setState(() {
_loggedin = false;
_loading = false;
});
},
icon: Icons.logout,
color: Colors.white,
label: 'Logout',
),
],
),
),
),
bottomNavigationBar: BottomNavigation(
children: [
if (!_loggedin) ...[
BottomNavigationItem(
onPressed: () async {
final bool res =
await showLogin(context, registration: true);
setState(() {
_loggedin = res;
});
},
icon: Icons.person_add_alt,
color: Colors.white,
label: 'Registrieren',
),
BottomNavigationItem(
onPressed: () async {
final bool res = await showLogin(context);
setState(() {
_loggedin = res;
});
},
icon: Icons.login,
color: Colors.white,
label: 'Login',
),
] else
BottomNavigationItem(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (builder) => const PersonsPage()));
},
icon: Icons.person_search,
color: Colors.white,
label: 'Personen',
),
BottomNavigationItem(
onPressed: () {},
icon: Icons.dashboard,
color: Colors.white,
label: 'Dashboard',
),
...[]
],
),
body: Padding(
padding: const EdgeInsets.fromLTRB(16, 40, 16, 16),
child: Center(
child: _loading
? const CircularProgressIndicator(
color: Colors.grey,
)
: Column(
children: [
Image.asset(
'lib/assets/logo_300x200.png',
),
const SizedBox(
height: 40,
),
Text(
'Digitale Spuren auf Knopfdruck entfernen'
.toUpperCase(),
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'sans-serif',
fontSize: 24,
height: 1.6,
fontWeight: FontWeight.normal,
letterSpacing: 6,
),
),
],
),
),
),
);
},
),
),
);

View File

@ -1,8 +1,10 @@
import 'package:app/model/services/backend_service.dart';
import 'package:app/widgets/background.dart';
import 'package:flutter/material.dart';
import 'package:app/util/validation.dart';
Future<bool> showLogin(BuildContext context) async {
Future<bool> showLogin(BuildContext context,
{bool registration = false}) async {
final formKey = GlobalKey<FormState>();
final mailController = TextEditingController();
final passwordController = TextEditingController();
@ -26,6 +28,23 @@ Future<bool> showLogin(BuildContext context) async {
}
}
void register() {
if (formKey.currentState!.validate()) {
submitted = true;
BackendService.createAccount(
email: mailController.text,
password: passwordController.text,
).then(
(r) {
if (r) {
loggedin = r;
Navigator.pop(context, true);
}
},
);
}
}
await showModalBottomSheet(
useSafeArea: true,
isScrollControlled: true,
@ -48,9 +67,9 @@ Future<bool> showLogin(BuildContext context) async {
const SizedBox(
height: 30,
),
const Text(
'Login',
style: TextStyle(
Text(
registration ? 'Registrieren' : 'Login',
style: const TextStyle(
fontFamily: 'sans-serif',
fontSize: 24,
height: 1.6,
@ -68,6 +87,12 @@ Future<bool> showLogin(BuildContext context) async {
height: 40,
),
TextFormField(
autofocus: true,
// inputFormatters: [
// FilteringTextInputFormatter.allow(
// emailRegExp,
// ),
// ],
controller: mailController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
@ -79,7 +104,7 @@ Future<bool> showLogin(BuildContext context) async {
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
if (value == null || !value.isValidEmail) {
return 'Bitte eine gültige E-Mail Adresse eingeben';
}
return null;
@ -89,6 +114,11 @@ Future<bool> showLogin(BuildContext context) async {
style: const TextStyle(
color: Colors.white,
),
// inputFormatters: [
// FilteringTextInputFormatter.allow(
// passwordRegExp,
// ),
// ],
controller: passwordController,
decoration: const InputDecoration(
fillColor: Color.fromARGB(30, 255, 255, 255),
@ -101,7 +131,7 @@ Future<bool> showLogin(BuildContext context) async {
keyboardType: TextInputType.visiblePassword,
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
if (value == null || !value.isValidPassword) {
return 'Bitte geben Sie Ihr Passwort ein';
}
return null;
@ -122,7 +152,11 @@ Future<bool> showLogin(BuildContext context) async {
child: const Icon(Icons.arrow_back),
),
ElevatedButton(
onPressed: !submitted ? login : null,
onPressed: !submitted
? !registration
? login
: register
: null,
child: const Icon(Icons.login),
),
],

View File

@ -0,0 +1,174 @@
import 'package:app/model/apis/api_response.dart';
import 'package:app/model/services/backend_service.dart';
import 'package:app/model/view_model/persons_vm.dart';
import 'package:app/pages/home_page.dart';
import 'package:app/pb/person.pb.dart';
import 'package:app/widgets/background.dart';
import 'package:app/widgets/bottom_navigation.dart';
import 'package:app/widgets/bottom_navigation_item.dart';
import 'package:app/widgets/side_drawer.dart';
import 'package:app/widgets/side_drawer_item.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class PersonsPage extends StatefulWidget {
const PersonsPage({super.key});
@override
State<PersonsPage> createState() => _PersonsPageState();
}
class _PersonsPageState extends State<PersonsPage> {
@override
void initState() {
super.initState();
_init();
}
void _init() async {
_setLoading(true);
_loggedin = await BackendService.isLoggedIn;
_setLoading(false);
}
void _setLoading(bool loading) {
setState(() {
_loading = loading;
});
}
void _checkResponse(ApiResponse response) {
if (response.status == Status.ERROR &&
response.message!.contains('unauthenticated')) {
BackendService.logout();
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (builder) => const HomePage()),
(route) => false);
}
}
bool _loading = true;
bool _loggedin = false;
List<Widget> _personsList(List<Person> persons) {
final List<Widget> list = [];
for (var p in persons) {
list.add(Card(
color: Colors.black,
child: Text(
'${p.firstname} ${p.lastname}',
style: const TextStyle(color: Colors.white),
),
));
}
return list;
}
@override
Widget build(BuildContext context) {
return Background(
child: ChangeNotifierProvider<PersonsViewModel>(
create: (context) => PersonsViewModel(),
child: Consumer<PersonsViewModel>(
builder: (context, value, child) {
_checkResponse(value.response);
return Scaffold(
floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat,
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
appBar: AppBar(
automaticallyImplyLeading: false,
),
drawer: SideDrawer(
children: [
const Spacer(
flex: 3,
),
SideDrawerItem(
onPressed: () {},
icon: Icons.question_answer,
color: Colors.white,
label: 'About',
),
SideDrawerItem(
onPressed: () {},
icon: Icons.privacy_tip,
color: Colors.white,
label: 'Datenschutz',
),
SideDrawerItem(
onPressed: () {},
icon: Icons.apartment,
color: Colors.white,
label: 'Impressum',
),
const Spacer(
flex: 1,
),
if (_loggedin || value.response.data != null)
SideDrawerItem(
onPressed: () async {
setState(() {
_loading = true;
});
await BackendService.logout();
// ignore: use_build_context_synchronously
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (builder) => const HomePage()),
(route) => false);
setState(() {
_loggedin = false;
_loading = false;
});
},
icon: Icons.logout,
color: Colors.white,
label: 'Logout',
),
],
),
bottomNavigationBar: BottomNavigation(
children: [
BottomNavigationItem(
onPressed: () {},
icon: Icons.dashboard,
color: Colors.white,
label: 'Dashboard',
),
BottomNavigationItem(
onPressed: () {
Navigator.of(context).pop();
},
icon: Icons.home,
color: Colors.white,
label: 'Home',
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: _loading
? const CircularProgressIndicator(
color: Colors.grey,
)
: value.response.status == Status.COMPLETED
? value.response.data.length > 0
? ListView(
children: _personsList(
(value.response.data as List<Person>)))
: const Text('Noch keine Personen angelegt')
: const Text('Lade Daten...'),
),
),
);
},
),
),
);
}
}

View File

@ -0,0 +1,27 @@
final emailRegExp = RegExp(r"^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+");
final nameRegExp =
RegExp(r"^\s*([A-Za-z]{1,}([\.,] |[-']| ))+[A-Za-z]+\.?\s*$");
final phoneRegExp = RegExp(r"^\+?0[0-9]{10}$");
final passwordRegExp = RegExp(r'^.+$');
extension valString on String {
bool get isValidEmail {
return emailRegExp.hasMatch(this);
}
bool get isValidName {
return nameRegExp.hasMatch(this);
}
bool get isValidPassword {
return passwordRegExp.hasMatch(this);
}
bool get isNotEmpty {
return this != trim();
}
bool get isValidPhone {
return phoneRegExp.hasMatch(this);
}
}