diff --git a/lib/screens/drawer.dart b/lib/components/drawer.dart similarity index 82% rename from lib/screens/drawer.dart rename to lib/components/drawer.dart index 00abdf7..c0a7433 100644 --- a/lib/screens/drawer.dart +++ b/lib/components/drawer.dart @@ -1,6 +1,5 @@ import 'package:attendance_app/components/navbar_item.dart'; -import 'package:attendance_app/screens/login_page.dart'; -// import 'package:attendance_app/screens/profile.dart'; +import 'package:attendance_app/services/logout.dart'; import 'package:flutter/material.dart'; import 'package:line_awesome_flutter/line_awesome_flutter.dart'; @@ -55,13 +54,6 @@ class DrawerItems extends StatelessWidget { icon: LineAwesomeIcons.user, text: 'Profile', onTap: () { - // Navigator.pop(context); - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => - // ProfileScreen()), // replace ProfileScreen with your destination widget - // ); Navigator.pushNamed(context, '/profile'); }, textStyle: theme.textTheme.bodyLarge @@ -90,13 +82,8 @@ class DrawerItems extends StatelessWidget { NavbarItem( icon: Icons.login_sharp, text: 'Log Out', - onTap: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => const LoginPage(), - ), - ); + onTap: () async { + await LogoutService().logoutAndNavigateToLogin(context); }, textStyle: theme.textTheme.bodyLarge ?.copyWith(color: theme.colorScheme.onSurface), diff --git a/lib/components/profile_card.dart b/lib/components/profile_card.dart new file mode 100644 index 0000000..27b92a2 --- /dev/null +++ b/lib/components/profile_card.dart @@ -0,0 +1,76 @@ +import 'package:attendance_app/services/store.dart'; +import 'package:flutter/material.dart'; + +class ProfileCard extends StatelessWidget { + ProfileCard({ + super.key, + }); + final TokenService _tokenService = TokenService(); + + @override + Widget build(BuildContext context) { + return FutureBuilder?>( + future: _tokenService.getDecodedToken(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError || snapshot.data == null) { + return const Center(child: Text("Failed to load profile")); + } + final decodedToken = snapshot.data!; + final String name = decodedToken['user']['name'] ?? 'Unknown'; + final String role = decodedToken['user']['position'] ?? 'User'; + final String imageUrl = decodedToken['user']['imageUrl'] ?? ''; + + return SizedBox( + // width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + elevation: 10.0, + ), + onPressed: () { + Navigator.pushNamed(context, '/profile'); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 8.0), + CircleAvatar( + radius: 50.0, + backgroundImage: imageUrl.isNotEmpty + ? NetworkImage(imageUrl) + : const AssetImage('assets/profileimg.png') + as ImageProvider, + ), + const SizedBox(height: 8.0), + Text( + name, + style: const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.bold, + ), + ), + Text( + role, + style: TextStyle( + fontSize: 16.0, + color: Colors.grey[800], + letterSpacing: 2.0, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 8cd688f..e3f9676 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,53 +1,80 @@ -import 'package:attendance_app/screens/userdashboard.dart'; -import 'package:attendance_app/screens/adminDashboard.dart'; +import 'package:attendance_app/screens/user_dashboard.dart'; +import 'package:attendance_app/screens/admin_dashboard.dart'; import 'package:attendance_app/screens/user_events.dart'; -import 'package:attendance_app/screens/capturepic.dart'; +import 'package:attendance_app/screens/capture_pic.dart'; import 'package:attendance_app/screens/login_page.dart'; -// import 'package:attendance_app/screens/attendace_history.dart'; import 'package:attendance_app/screens/registration.dart'; import 'package:attendance_app/screens/profile.dart'; -import 'package:attendance_app/screens/locationpage.dart'; +import 'package:attendance_app/screens/location_page.dart'; +import 'package:attendance_app/services/store.dart'; import 'package:attendance_app/src/shared/data.dart'; import 'package:flutter/material.dart'; -import 'dart:async'; - import 'package:get/get.dart'; +import 'dart:async'; Future main() async { fillData(); - // how to capture the image and upload it to the server is remaining - // also all the api calls are remaining - runApp(const MaterialApp(home: MyApp())); + runApp(const MyApp()); // Use MyApp directly } -class MyApp extends StatelessWidget { - // final List cameras; - +class MyApp extends StatefulWidget { const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final TokenService _tokenService = TokenService(); + String initialRoute = "/login"; // Default route to '/login' + + @override + void initState() { + super.initState(); + _checkTokenStatus(); + } + + Future _checkTokenStatus() async { + bool isValid = await _tokenService.isTokenValid(); + if (isValid) { + // Token is valid, check the role + Map? decodedToken = + await _tokenService.getDecodedToken(); + if (decodedToken != null) { + String role = decodedToken['user']['role'] ?? ''; + setState(() { + initialRoute = role == 'ADMIN' ? '/adminDashboard' : '/userDashboard'; + // Navigate to the role-specific page + Get.offNamed(initialRoute); // Navigate here + }); + } + } else { + // Token is not valid or doesn't exist, navigate to login + Get.offNamed('/login'); + } + } + @override Widget build(BuildContext context) { - const String initialRoute = "/login"; return GetMaterialApp( + // Use GetMaterialApp here title: 'Attendance App', debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.white), useMaterial3: true, ), - initialRoute: initialRoute, routes: { '/login': (context) => const LoginPage(), - // '/history': (context) => const History(), // made it as user event page - '/userDashboard': (context) => const userDashboard(), - '/adminDashboard': (context) => const adminDashboard(), + '/userDashboard': (context) => const UserDashboard(), + '/adminDashboard': (context) => const AdminDashboard(), '/usereventpage': (context) => const userEvents(), '/register': (context) => const RegistrationPage(), '/picture': (context) => const CapturePicPage(), '/profile': (context) => const ProfileScreen(), '/location': (context) => const LocationPage(), - - }, + initialRoute: initialRoute, // Default, but overridden by dynamic routing ); } } diff --git a/lib/screens/adminDashboard.dart b/lib/screens/admin_dashboard.dart similarity index 65% rename from lib/screens/adminDashboard.dart rename to lib/screens/admin_dashboard.dart index 1298aea..bd90f15 100644 --- a/lib/screens/adminDashboard.dart +++ b/lib/screens/admin_dashboard.dart @@ -1,17 +1,19 @@ import 'dart:math'; +import 'package:attendance_app/components/profile_card.dart'; +import 'package:attendance_app/services/logout.dart'; import 'package:flutter/material.dart'; -import 'package:attendance_app/screens/drawer.dart'; +import 'package:attendance_app/components/drawer.dart'; import 'package:attendance_app/components/background_painter.dart'; import 'package:attendance_app/components/calendar.dart'; -class adminDashboard extends StatefulWidget { - const adminDashboard({super.key}); +class AdminDashboard extends StatefulWidget { + const AdminDashboard({super.key}); @override - State createState() => _Dashboard1State(); + State createState() => _Dashboard1State(); } -class _Dashboard1State extends State { +class _Dashboard1State extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -22,9 +24,8 @@ class _Dashboard1State extends State { IconButton( icon: const Icon(Icons.logout), tooltip: 'Logout', - onPressed: () { - // logoutlogic - Navigator.pop(context); + onPressed: () async { + await LogoutService().logoutAndNavigateToLogin(context); }, ), ], @@ -41,7 +42,7 @@ class _Dashboard1State extends State { child: SingleChildScrollView( child: Column( children: [ - const ProfileCard(name: "Ram Krishna", role: "Senior Officer"), + ProfileCard(), const SizedBox(height: 20.0), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -68,7 +69,7 @@ class _Dashboard1State extends State { width: min(double.infinity, 400), child: ElevatedButton( onPressed: () { - Navigator.pushNamed(context, '/history'); + Navigator.pushNamed(context, '/usereventpage'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, @@ -90,66 +91,6 @@ class _Dashboard1State extends State { } } -class ProfileCard extends StatelessWidget { - const ProfileCard({ - super.key, - required this.name, - required this.role, - }); - - final String name; - final String role; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: min(double.infinity, 400), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - elevation: 10.0, - ), - onPressed: () { - Navigator.pushNamed(context, '/profile'); - }, - // padding: const EdgeInsets.all(4.0), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 8.0), - const CircleAvatar( - radius: 50.0, - backgroundImage: AssetImage('assets/profileimg.png'), - ), - const SizedBox(height: 8.0), - Text( - name, - style: const TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), - ), - Text( - role, - style: TextStyle( - fontSize: 16.0, - color: Colors.grey[800], - letterSpacing: 2.0, - ), - ), - ], - ), - ), - ), - ); - } -} - class ActionButtons extends StatelessWidget { final String label; final IconData icon; diff --git a/lib/screens/capturepic.dart b/lib/screens/capture_pic.dart similarity index 100% rename from lib/screens/capturepic.dart rename to lib/screens/capture_pic.dart diff --git a/lib/screens/locationpage.dart b/lib/screens/location_page.dart similarity index 100% rename from lib/screens/locationpage.dart rename to lib/screens/location_page.dart diff --git a/lib/screens/login_page.dart b/lib/screens/login_page.dart index 755fe7f..1cb7294 100644 --- a/lib/screens/login_page.dart +++ b/lib/screens/login_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:attendance_app/services/store.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -20,6 +21,8 @@ class _LoginPageState extends State { var size, height, width; TextEditingController userIdController = TextEditingController(); TextEditingController passwordController = TextEditingController(); + bool _isLoading = false; + final TokenService _tokenService = TokenService(); @override void initState() { @@ -35,172 +38,169 @@ class _LoginPageState extends State { return Scaffold( backgroundColor: const Color.fromARGB(255, 255, 255, 255), body: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/Privacy policy-rafiki.png', - width: double.infinity, - height: height / 3, - ), - // const SizedBox( - // height: 25, - // ), - const Text( - "Welcome Back!", - style: TextStyle( - fontSize: 32, - fontWeight: FontWeight.w800, - color: Color.fromARGB(255, 32, 146, 238)), - ), - const SizedBox( - height: 10, - ), - const Text( - "Login to your account!", - style: TextStyle( - fontSize: 16, + child: Stack(children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/Privacy policy-rafiki.png', + width: double.infinity, + height: height / 3, + ), + const Text( + "Welcome Back!", + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w800, + color: Color.fromARGB(255, 32, 146, 238)), + ), + const SizedBox( + height: 10, ), - textAlign: TextAlign.center, - ), - SizedBox( - height: height / 20, - ), - Container( - // height: 55, - decoration: BoxDecoration( - border: Border.all(width: 1, color: Colors.grey), - borderRadius: BorderRadius.circular(10)), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - // mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - width: 10, - ), - Expanded( - child: Container( - key: test1, + const Text( + "Login to your account!", + style: TextStyle( + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + SizedBox( + height: height / 20, + ), + Container( + decoration: BoxDecoration( + border: Border.all(width: 1, color: Colors.grey), + borderRadius: BorderRadius.circular(10)), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + width: 10, + ), + Expanded( + child: Container( + key: test1, alignment: Alignment.centerLeft, - child: TextField( - decoration: const InputDecoration( - hintStyle: TextStyle(fontSize: 15), - - hintText: "Enter User ID", - border: InputBorder.none, - prefixIcon: Icon(Icons.perm_identity_rounded), + child: TextField( + decoration: const InputDecoration( + hintStyle: TextStyle(fontSize: 15), + hintText: "Enter Employee Id", + border: InputBorder.none, + prefixIcon: Icon(Icons.perm_identity_rounded), + ), + controller: userIdController, ), - controller: userIdController, ), ), - ), - ], + ], + ), ), - ), - - const SizedBox( - height: 20, - ), - - Container( - // height: 55, - decoration: BoxDecoration( - border: Border.all(width: 1, color: Colors.grey), - borderRadius: BorderRadius.circular(10)), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - // mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - width: 10, - ), - Expanded( - child: Container( - key: test2, + const SizedBox( + height: 20, + ), + Container( + decoration: BoxDecoration( + border: Border.all(width: 1, color: Colors.grey), + borderRadius: BorderRadius.circular(10)), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + width: 10, + ), + Expanded( + child: Container( + key: test2, alignment: Alignment.centerLeft, - child: TextField( - decoration: const InputDecoration( - hintStyle: TextStyle(fontSize: 15), - hintText: "Enter Password", - border: InputBorder.none, - prefixIcon: Icon(Icons.key_outlined), + child: TextField( + decoration: const InputDecoration( + hintStyle: TextStyle(fontSize: 15), + hintText: "Enter Password", + border: InputBorder.none, + prefixIcon: Icon(Icons.key_outlined), + ), + controller: passwordController, + obscureText: true, ), - controller: passwordController, - obscureText: true, ), ), - ), - ], + ], + ), ), - ), - - SizedBox( - height: height / 20, - ), - SizedBox( - key: test3, + SizedBox( + height: height / 20, + ), + SizedBox( + key: test3, width: double.infinity, - height: 45, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueAccent, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10))), - //onPressed:_loginUser, - onPressed:(){ - Get.snackbar("Logged In", "Welcome Back {User}!", - duration: const Duration(seconds: 1), - colorText: Colors.white, - backgroundColor: Colors.green); - Navigator.pushNamed(context, '/userDashboard'); - }, - child: const Text("Login")), - ), - const SizedBox( - height: 10.0, - ), - SizedBox( - key: test4, + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10))), + onPressed: _loginUser, + child: const Text("Login")), + ), + const SizedBox( + height: 10.0, + ), + SizedBox( + key: test4, width: double.infinity, - height: 45, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - // backgroundColor: Colors.blueAccent[100], - // foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10))), - onPressed: () { - Navigator.pushNamed(context, '/register'); - }, - child: const Text("New User ? Register")), - ) - ], + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10))), + onPressed: () { + Navigator.pushNamed(context, '/register'); + }, + child: const Text("New User ? Register")), + ) + ], + ), ), ), - ), + if (_isLoading) + Container( + color: Colors.black.withOpacity(0.5), + child: const Center( + child: CircularProgressIndicator(), + ), + ) + ]), ), ); } // Function to handle the login process void _loginUser() async { - String userId = userIdController.text; + setState(() { + _isLoading = true; + }); + + int userId = int.parse(userIdController.text); String password = passwordController.text; - if (userId.isEmpty || password.isEmpty) { - Get.snackbar("Error", "Please enter both User ID and Password", + if (userId == null || password.isEmpty || userId.isLowerThan(1)) { + Get.snackbar("Error", "Please enter valid Employee Id and Password", colorText: Colors.white, backgroundColor: Colors.red); + setState(() { + _isLoading = false; + }); return; } + // Make API call to login user try { // Replace with your actual API endpoint final response = await http.post( - Uri.parse('http://localhost:3000/login'), + Uri.parse('http://localhost:3000/user/login'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'employee_id': userId, @@ -211,16 +211,25 @@ class _LoginPageState extends State { if (response.statusCode == 200) { final responseData = jsonDecode(response.body); String token = responseData['token']; - // Decode token and navigate based on role _navigateBasedOnRole(token); - } else { - Get.snackbar("Error", "Invalid login credentials", + } else if (response.statusCode == 404) { + Get.snackbar("Error: No user found", "Please Check Employee Id", + colorText: Colors.white, backgroundColor: Colors.red); + } else if (response.statusCode == 400) { + Get.snackbar("Error: Invalid Password", "Please try again", + colorText: Colors.white, backgroundColor: Colors.red); + } else if (response.statusCode == 500) { + Get.snackbar("Sorry!", "Server Error, Please Try Again Later", colorText: Colors.white, backgroundColor: Colors.red); } } catch (e) { - Get.snackbar("Error", "An error occurred during login", - colorText: Colors.white, backgroundColor: Color.fromARGB(255, 244, 130, 54)); + Get.snackbar("Something went wrong", "Please Try Again Later", + colorText: Colors.white, backgroundColor: Colors.red); + } finally { + setState(() { + _isLoading = false; + }); } } @@ -228,7 +237,7 @@ class _LoginPageState extends State { void _navigateBasedOnRole(String token) { Map decodedToken = JwtDecoder.decode(token); String role = decodedToken['user']['role']; - + _tokenService.saveToken(token); if (role == 'ADMIN') { Get.toNamed('/adminDashboard'); } else if (role == 'USER') { @@ -238,4 +247,4 @@ class _LoginPageState extends State { colorText: Colors.white, backgroundColor: Colors.red); } } -} \ No newline at end of file +} diff --git a/lib/screens/registration.dart b/lib/screens/registration.dart index 3c870f5..b93b331 100644 --- a/lib/screens/registration.dart +++ b/lib/screens/registration.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; // import 'package:attendance_app/screens/capturepic.dart'; // registration page starts @@ -12,255 +16,298 @@ class RegistrationPage extends StatefulWidget { class RegistrationPageState extends State { final GlobalKey _formKey = GlobalKey(); - var size,height,width; - final TextEditingController _nameController = TextEditingController(); - final TextEditingController _ageController = TextEditingController(); + var size, height, width; + bool _isLoading = false; + TextEditingController _nameController = TextEditingController(); final TextEditingController _positionController = TextEditingController(); - final TextEditingController _bloodGroupController = TextEditingController(); - final TextEditingController _employeeIdController = TextEditingController(); + TextEditingController _employeeIdController = TextEditingController(); + TextEditingController _passwordController = TextEditingController(); - void _nextPage() async { - await Navigator.pushNamed(context, '/picture'); - Navigator.pushNamed(context, '/login'); - // if (_formKey.currentState?.validate() ?? false) { - // Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) => CapturePicPage(cameras: widget.cameras)), - // ); - } + void _submit() async { + if (_formKey.currentState!.validate()) { + setState(() { + _isLoading = true; + }); + bool _error = false; + + // Access form values + String name = _nameController.text.trim(); + String position = _positionController.text.trim(); + int employeeId = int.parse(_employeeIdController.text.trim()); + String password = _passwordController.text.trim(); - void _submit() { - // submission logic here - // upload the data and the captured picture + try { + final response = await http.post( + Uri.parse('http://localhost:3000/user/register'), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'role': "USER", + 'employee_id': employeeId, + 'name': name, + 'position': position, + 'password': password + }), + ); + if (response.statusCode == 200) { + Get.snackbar( + "Profile Add successfully", "Please Provide Picture Next", + colorText: Colors.white, backgroundColor: Colors.green); + } else { + final responseData = jsonDecode(response.body); + Get.snackbar("Error", responseData["msg"], + colorText: Colors.white, backgroundColor: Colors.red); + _error = true; + } + } catch (e) { + print(e); + Get.snackbar("Error", "An error occurred, please try again", + colorText: Colors.white, backgroundColor: Colors.red); + _error = true; + } finally { + setState(() { + _isLoading = false; + }); + } + if (_error) { + return; + } + await Navigator.pushNamed(context, '/picture'); + + /// TODO: upload the picture here next, need to implement the picture upload + Navigator.pushNamed(context, '/login'); + } } @override Widget build(BuildContext context) { - size = MediaQuery.of(context).size; + size = MediaQuery.of(context).size; height = size.height; width = size.width; return Scaffold( backgroundColor: Colors.white, - body: - SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 30,), + body: SafeArea( + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + ), child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.asset( - "assets/Police car-rafiki.png", - height: height/3, - width: double.infinity, - ), - const Text("Register Here!", - style: TextStyle( - fontFamily: "OpenSans-VariableFont_wdth,wght", - color: Color.fromARGB(221, 48, 124, 244), - fontSize: 32, - fontWeight: FontWeight.w800 - ), - ), - const SizedBox( - height: 8, - ), - const Text("Register Now for Our Facial Attendance App"), - SizedBox( - height: height/30, - ), - Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.black12, - ), - color: Colors.grey[100], - borderRadius: - const BorderRadius.all(Radius.circular(10))), - child: const TextField( - - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'Employee ID', - contentPadding: EdgeInsets.all(10)), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + "assets/Police car-rafiki.png", + height: height / 3, + width: double.infinity, ), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.black12, - ), - color: Colors.grey[100], - borderRadius: - const BorderRadius.all(Radius.circular(10))), - child: const TextField( - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'Full Name', - contentPadding: EdgeInsets.all(10)), + const Text( + "Register Here!", + style: TextStyle( + fontFamily: "OpenSans-VariableFont_wdth,wght", + color: Color.fromARGB(221, 48, 124, 244), + fontSize: 32, + fontWeight: FontWeight.w800), ), - ), - const SizedBox( - height: 20, - ), - Row( - children: [ - Expanded( - flex: 1, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.black12, - ), - color: Colors.grey[100], - borderRadius: - const BorderRadius.all(Radius.circular(10))), - child: const TextField( - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'Age', - contentPadding: EdgeInsets.all(10)), + const SizedBox( + height: 8, + ), + const Text("Register Now for Our Facial Attendance App"), + SizedBox( + height: height / 30, + ), + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black12, ), - ), + color: Colors.grey[100], + borderRadius: + const BorderRadius.all(Radius.circular(10))), + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter Employee ID'; + } + if (int.tryParse(value) == null) { + return 'Employee ID must be a number'; + } + return null; + }, + controller: _employeeIdController, + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Employee ID', + contentPadding: EdgeInsets.all(10)), ), - const SizedBox( - width: 15, + ), + const SizedBox( + height: 16, + ), + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black12, + ), + color: Colors.grey[100], + borderRadius: + const BorderRadius.all(Radius.circular(10))), + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your name'; + } + return null; + }, + controller: _nameController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'Full Name', + contentPadding: EdgeInsets.all(10)), ), - Expanded( - flex: 2, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.black12, + ), + const SizedBox( + height: 20, + ), + Row( + children: [ + Expanded( + flex: 1, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black12, + ), + color: Colors.grey[100], + borderRadius: const BorderRadius.all( + Radius.circular(10))), + child: const Padding( + padding: EdgeInsets.all(13.0), + child: Text( + 'USER', + style: TextStyle( + fontSize: 15, + color: Color.fromARGB(255, 228, 112, 112), + fontWeight: FontWeight.bold), ), - color: Colors.grey[100], - borderRadius: - const BorderRadius.all(Radius.circular(10))), - child: const TextField( - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'Position', - contentPadding: EdgeInsets.all(10)), + ), ), ), - ), - ], - ), - const SizedBox( - height: 20, - ), - const SizedBox( - height: 40, - ), - - SizedBox( - width: double.infinity, - height: 45, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueAccent, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10))), - onPressed: _nextPage, - child: const Text('Register'), + const SizedBox( + width: 15, + ), + Expanded( + flex: 2, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black12, + ), + color: Colors.grey[100], + borderRadius: const BorderRadius.all( + Radius.circular(10))), + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your position'; + } + return null; + }, + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Position', + contentPadding: EdgeInsets.all(10)), + controller: _positionController, + ), + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + // Password + Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black12, + ), + color: Colors.grey[100], + borderRadius: + const BorderRadius.all(Radius.circular(10))), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + width: 10, + ), + Expanded( + child: Container( + alignment: Alignment.centerLeft, + child: TextFormField( + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your password'; + } + if (value.length < 6) { + return 'Password must be at least 6 characters'; + } + return null; + }, + decoration: const InputDecoration( + hintText: "Enter Password", + border: InputBorder.none, + prefixIcon: Icon(Icons.key_outlined), + contentPadding: EdgeInsets.all(10)), + controller: _passwordController, + obscureText: true, + ), + ), ), - ), - // Form( - // key: _formKey, - // child: Column( - // children: [ - // TextFormField( - // controller: _employeeIdController, - // decoration: const InputDecoration(labelText: 'Employee ID'), - // validator: (value) { - // if (value == null || value.isEmpty) { - // return 'Please enter your Employee ID'; - // } - // return null; - // }, - // ), - // SizedBox( - // height: 10, - // ), - // Row( - // children: [ - // TextFormField( - // controller: _nameController, - // decoration: const InputDecoration( - // labelText: 'First Name', - // border: OutlineInputBorder( - // borderRadius: BorderRadius.all(Radius.circular(10)) - // ) - // ), - // validator: (value) { - // if (value == null || value.isEmpty) { - // return 'Please enter your Name'; - // } - // return null; - // }, - // ), - // TextFormField( - // // controller: _nameController, - // decoration: const InputDecoration( - // labelText: 'First Name', - // border: OutlineInputBorder( - // borderRadius: BorderRadius.all(Radius.circular(10)) - // ) - // ), - // validator: (value) { - // if (value == null || value.isEmpty) { - // return 'Please enter your Name'; - // } - // return null; - // }, - // ), - // ], - // ), - // SizedBox( - // height: 10, - // ), - // TextFormField( - // controller: _ageController, - // decoration: const InputDecoration(labelText: 'Age'), - // validator: (value) { - // if (value == null || value.isEmpty) { - // return 'Please enter your Age'; - // } - // return null; - // }, - // ), - // TextFormField( - // controller: _positionController, - // decoration: const InputDecoration(labelText: 'Position'), - // ), - // TextFormField( - // controller: _bloodGroupController, - // decoration: const InputDecoration(labelText: 'Blood Group'), - // ), - // const SizedBox(height: 24), - // ElevatedButton( - // onPressed: _nextPage, - // child: const Text('Next'), - // ), - // ], - - // ) - - - // ), - - - - ], + ], + ), + ), + const SizedBox( + height: 20, + ), + const SizedBox( + height: 40, + ), + SizedBox( + width: double.infinity, + height: 45, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10))), + onPressed: _submit, + child: const Text('Register'), + ), + ), + ], + ), ), ), ), - ), + if (_isLoading) + Container( + height: height, + width: width, + color: Colors.black.withOpacity(0.5), + child: const Center( + child: CircularProgressIndicator(), + ), + ), + ], + ), + ), ); } } diff --git a/lib/screens/userdashboard.dart b/lib/screens/user_dashboard.dart similarity index 64% rename from lib/screens/userdashboard.dart rename to lib/screens/user_dashboard.dart index 88901be..d39685f 100644 --- a/lib/screens/userdashboard.dart +++ b/lib/screens/user_dashboard.dart @@ -1,19 +1,23 @@ import 'dart:math'; +import 'package:attendance_app/components/profile_card.dart'; +import 'package:attendance_app/services/logout.dart'; +import 'package:attendance_app/services/store.dart'; import 'package:flutter/material.dart'; -import 'package:attendance_app/screens/drawer.dart'; +import 'package:attendance_app/components/drawer.dart'; import 'package:attendance_app/screens/filter.dart'; import 'package:attendance_app/components/background_painter.dart'; import 'package:attendance_app/components/calendar.dart'; -// import 'package:get/get.dart'; -class userDashboard extends StatefulWidget { - const userDashboard({super.key}); +class UserDashboard extends StatefulWidget { + const UserDashboard({super.key}); @override - State createState() => _DashboardState(); + State createState() => _DashboardState(); } -class _DashboardState extends State { +class _DashboardState extends State { + final TokenService _tokenService = TokenService(); + @override Widget build(BuildContext context) { return Scaffold( @@ -24,18 +28,20 @@ class _DashboardState extends State { IconButton( icon: const Icon(Icons.logout), tooltip: 'Logout', - onPressed: () { - // logoutlogic - Navigator.pop(context); + onPressed: () async { + await LogoutService().logoutAndNavigateToLogin(context); + // // logoutlogic + // _tokenService.deleteToken(); + // Navigator.pushNamed(context, '/login'); }, ), IconButton( - onPressed: () => Navigator.of(context) - .push(MaterialPageRoute(builder: (_) => const SearchPage())), - icon: Icon( - Icons.search_rounded, - color: Theme.of(context).primaryColorDark, - )), + onPressed: () => Navigator.of(context) + .push(MaterialPageRoute(builder: (_) => const SearchPage())), + icon: Icon( + Icons.search_rounded, + color: Theme.of(context).primaryColorDark, + )), ], ), drawer: const Drawer(child: DrawerItems()), @@ -50,7 +56,7 @@ class _DashboardState extends State { child: SingleChildScrollView( child: Column( children: [ - const ProfileCard(name: "Ram Krishna", role: "Sub Inspector"), + ProfileCard(), const SizedBox(height: 20.0), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -99,66 +105,6 @@ class _DashboardState extends State { } } -class ProfileCard extends StatelessWidget { - const ProfileCard({ - super.key, - required this.name, - required this.role, - }); - - final String name; - final String role; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: min(double.infinity, 400), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - elevation: 10.0, - ), - onPressed: () { - Navigator.pushNamed(context, '/profile'); - }, - // padding: const EdgeInsets.all(4.0), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 8.0), - const CircleAvatar( - radius: 50.0, - backgroundImage: AssetImage('assets/profileimg.png'), - ), - const SizedBox(height: 8.0), - Text( - name, - style: const TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), - ), - Text( - role, - style: TextStyle( - fontSize: 16.0, - color: Colors.grey[800], - letterSpacing: 2.0, - ), - ), - ], - ), - ), - ), - ); - } -} - class ActionButtons extends StatelessWidget { final String label; final IconData icon; diff --git a/lib/services/logout.dart b/lib/services/logout.dart new file mode 100644 index 0000000..03afa2e --- /dev/null +++ b/lib/services/logout.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; // or use Navigator.pushNamed if you're not using GetX +import 'package:attendance_app/services/store.dart'; // TokenService path + +class LogoutService { + final TokenService _tokenService = TokenService(); + + Future logoutAndNavigateToLogin(BuildContext context) async { + // Delete the token + await _tokenService.deleteToken(); + + // Navigate to the login page + // Navigator.pushNamed(context, '/login'); + + // This clears the navigation stack + Get.offAllNamed('/login'); + } +} diff --git a/lib/services/store.dart b/lib/services/store.dart new file mode 100644 index 0000000..cf74909 --- /dev/null +++ b/lib/services/store.dart @@ -0,0 +1,41 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; + +// TODO: Understand how it saves the token in keycahin in ios + +class TokenService { + final _storage = const FlutterSecureStorage(); + + Future saveToken(String token) async { + await _storage.write(key: 'jwt', value: token.split(' ')[1]); + } + + Future getToken() async { + return await _storage.read(key: 'jwt'); + } + + Future deleteToken() async { + await _storage.delete(key: 'jwt'); + } + + Future isTokenValid() async { + final token = await getToken(); + + // If no token exists, return false + if (token == null) { + return false; + } + + // Check if the token has expired + return !JwtDecoder.isExpired(token); + } + + Future?> getDecodedToken() async { + final token = await getToken(); + + if (token == null) { + return null; + } + return JwtDecoder.decode(token); + } +} diff --git a/lib/src/shared/utils.dart b/lib/src/shared/utils.dart index 7bfc30d..3e40520 100644 --- a/lib/src/shared/utils.dart +++ b/lib/src/shared/utils.dart @@ -58,13 +58,13 @@ bool is_Past(DateTime? a, DateTime? b) { if (a == null || b == null) { return false; } - if(a.year < b.year) { + if (a.year < b.year) { return true; - } else if(a.year == b.year){ - if(a.month < b.month) { + } else if (a.year == b.year) { + if (a.month < b.month) { return true; - } else if(a.month == b.month){ - if(a.day < b.day) return true; + } else if (a.month == b.month) { + if (a.day < b.day) return true; } } return false; @@ -81,8 +81,8 @@ bool is_Absent(DateTime? a) { class Event { final String title; final String location; - // final int x,y; - // To Do : add new attributes to make this as per the desired Event struct. + // final int x,y; + // To Do : add new attributes to make this as per the desired Event struct. const Event( this.title, @@ -99,12 +99,15 @@ final kEvents = LinkedHashMap>( )..addAll(_kEventSource); // dummy data generator -final _kEventSource = { for (var item in List.generate(50, (index) => index)) DateTime.utc(kFirstDay.year, kFirstDay.month, item * 5) : List.generate( - item % 4 + 1, (index) => Event('Event $item | ${index + 1}' , 'Sample Location')) } - ..addAll({ +final _kEventSource = { + for (var item in List.generate(50, (index) => index)) + DateTime.utc(kFirstDay.year, kFirstDay.month, item * 5): List.generate( + item % 4 + 1, + (index) => Event('Event $item | ${index + 1}', 'Sample Location')) +}..addAll({ kToday: [ - const Event('SnT Code :<' , 'OAT'), - const Event('Finishing Party ~yash' , 'Mama Mio :)'), + const Event('SnT Code :<', 'OAT'), + const Event('Finishing Party ~yash', 'Mama Mio :)'), ], }); diff --git a/pubspec.yaml b/pubspec.yaml index 83239e5..a6513c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: intl: ^0.19.0 jwt_decoder: ^2.0.1 http: ^1.2.2 + flutter_secure_storage: ^9.2.2 dev_dependencies: flutter_test: diff --git a/server/controllers/eventController.js b/server/controllers/eventController.js index 3a0a3e8..cfb93e2 100644 --- a/server/controllers/eventController.js +++ b/server/controllers/eventController.js @@ -4,7 +4,6 @@ const EventQueries = require("../Queries/eventQueries"); const createEvent = async (req, res) => { try { - // FIXME: destructuring as array why? what do you expect in input? const { title, description, date, locationName, xCoordinate, yCoordinate } = req.body; console.log(title); @@ -40,7 +39,6 @@ const createEvent = async (req, res) => { // TODO: as if the error occurs in between, the event is created but can never be used const createSubEvent = async (req, res) => { try { - // FIXME: Why array? const { title, description, date, locationName, xCoordinate, yCoordinate } = req.body; const location = { diff --git a/server/controllers/userController.js b/server/controllers/userController.js index 6e7bfa4..c0cb0c1 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -1,5 +1,6 @@ const { PrismaClient } = require("@prisma/client"); +// TODO: write error handling for the database downtime const prisma = new PrismaClient(); const bcrypt = require("bcrypt"); const jwt = require("jsonwebtoken"); @@ -23,7 +24,7 @@ const registerUser = async (req, res) => { }); console.log("User Registered"); } catch (err) { - res.status(500).send("Server Error"); + res.status(500).json({ msg: "Server Error" }); } }; // login user @@ -44,12 +45,19 @@ const login = async (req, res) => { // payload const payload = { user: { + // TODO: include the profile pic url field userId: user.profile.employeeId, role: user.role, + name: user.profile.name, + position: user.profile.position, }, }; + // Options for JWT, including expiry time + const options = { + expiresIn: "7d", // Token expires in 7 days + }; // Sign the token - const token = jwt.sign(payload, process.env.JWT_SECRET); + const token = jwt.sign(payload, process.env.JWT_SECRET, options); const literal = "Bearer ".concat(token); // send the token res.json({ @@ -58,7 +66,7 @@ const login = async (req, res) => { }); } catch (err) { console.error(err.message); - res.status(500).send("Server Error"); + res.status(500).json({ msg: "Invalid Password" }); } }; // TODO: @@ -71,19 +79,20 @@ const markAttendance = async (req, res) => { log, locationName, } = req.body; - - try{ - if(!id || !lat || !log || !locationName){ -return res.status(400).json({ - success: false, - error: { - code: 'MISSING_PARAMETERS', - message: 'Please provide all the required details: id, lat, log, and locationName' - } -}); + + try { + if (!id || !lat || !log || !locationName) { + return res.status(400).json({ + success: false, + error: { + code: "MISSING_PARAMETERS", + message: + "Please provide all the required details: id, lat, log, and locationName", + }, + }); } - if(isNaN(parseFloat(lat)) || isNaN(parseFloat(log))){ - return res.status(400).json({msg:"Invalid Coordinates"}) + if (isNaN(parseFloat(lat)) || isNaN(parseFloat(log))) { + return res.status(400).json({ msg: "Invalid Coordinates" }); } const location = await prisma.location.create({ data: { @@ -107,8 +116,7 @@ return res.status(400).json({ return res.status(500).json({ msg: "Error in marking attendance" }); } res.status(200).send("Marked Attendance!"); - } - catch (err) { + } catch (err) { console.error(err.message); res.status(500).send("Server Error"); } @@ -118,7 +126,8 @@ return res.status(400).json({ const userEvents = async (req, res) => { console.log("Get All Events got hit!"); try { - const { employeeId } = req.body; + // const { employeeId } = req.body; + const employeeId = req.userId; const { error, events } = await getUserEvents(employeeId); if (error) { return res.status(error.code).json({ msg: error.msg }); @@ -126,7 +135,7 @@ const userEvents = async (req, res) => { res.status(200).json({ events }); } catch (err) { console.error(err.message); - res.status(400).send("Bad Request"); + res.status(400).json({ msg: "Bad Request" }); } }; @@ -142,7 +151,7 @@ const getAttendance = async (req, res) => { res.status(200).json({ Attendance: user.Attendance }); } catch { console.error(err.message); - res.status(400).send("Bad Request"); + res.status(400).json({ msg: "Bad Request" }); } }; @@ -156,5 +165,4 @@ module.exports = { markAttendance, userEvents, getAttendance, - }; diff --git a/server/middlewares/authenticate.js b/server/middlewares/authenticate.js index 8236de9..b7f1131 100644 --- a/server/middlewares/authenticate.js +++ b/server/middlewares/authenticate.js @@ -2,6 +2,7 @@ const { getUser } = require("../Queries/userQueries"); const jwt = require("jsonwebtoken"); +// Need to add time limit or expiry time for token exports.authenticateToken = (req, res, next) => { console.log("authenticate middleware got hit"); const authHeader = req.headers["authorization"]; @@ -15,6 +16,9 @@ exports.authenticateToken = (req, res, next) => { req.userId = decoded.user.userId; next(); } catch (err) { + if (err.name === "TokenExpiredError") { + return res.status(401).json({ message: "Token expired" }); + } res .status(401) .json({ message: "Verification failed, token is not valid" }); diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js index a89cb91..08df8a3 100644 --- a/server/routes/userRoutes.js +++ b/server/routes/userRoutes.js @@ -6,13 +6,15 @@ const { markAttendance, } = require("../controllers/userController"); +const { authenticateToken } = require("../middlewares/authenticate"); + const express = require("express"); const router = express.Router(); router.post("/register", registerUser); router.post("/login", login); -router.post("/events", userEvents); +router.post("/events", authenticateToken, userEvents); router.post("/attendance", getAttendance); router.post("/markattendance", markAttendance); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/test/login_page_test.dart b/test/login_page_test.dart index 14c1bfe..c5a59c3 100644 --- a/test/login_page_test.dart +++ b/test/login_page_test.dart @@ -10,9 +10,9 @@ void main(){ ), ); - // username input - await tester.enterText(find.byKey(const Key('Test1')), 'testuser'); - expect(find.text('testuser'), findsOneWidget); + // username input, integer + await tester.enterText(find.byKey(const Key('Test1')),'230626'); + expect(find.text('230626'), findsOneWidget); // password input await tester.enterText(find.byKey(const Key('Test2')), 'password123');