diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index 599e5219..1e1d277c 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -5,1286 +5,1310 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml index 2d486b53..ad1470cf 100644 --- a/.idea/libraries/Dart_SDK.xml +++ b/.idea/libraries/Dart_SDK.xml @@ -1,29 +1,29 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml index ecb7d10a..8f21f709 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,14 +5,11 @@ - + - - - - - - + + + - { - "keyToString": { - "Flutter.Unnamed.executor": "Run", - "Flutter.main.dart.executor": "Run", - "Flutter.main.executor": "Run", - "RunOnceActivity.OpenProjectViewOnStart": "true", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.cidr.known.project.marker": "true", - "RunOnceActivity.git.unshallow": "true", - "RunOnceActivity.readMode.enableVisualFormatting": "true", - "cf.first.check.clang-format": "false", - "cidr.known.project.marker": "true", - "com.google.services.firebase.aqiPopupShown": "true", - "dart.analysis.tool.window.visible": "false", - "git-widget-placeholder": "master", - "ignore.virus.scanning.warn.message": "true", - "io.flutter.reload.alreadyRun": "true", - "kotlin-language-version-configured": "true", - "last_opened_file_path": "/Users/yatikajena/Desktop/iitgHABapp/frontend2/ios/Runner", - "settings.editor.selected.configurable": "preferences.pluginManager", - "show.migrate.to.gradle.popup": "false" + +}]]> diff --git a/frontend2/lib/constants/endpoint.dart b/frontend2/lib/constants/endpoint.dart index 26f23f62..95fc8670 100644 --- a/frontend2/lib/constants/endpoint.dart +++ b/frontend2/lib/constants/endpoint.dart @@ -1,6 +1,11 @@ const String baseUrl = "https://hab.codingclub.in/api"; const String authUrl = "https://hab.codingclub.in/api"; +class MessRebateEndpoints { + static const String sendApplication = '$baseUrl/leave/apply'; + static const String getApplications = '$baseUrl/leave/my-applications'; +} + class NotificationEndpoints { static const String registerToken = '$baseUrl/notification/register-token'; static const String welcome = '$baseUrl/notification/welcome'; @@ -27,11 +32,11 @@ class HostelEndpoint { class AuthEndpoints { // For initial Microsoft login - redirects through backend static String get getAccess => - 'https://login.microsoftonline.com/850aa78d-94e1-4bc6-9cf3-8c11b530701c/oauth2/v2.0/authorize?client_id=2cdac4f3-1fda-4348-a057-9bb2e3d184a1&response_type=code&redirect_uri=$authUrl/auth/login/redirect/mobile&scope=offline_access%20User.Read&state=12345'; + 'https://login.microsoftonline.com/850aa78d-94e1-4bc6-9cf3-8c11b530701c/oauth2/v2.0/authorize?client_id=2cdac4f3-1fda-4348-a057-9bb2e3d184a1&response_type=code&redirect_uri=$authUrl/auth/login/redirect/mobile&scope=offline_access%20User.Read&state=12345&prompt=select_account'; // For linking Microsoft account - uses same redirect URI as login but with state=link static String get linkMicrosoft => - 'https://login.microsoftonline.com/850aa78d-94e1-4bc6-9cf3-8c11b530701c/oauth2/v2.0/authorize?client_id=2cdac4f3-1fda-4348-a057-9bb2e3d184a1&response_type=code&redirect_uri=$authUrl/auth/login/redirect/mobile&scope=offline_access%20User.Read&state=link'; + 'https://login.microsoftonline.com/850aa78d-94e1-4bc6-9cf3-8c11b530701c/oauth2/v2.0/authorize?client_id=2cdac4f3-1fda-4348-a057-9bb2e3d184a1&response_type=code&redirect_uri=$authUrl/auth/login/redirect/mobile&scope=offline_access%20User.Read&state=link&prompt=select_account'; } class Userendpoints { diff --git a/frontend2/lib/main.dart b/frontend2/lib/main.dart index 64e27d18..823b43fc 100644 --- a/frontend2/lib/main.dart +++ b/frontend2/lib/main.dart @@ -116,8 +116,8 @@ class _MyAppState extends State { // Use `.map()` to transform the stream into a stream of ConnectivityResult _connectivityStream = _connectivity.onConnectivityChanged.map( - (List results) => - results.isNotEmpty ? results[0] : ConnectivityResult.none); + (List results) => + results.isNotEmpty ? results[0] : ConnectivityResult.none); _connectivityStream.listen((ConnectivityResult result) { _handleConnectivityChange(result); @@ -165,12 +165,13 @@ class _MyAppState extends State { navigatorKey: navigatorKey, home: widget.updateRequired - ? const UpdateRequiredScreen() - : (widget.isLoggedIn - ? const MainNavigationScreen() - : const LoginScreen()), + ? const UpdateRequiredScreen() : + (widget.isLoggedIn + ? + const MainNavigationScreen() + : const LoginScreen()), - //home: ProfileScreen(), + // home: ProfileScreen(), builder: EasyLoading.init(), routes: { '/home': (context) => const MainNavigationScreen(), diff --git a/frontend2/lib/screens/home_screen.dart b/frontend2/lib/screens/home_screen.dart index 6aee7ad1..7657abdf 100644 --- a/frontend2/lib/screens/home_screen.dart +++ b/frontend2/lib/screens/home_screen.dart @@ -15,6 +15,7 @@ import '../utilities/startupitem.dart'; import '../widgets/alerts_card.dart'; import '../widgets/microsoft_required_dialog.dart'; import 'mess_preference.dart'; +import 'leave_application_screen.dart'; class HomeScreen extends StatefulWidget { final void Function(int)? onNavigateToTab; @@ -223,6 +224,78 @@ class _HomeScreenState extends State { ), ), ), + const SizedBox(width: 16), + + //Rebate + Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(18), + onTap: () async { + final prefs = await SharedPreferences.getInstance(); + final hasMicrosoftLinked = + prefs.getBool('hasMicrosoftLinked') ?? false; + + if (!mounted) return; + + if (!hasMicrosoftLinked) { + showDialog( + context: context, + builder: (context) => const MicrosoftRequiredDialog( + featureName: 'Mess Rebate', + ), + ); + return; + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LeaveApplicationScreen(), + ), + ); + }, + child: Container( + height: 90, + decoration: BoxDecoration( + // color: const Color(0xFFF6F6F6), + color: const Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(18), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Color(0xFF3754DB), + shape: BoxShape.circle, + ), + child: Center( + child: SvgPicture.asset( + 'assets/icon/messicon.svg', + colorFilter: const ColorFilter.mode( + Colors.white, BlendMode.srcIn), + width: 22, + height: 22, + ), + ), + ), + const SizedBox(height: 8), + const Text( + "Mess Rebate", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w600, + fontSize: 15, + ), + ), + ], + ), + ), + ), + ), ], ), ); diff --git a/frontend2/lib/screens/leave_application_list_screen.dart b/frontend2/lib/screens/leave_application_list_screen.dart new file mode 100644 index 00000000..58c615d5 --- /dev/null +++ b/frontend2/lib/screens/leave_application_list_screen.dart @@ -0,0 +1,186 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_launcher_icons/constants.dart'; +import 'package:frontend2/apis/dio_client.dart'; +import 'package:frontend2/apis/protected.dart'; +import 'package:frontend2/constants/endpoint.dart'; +import 'package:frontend2/screens/leave_application_screen.dart'; +import 'package:intl/intl.dart'; + +class LeaveApplicationListScreen extends StatefulWidget { + const LeaveApplicationListScreen({super.key}); + + @override + State createState() => _LeaveApplicationListScreenState(); +} + +class _LeaveApplicationListScreenState extends State { + + var myApplications = []; + bool isLoading = true; + + + @override + void initState(){ + super.initState(); + _fetchHistory(); + } + + Future _fetchHistory ()async{ + final accessToken = await getAccessToken(); + if (accessToken == 'error') { + setState(() { + isLoading = false; + }); + return;} + final dio = DioClient().dio; + final response = await dio.get( + MessRebateEndpoints.getApplications, + options: Options(headers: {'Authorization': 'Bearer $accessToken'}), + ); + if (response.statusCode == 200) { + final data = response.data as Map; + setState(() { + myApplications = data['myApplications'] ?? []; + isLoading = false; + }); + } else { + setState(() { + isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFEDEDED), + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.transparent, + iconTheme: const IconThemeData(color: Colors.black), + title: const Text( + "Applications List", + style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LeaveApplicationScreen(), + ), + ); + }, + ), + ), + + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : myApplications.isEmpty + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.assignment_outlined, + size: 80, + color: Colors.grey[400], + ), + const SizedBox(height: 20), + const Text( + "No previous applications or requests", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black54, + ), + ), + const SizedBox(height: 8), + const Text( + "Your leave history will appear here.", + style: TextStyle( + fontSize: 14, + color: Colors.black38, + ), + ), + ], + ), + ) : ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: myApplications.length, + itemBuilder: (context, index) { + final application = myApplications[index]; + + final startDate = DateFormat('dd MMM yyyy') + .format(DateTime.parse(application['startDate'])); + final endDate = DateFormat('dd MMM yyyy') + .format(DateTime.parse(application['endDate'])); + + final status = application['status'] ?? ''; + + final fb = application['feedback']??'N/A'; + + Color statusColor; + if (status.toLowerCase() == 'approved') { + statusColor = Colors.green; + } else if (status.toLowerCase() == 'rejected') { + statusColor = Colors.red; + } else { + statusColor = Colors.grey; + } + + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + + title: Text( + application['leaveType'] ?? 'Unknown', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + + subtitle: Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + "$startDate → $endDate"+((status.toLowerCase() == 'rejected')?"\nFeedback: $fb":""), + style: TextStyle( + color: Colors.grey[700], + ), + ), + ), + + trailing: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + status.toUpperCase(), + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/frontend2/lib/screens/leave_application_screen.dart b/frontend2/lib/screens/leave_application_screen.dart new file mode 100644 index 00000000..440db5a2 --- /dev/null +++ b/frontend2/lib/screens/leave_application_screen.dart @@ -0,0 +1,310 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_launcher_icons/constants.dart'; +import 'package:frontend2/apis/dio_client.dart'; +import 'package:frontend2/apis/protected.dart'; +import 'package:frontend2/constants/endpoint.dart'; +import 'package:frontend2/screens/leave_application_list_screen.dart'; +import 'package:intl/intl.dart'; +import 'package:frontend2/screens/home_screen.dart'; + +class LeaveApplicationScreen extends StatefulWidget { + const LeaveApplicationScreen({super.key}); + @override + State createState() => _LeaveApplicationScreenState(); +} + +class _LeaveApplicationScreenState extends State { + + int? _selectedValue; + DateTimeRange? _selectedDateRange; + PlatformFile? _pickedFile; + final TextEditingController _accountNumberController = TextEditingController(); + final TextEditingController _ifscController = TextEditingController(); + final TextEditingController _bankNameController = TextEditingController(); + final TextEditingController _accountHolderController = TextEditingController(); + + + Future _selectDateRange() async { + final DateTimeRange? picked = await showDateRangePicker( + context: context, + firstDate: DateTime.now(), + lastDate: DateTime(2027), + builder: (context, child) { + return Theme(data: ThemeData.light(), child: child!); + }, + ); + if (picked != null) { + setState(() => _selectedDateRange = picked); + } + } + + Future _pickFile() async { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['pdf', 'jpg', 'png'], + ); + if (result != null) { + setState(() => _pickedFile = result.files.first); + } + } + + Future _sendRequest({required int reason, + required DateTimeRange range, + required PlatformFile file, + }) async { + + if (file.path == null) { return; } + + final accessToken = await getAccessToken(); + + final dio = DioClient().dio; + + try { + FormData formData = FormData.fromMap({ + "leaveType": reason == 1 ? 'Academic' : 'Medical', + "startDate": DateFormat("yyyy-MM-dd").format(range.start), + "endDate": DateFormat("yyyy-MM-dd").format(range.end), + "proofDocument": await MultipartFile.fromFile( + file.path!, + filename: file.name, + ), + "bankAccountNumber": _accountNumberController.text, + "bankIFSCCode": _ifscController.text, + "bankName": _bankNameController.text, + "bankAccountHoldersName": _accountHolderController.text, + }); + + final response = await dio.post( + MessRebateEndpoints.sendApplication, + data: formData, + options: Options( + headers: { + 'Authorization': 'Bearer $accessToken', + "Content-Type": "multipart/form-data" + }, + ), + ); + if(response.statusCode==200) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Application Request Successful!")), + ); + } + + } catch (e) { + print(e); // IMPORTANT for debugging + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Error")), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFEDEDED), + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.transparent, + iconTheme: const IconThemeData(color: Colors.black), + title: const Text( + "Mess Rebate", + style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const HomeScreen(), + ), + ); + }, + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildSection( + title: "Type of Leave", + child: Column( + children: [ + RadioListTile( + title: const Text("Academic Leave"), + value: 1, + groupValue: _selectedValue, + onChanged: (val) => setState(() => _selectedValue = val), + ), + RadioListTile( + title: const Text("Medical Leave"), + value: 2, + groupValue: _selectedValue, + onChanged: (val) => setState(() => _selectedValue = val), + ), + ], + ), + ), + const SizedBox(height: 16), + + + _buildSection( + title: "Leave Duration", + child: ListTile( + leading: const Icon(Icons.calendar_month), + title: Text(_selectedDateRange == null + ? "Select Date Range" + : "${DateFormat('dd MMM').format(_selectedDateRange!.start)} - ${DateFormat('dd MMM').format(_selectedDateRange!.end)}"), + trailing: const Icon(Icons.edit), + onTap: _selectDateRange, + ), + ), + const SizedBox(height: 16), + + + _buildSection( + title: "Supporting Documents", + child: ListTile( + leading: const Icon(Icons.upload_file), + title: Text(_pickedFile == null ? "Upload File (PDF/IMG) (Max. Size - 5MB)" : _pickedFile!.name), + subtitle: _pickedFile != null ? Text("${(_pickedFile!.size / 1024).toStringAsFixed(2)} KB") : null, + trailing: const Icon(Icons.attach_file), + onTap: _pickFile, + ), + ), + const SizedBox(height: 16), + + _buildSection( + title: "Bank Details", + child: Column( + children: [ + + TextField( + controller: _accountNumberController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: "Bank Account Number", + border: OutlineInputBorder(), + ), + ), + + const SizedBox(height: 12), + + TextField( + controller: _ifscController, + decoration: const InputDecoration( + labelText: "IFSC Code", + border: OutlineInputBorder(), + ), + ), + + const SizedBox(height: 12), + + TextField( + controller: _bankNameController, + decoration: const InputDecoration( + labelText: "Bank Name", + border: OutlineInputBorder(), + ), + ), + + const SizedBox(height: 12), + + TextField( + controller: _accountHolderController, + decoration: const InputDecoration( + labelText: "Account Holder Name", + border: OutlineInputBorder(), + ), + ), + + ], + ), + ), + + const SizedBox(height: 32), + + + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + onPressed: () { + + if (_selectedValue != null && _selectedDateRange != null && _pickedFile!=null&&_accountNumberController.text!=""&&_ifscController.text!=""&&_bankNameController.text!=""&&_accountHolderController.text!="") { + if ((_pickedFile!.size)/(1024*1024)>=5) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("File size limit - 5MB")), + ); + }else if ((_selectedDateRange!.end.difference(_selectedDateRange!.start).inDays + 1)<5){ + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Number of leave days must be >=5")), + ); + }else { + _sendRequest(reason: _selectedValue!, + range: _selectedDateRange!, + file: _pickedFile!); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Success!")), + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Please fill all fields")), + ); + } + }, + child: const Text("Submit Application", style: TextStyle(color: Colors.white, fontSize: 16)), + ), + + const Divider(), + + ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: const Color(0xFFE3F2FD), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const LeaveApplicationListScreen(), + ), + ); + }, + child: const Text("View history", style: TextStyle(color: Colors.blueAccent, fontSize: 14)), + ), + + const SizedBox(height: 32), + ], + ), + ), + ); + } + + + Widget _buildSection({required String title, required Widget child}) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 5)], + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + const Divider(), + child, + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend2/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend2/macos/Flutter/GeneratedPluginRegistrant.swift index 71cc0fe6..197588be 100644 --- a/frontend2/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend2/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import connectivity_plus import desktop_webview_window import device_info_plus +import file_picker import file_selector_macos import firebase_analytics import firebase_auth @@ -29,6 +30,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) diff --git a/frontend2/pubspec.lock b/frontend2/pubspec.lock index 5f3ed86c..522d07dd 100644 --- a/frontend2/pubspec.lock +++ b/frontend2/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -225,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 + url: "https://pub.dev" + source: hosted + version: "8.3.7" file_selector_linux: dependency: transitive description: @@ -724,18 +732,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -1089,10 +1097,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: diff --git a/frontend2/pubspec.yaml b/frontend2/pubspec.yaml index 590afacc..86aac5c3 100644 --- a/frontend2/pubspec.yaml +++ b/frontend2/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: flutter_easyloading: ^3.0.5 shared_preferences: ^2.3.2 http: ^1.2.2 + file_picker: ^8.0.0 google_sign_in: ^6.2.2 firebase_auth: ^5.4.0 firebase_core: ^3.10.1 diff --git a/server/v1/.gitignore b/server/v1/.gitignore index eaeffc46..d8eab98a 100644 --- a/server/v1/.gitignore +++ b/server/v1/.gitignore @@ -1,3 +1,4 @@ node_modules/ .env -.secrets/ \ No newline at end of file +.secrets/ +modules/leave/uploads \ No newline at end of file diff --git a/server/v1/index.js b/server/v1/index.js index 850c07d9..e8134175 100644 --- a/server/v1/index.js +++ b/server/v1/index.js @@ -12,6 +12,7 @@ const feedbackRoute = require("./modules/feedback/feedbackRoute.js"); const hostelRoute = require("./modules/hostel/hostelRoute.js"); const notificationRoute = require("./modules/notification/notificationRoute.js"); const messRoute = require("./modules/mess/messRoute.js"); +const leaveRoute = require("./modules/leave/leaveRoute.js"); const logsRoute = require("./modules/mess/ScanLogsRoute.js"); const bugReportRoute = require("./modules/bug_report/bugReportRoute.js"); const cors = require("cors"); @@ -209,6 +210,9 @@ app.use("/api/notification", notificationRoute); // Mess route app.use("/api/mess", messRoute); +//Mess rebate route +app.use("/api/leave", leaveRoute); + //mess change route app.use("/api/mess-change", messChangeRouter); diff --git a/server/v1/modules/leave/leaveController.js b/server/v1/modules/leave/leaveController.js new file mode 100644 index 00000000..3c6d08ce --- /dev/null +++ b/server/v1/modules/leave/leaveController.js @@ -0,0 +1,519 @@ +const Leave = require("./leaveModel.js"); +const multer = require("multer"); +const path = require("path"); +const fs = require("fs"); +const mongoose = require("mongoose"); + +const uploadDir = path.join(__dirname, ".", "uploads"); + +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); +} + +// const storage = multer.diskStorage({ +// destination: (req, file, cb) => { +// cb(null, uploadDir); +// }, +// filename: (req, file, cb) => { +// const timeStamp = Date.now(); +// cb(null, `leave-${req.user?req.user._id:req.hostel._id}-${timeStamp}-${file.originalname}`); +// }, +// }); + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|pdf/; + const extname = allowedTypes.test( + path.extname(file.originalname).toLowerCase(), + ); + const mimetype = allowedTypes.test(file.mimetype); + + if (mimetype && extname) { + return cb(null, true); + } else { + cb(new Error("UNSUPPORTED_FILE_TYPE")); + } + }, +}); + +const uploadMiddleware = async (req, res, next) => { + //console.log("Started uploading document to server"); + upload.single("proofDocument")(req, res, (err) => { + if (err) { + if (err.message == "UNSUPPORTED_FILE_TYPE") { + res.status(400).json({ + message: "Invalid file Type. Only jpg, pdf, png are allowed!", + error: err.message, + }); + return; + } + } + //console.log("Passing file to Onedrive Uploader System"); + next(); + }); +}; + +// Get all approved leaves for a specific month/year +async function getRebateDaysForMonth(messHostelId, month, year) { + const startOfMonth = new Date(year, month - 1, 1); + const endOfMonth = new Date(year, month, 0, 23, 59, 59); + let query = {}; + + + //Find approved leaves in leaves database + const leaves = await Leave.find({ + messHostel: messHostelId, + status: "approved", + isEligibleForRebate: true, + $or: [{ startDate: { $lte: endOfMonth }, endDate: { $gte: startOfMonth } }], + }).populate("user", "name rollNumber -_id"); + + query.totalRebateDays = leaves.reduce( + (sum, leave) => sum + leave.eligibleDays, + 0, + ); + query.eligibleApplications = leaves; + + return query; +} + + +//Validation of presence of all fields before uploading file to onedrive +const validateApply = async (req, res, next) => { + const fields = [ + "leaveType", + "startDate", + "endDate", + "bankAccountNumber", + "bankIFSCCode", + "bankName", + "bankAccountHoldersName", + ] + + const missingFields = fields.filter(field => !req.body[field]); + + if(missingFields.length>0) { + return res.status(400).json({ + message: "Fields cannot be empty", + emptyFields: missingFields, + }) + } + + //console.log(req.body) + + next(); +}; + + +//Apply for leave(Student endpoint) +const applyForLeave = async (req, res) => { + try { + const { + leaveType, + startDate, + endDate, + bankAccountNumber, + bankIFSCCode, + bankName, + bankAccountHoldersName, + } = req.body; + //No error handling required due to presence of validateApply() + + //File handling + if (!req.file) { + res.status(400).json({ + message: "Please upload proof document", + }); + return; + } + + //Obtain url and fileName generated by uploadToOnedrive.js + const proofDocumentUrl = req.file.leaveUrl; + const proofDocumentFilename = req.file.originalname; + + //Genration of Date object + const [startYear, startMonth, startDay] = startDate.split("-").map(Number); + const [endYear, endMonth, endDay] = endDate.split("-").map(Number); + const start = new Date(startYear, startMonth - 1, startDay); + const end = new Date(endYear, endMonth - 1, endDay); + + //Get difference between two dates + const diffBtwDates = Math.abs(end - start); + const numberOfDays = Math.floor(diffBtwDates / (1000 * 60 * 60 * 24)); + + //numberofdays business logic + + if (numberOfDays < 5) { + res.status(400).json({ + message: "Number of days must be greater than 5", + }); + return; + } + + let eligibleDays = 0; + //Calculation of eligible days + if (numberOfDays >= 5) { + eligibleDays = numberOfDays; + } + + //Validation Rules / Business logic + if (!(leaveType == "Academic" || leaveType == "Medical")) { + res.status(400).json({ + message: "Leave type is invalid", + }); + return; + } + + if (end - start < 0) { + res.status(400).json({ + message: "Start, end date combination is invalid", + }); + return; + } + + //Autogenerated variables + const appliedAt = new Date(Date.now()); + + //Only if user exists + if (req.user && req.user.curr_subscribed_mess) { + const leaveApplication = new Leave({ + user: req.user, + leaveType, + startDate: start, + endDate: end, + numberOfDays, + eligibleDays, + status: "pending", + proofDocumentUrl, + proofDocumentFilename, + appliedAt, + messHostel: req.user.curr_subscribed_mess, + bankAccountNumber, + bankIFSCCode, + bankName, + bankAccountHoldersName, + }); + + await leaveApplication.save(); + + res.status(201).json({ + message: "Leave Application submited successfully", + leaveApplication: { + id: leaveApplication._id, + user: req.user, + eligibleDays, + status: leaveApplication.status, + appliedAt: leaveApplication.appliedAt, + }, + }); + } else { + if (req.user && !req.user.curr_subscribed_mess) { + res.status(400).json({ + message: "Hostel not provided", + }); + } + res.status(400).json({ + message: "Please login first", + }); + } + } catch (err) { + console.error("Error submitting leave application"); + res.status(500).json({ + message: "Error submitting leave application", + error: err.message, + }); + } +}; + +const getApplications = async (req, res) => { + //Search by User ObjectID + const myApplications = await Leave.find({ + user: req.user, + }).sort({ + appliedAt: -1, + }); + + //For empty applications array + if (myApplications.length === 0) { + res.status(200).json({ + message: "No past applications available", + }); + return; + } + + res.status(200).json({ + message: "Retrieved applications successfully", + myApplications, + }); + return; +}; + +const getApplicationByID = async (req, res) => { + const { id } = req.params; + //Search by Application ObjectID + if (!mongoose.Types.ObjectId.isValid(id)) { + res.status(400).json({ + message: "Incorrect Object ID format", + }); + return; + } else { + try { + let application = await Leave.findById(id); + + if (!application.user.equals(req.user._id)) { + application = null; + } + + //For empty appplication variable + if (application == null) { + res.status(404).json({ + message: "There are no such leave applications", + }); + } else { + res.status(200).json({ + message: "Application retrieved successfully", + application, + }); + } + } catch (err) { + res.status(500).json({ + message: "Invalid request", + error: err.message, + }); + } + } +}; + +const getApplicationProof = async (req, res) => { + const { id } = req.params; + //Retrieval of proofDocumentUrl from application ObjectID + if (!mongoose.Types.ObjectId.isValid(id)) { + res.status(400).json({ + message: "Incorrect Object ID format", + }); + return; + } else { + try { + const application = await Leave.findOne({ + _id: id, + user: req.user, + }); + if (application == null) { + res.status(404).json({ + message: "There are no such leave applications", + }); + } else { + res.status(200).json({ + message: "Information retrieved successfully", + proofDocumentUrl: application.proofDocumentUrl, + }); + } + } catch (err) { + res.status(500).json({ + message: "Invalid request", + error: err.message, + }); + } + } +}; + +const getAllPendingApplications = async (req, res) => { + const pendingApplications = await Leave.find({ + messHostel: req.hostel, + status: "pending", + }) + .sort({ + appliedAt: -1, + }) + .populate("user", "name rollNumber email -_id"); + + //For empty applications array + if (pendingApplications.length === 0) { + res.status(404).json({ + message: "No applications available", + }); + return; + } + + res.status(200).json({ + message: "Retrieved applications successfully", + pendingApplications, + }); + return; +}; + +const filterApplications = async (req, res) => { + const query = { + messHostel: req.hostel, + }; + + if (req.query.status) { + query.status = req.query.status; + } + + + if (req.query.month && req.query.year) { + let month = parseInt(req.query.month); + let year = parseInt(req.query.year); + if(!(month>0 && month <=12)) { + return res.status(400).json({ + message: "Wrong month format", + }) + } + query.startDate = { + $gte: new Date(year, month - 1, 1), + $lt: new Date(year, month, 1), + }; + } else if (req.query.year && !req.query.month) { + query.startDate = { + $gte: new Date(req.query.year, 0, 1), + $lt: new Date(req.query.year + 1, 0, 1), + }; + } + else if (req.query.month && !req.query.year) { + res.status(404).json({ + message: "Please input both year and month", + }); + return; + } + + try { + const filteredApplications = await Leave.find(query).sort({ + appliedAt: -1, + }); + + if (filteredApplications.length === 0) { + res.status(404).json({ + message: "No such applications could be found", + }); + return; + } + + res.status(200).json({ + message: "Documents fetched successfully", + filteredApplications, + }); + } catch (err) { + res.status(500).json({ + message: "Error occured while retrieving leave applications", + error: err.message, + }); + } +}; + +const approveApplication = async (req, res) => { + const { id } = req.params; + + try { + let application = await Leave.findById(id); + if (!application.messHostel.equals(req.hostel._id)) { + application = null; + } + + if (application !== null) { + const query = { status: "approved" }; + if (req.body.feedback) { + query.feedback = req.body.feedback; + } + + if (application.eligibleDays >= 5) { + console.log("Marking eligible for rebate = true"); + query.isEligibleForRebate = true; + } + + const updatedDoc = await Leave.findByIdAndUpdate(id, query, {new: true}).populate("user", "name rollNumber email -_id"); + + res.status(201).json({ + message: `Approved application with ID ${id}`, + updatedApplication: updatedDoc, + }); + } else { + res.status(401).json({ + message: "You are not authorised to do that", + }); + } + } catch (err) { + res.status(500).json({ + message: "Error in approving the leave application", + error: err.message, + }); + } +}; + +const rejectApplication = async (req, res) => { + const { id } = req.params; + + try { + let application = await Leave.findById(id); + if (!application.messHostel.equals(req.hostel._id)) { + application = null; + } + + if (application !== null) { + const query = { status: "rejected" }; + if (req.body.feedback) { + query.feedback = req.body.feedback; + } + + const updatedDoc = await Leave.findByIdAndUpdate(id, query, {new: true}).populate("user", "name rollNumber email -_id"); + + res.status(201).json({ + message: `Rejected application with ID ${id}`, + updatedApplication: updatedDoc, + }); + } else { + res.status(401).json({ + message: "You are not authorised to do that", + }); + } + } catch (err) { + res.status(500).json({ + message: "Error in approving the leave application", + error: err.message, + }); + } +}; + +const getRebateSummary = async (req, res) => { + const hostel = req.hostel._id; + + if (!(req.query.month && req.query.year)) { + return res.status(400).json({ + message: "Year or month not specified", + }); + } + + let year = parseInt(req.query.year); + let month = parseInt(req.query.month); + + if(!(month>0 && month<=12)) { + return res.status(400).json({ + message: "Invalid month format" + }) + } + + console.log(`month = ${month} and year = ${year}`); + + const result = await getRebateDaysForMonth(hostel, month, year); + + res.status(200).json({ + message: "Rebate summary retrieved successfully", + rebateSummary: result, + }); +}; + +module.exports = { + uploadMiddleware, + applyForLeave, + getApplications, + getApplicationByID, + getApplicationProof, + getAllPendingApplications, + filterApplications, + approveApplication, + rejectApplication, + getRebateSummary, + validateApply +}; diff --git a/server/v1/modules/leave/leaveModel.js b/server/v1/modules/leave/leaveModel.js new file mode 100644 index 00000000..7f0ad78c --- /dev/null +++ b/server/v1/modules/leave/leaveModel.js @@ -0,0 +1,85 @@ +const mongoose = require("mongoose"); + +//Define Leave Application Model +const leaveSchema = new mongoose.Schema({ + user: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + leaveType: { + type: String, + enum: ["Academic", "Medical"], + required: true, + }, + startDate: { + type: Date, + required: true, + }, + endDate: { + type: Date, + required: true, + }, + numberOfDays: { + type: Number, + required: true, + }, + eligibleDays: { + type: Number, + required: true, + }, + status: { + type: String, + enum: ["pending", "approved", "rejected"], + default: "pending", + required: true, + }, + proofDocumentUrl: { + type: String, + required: true, + }, + proofDocumentFilename: { + type: String, + required: true, + }, + appliedAt: { + type: Date, + required: true, + }, + feedback: { + type: String, + required: false, + }, + messHostel: { + type: mongoose.Schema.Types.ObjectId, + ref: "Hostel", + required: true, + }, + isEligibleForRebate: { + type: Boolean, + default: false, + required: true + }, + bankAccountNumber: { + type: Number, + required: true, + }, + bankIFSCCode: { + type: String, + required: true, + }, + bankName: { + type: String, + required: true, + }, + bankAccountHoldersName: { + type: String, + required: true, + }, +}); + + +const Leave = mongoose.model("Leave", leaveSchema); + +//Export leave application model +module.exports = Leave; \ No newline at end of file diff --git a/server/v1/modules/leave/leaveRoute.js b/server/v1/modules/leave/leaveRoute.js new file mode 100644 index 00000000..a0e488b9 --- /dev/null +++ b/server/v1/modules/leave/leaveRoute.js @@ -0,0 +1,47 @@ +const express = require("express"); + +const { + authenticateJWT, + authenticateAdminJWT, +} = require("../../middleware/authenticateJWT.js"); + +const { uploadToOnedrive } = require('./uploadToOnedrive.js') + +const { + uploadMiddleware, + applyForLeave, + getApplications, + getApplicationByID, + getApplicationProof, + getAllPendingApplications, + filterApplications, + approveApplication, + rejectApplication, + getRebateSummary, + validateApply +} = require("./leaveController.js"); + +const leaveRouter = express.Router(); + +//User/Student Endpoint + +leaveRouter.post('/apply', authenticateJWT, uploadMiddleware, validateApply, uploadToOnedrive, applyForLeave); + +leaveRouter.get('/my-applications', authenticateJWT, getApplications); + +leaveRouter.get('/:id', authenticateJWT, getApplicationByID); + +leaveRouter.get('/:id/proof', authenticateJWT, getApplicationProof); + +//Hostel Office Endpoints +leaveRouter.get('/hostel/pending', authenticateAdminJWT, getAllPendingApplications); + +leaveRouter.get('/hostel/all', authenticateAdminJWT, filterApplications); + +leaveRouter.post('/:id/approve', authenticateAdminJWT, approveApplication); + +leaveRouter.post('/:id/reject', authenticateAdminJWT, rejectApplication); + +leaveRouter.get('/hostel/rebate-summary', authenticateAdminJWT, getRebateSummary); + +module.exports = leaveRouter; diff --git a/server/v1/modules/leave/uploadToOnedrive.js b/server/v1/modules/leave/uploadToOnedrive.js new file mode 100644 index 00000000..67ace0d9 --- /dev/null +++ b/server/v1/modules/leave/uploadToOnedrive.js @@ -0,0 +1,181 @@ +const axios = require('axios'); +const { getDelegatedAccessToken } = require('../../utils/delegatedGraphAuth.js'); +require('dotenv').config(); + +const LEAVE_FOLDER_ID = process.env.ONEDRIVE_LEAVE_FOLDER_ID; + +async function requireDelegatedToken() { + const tok = await getDelegatedAccessToken(); + if (!tok) { + throw new Error( + "Delegated token not available. Login as storage user and seed access+refresh tokens via /api/_debug/graph/delegated-token." + ); + } + return tok; +} + +async function graphGET(url, token, config = {}) { + const { data } = await axios.get(url, { + ...config, + headers: { ...(config.headers || {}), Authorization: `Bearer ${token}` }, + }); + return data; +} + +async function graphPUT(url, token, body, headers = {}) { + const { data } = await axios.put(url, body, { + headers: { Authorization: `Bearer ${token}`, ...headers }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + }); + return data; +} + +async function graphPOST(url, token, body, headers = {}) { + const { data } = await axios.post(url, body, { + headers: { Authorization: `Bearer ${token}`, ...headers }, + }); + return data; +} + +function extFromMime(mime) { + if (!mime) return ".jpg"; + if (mime.includes("png")) return ".png"; + if (mime.includes("jpeg")) return ".jpg"; + if (mime.includes("jpg")) return ".jpg"; + if (mime.includes("webp")) return ".webp"; + return ".jpg"; +} + +async function getMe(token) { + return graphGET("https://graph.microsoft.com/v1.0/me", token); +} + +async function getMyDrive(token) { + return graphGET("https://graph.microsoft.com/v1.0/me/drive", token); +} + +async function getItemById(token, itemId) { + const url = `https://graph.microsoft.com/v1.0/me/drive/items/${itemId}?$select=id,name,parentReference,webUrl`; + return graphGET(url, token); +} + +async function uploadToParentByName( + token, + parentId, + filename, + buffer, + mimeType +) { + // console.log(filename, buffer, mimeType); + const url = `https://graph.microsoft.com/v1.0/me/drive/items/${parentId}:/${encodeURIComponent( + filename + )}:/content`; + + const data = await graphPUT(url, token, buffer, { + "Content-Type": mimeType || "application/octet-stream", + }); + + return data; // driveItem +} + +async function createOrganizationViewLink(token, itemId) { + const url = `https://graph.microsoft.com/v1.0/me/drive/items/${itemId}/createLink`; + const data = await graphPOST( + url, + token, + { type: "view", scope: "organization" }, + { "Content-Type": "application/json" } + ); + + return data?.link?.webUrl; +} + + + +async function uploadToOnedrive(req, res, next) { + try { + const file = req.file; + //console.log("File received by Onedrive Uploader"); + if (!file) return res.status(400).json({ message: "No file uploaded" }); + if (!LEAVE_FOLDER_ID) { + return res + .status(400) + .json({ message: "ONEDRIVE_LEAVE_FOLDER_ID is not configured" }); + } + + const ext = extFromMime(file.mimetype); + const timeStamp = Date.now() + const targetName = `leave-${req.user._id}-${timeStamp}-${file.originalname}`; + + // Delegated token required to use /me/drive + const token = await requireDelegatedToken(); + + // Sanity checks: who am I? which drive? does folder exist? + let me, drive, parentItem; + try { + me = await getMe(token); + } catch (e) {} + + try { + drive = await getMyDrive(token); + } catch (e) {} + + try { + parentItem = await getItemById(token, LEAVE_FOLDER_ID); + if ( + drive?.id && + parentItem?.parentReference?.driveId && + drive.id !== parentItem.parentReference.driveId + ) { + return res.status(400).json({ + message: + "Configured folder belongs to a different drive than the token user's drive.", + }); + } + } catch (e) { + // Parent folder lookup failed + return res.status(400).json({ + message: + "Configured ONEDRIVE_LEAVE_FOLDER_ID not found or not accessible for this account.", + hint: "Fetch it with GET /v1.0/me/drive/root:/HAB%20App/rebate-requests and use the returned id.", + }); + } + console.log(`Starting upload to onedrive to ${targetName} for user: ${req.user?.name}`) + // Upload new content to the parent folder with file name = roll.ext + const uploaded = await uploadToParentByName( + token, + LEAVE_FOLDER_ID, + targetName, + file.buffer, + file.mimetype + ); + + //console.log("Uplaoding to onedrive successful."); + //console.log("Creating organization view link"); + + // Create org-scoped view link (tenant must allow it) + let publicUrl = null; + try { + publicUrl = await createOrganizationViewLink(token, uploaded.id); + } catch (e) { + return res.status(401).json({ + message: "Error in creating public link. Please try again" + }) + } + + req.file.leaveId = uploaded.id; + if (publicUrl) req.file.leaveUrl = publicUrl; + + console.log("Uploading to onedrive successful"); + next(); + } catch (err) { + const status = err.response?.status; + const msg = err.response?.data?.error?.message || err.message; + return res + .status(status === 403 ? 403 : 500) + .json({ message: "Failed to upload leave application", error: msg, status }); + } +} + +module.exports = {uploadToOnedrive}; \ No newline at end of file diff --git a/server/v1/package-lock.json b/server/v1/package-lock.json index 6af07f8b..efdcfe92 100644 --- a/server/v1/package-lock.json +++ b/server/v1/package-lock.json @@ -3157,86 +3157,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/mongoose/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mongoose/node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongoose/node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongoose/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/mongoose/node_modules/mongodb": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", diff --git a/server/v2/package-lock.json b/server/v2/package-lock.json index 7ff856a4..7a21a1ae 100644 --- a/server/v2/package-lock.json +++ b/server/v2/package-lock.json @@ -3157,86 +3157,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/mongoose/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mongoose/node_modules/gaxios": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", - "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongoose/node_modules/gcp-metadata": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", - "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "gaxios": "^5.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mongoose/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/mongoose/node_modules/mongodb": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz",