feat: Add document verification and rejection functionality with remote logging support
This commit is contained in:
parent
a02887845b
commit
6d70afc779
@ -75,6 +75,9 @@ class LoginController extends MyController {
|
||||
basicValidator.clearErrors();
|
||||
} else {
|
||||
await _handleRememberMe();
|
||||
// ✅ Enable remote logging after successful login
|
||||
enableRemoteLogging();
|
||||
logSafe("✅ Remote logging enabled after login.");
|
||||
|
||||
// ✅ Commented out FCM token registration after login
|
||||
/*
|
||||
|
@ -45,6 +45,26 @@ class DocumentDetailsController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify document
|
||||
Future<bool> verifyDocument(String documentId) async {
|
||||
final result =
|
||||
await ApiService.verifyDocumentApi(id: documentId, isVerify: true);
|
||||
if (result) {
|
||||
await fetchDocumentDetails(documentId); // refresh details
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Reject document
|
||||
Future<bool> rejectDocument(String documentId) async {
|
||||
final result =
|
||||
await ApiService.verifyDocumentApi(id: documentId, isVerify: false);
|
||||
if (result) {
|
||||
await fetchDocumentDetails(documentId); // refresh details
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Fetch Pre-Signed URL for a given version
|
||||
Future<String?> fetchPresignedUrl(String versionId) async {
|
||||
return await ApiService.getPresignedUrlApi(versionId);
|
||||
|
@ -1,7 +1,8 @@
|
||||
class ApiEndpoints {
|
||||
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||
//
|
||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
||||
|
||||
// Dashboard Module API Endpoints
|
||||
static const String getDashboardAttendanceOverview =
|
||||
"/dashboard/attendance-overview";
|
||||
@ -85,4 +86,8 @@ class ApiEndpoints {
|
||||
static const String getDocumentVersion = "/document/get/version";
|
||||
static const String getDocumentVersions = "/document/list/versions";
|
||||
static const String editDocument = "/document/edit";
|
||||
static const String verifyDocument = "/document/verify";
|
||||
|
||||
/// Logs Module API Endpoints
|
||||
static const String uploadLogs = "/log";
|
||||
}
|
||||
|
@ -247,6 +247,89 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> postLogsApi(List<Map<String, dynamic>> logs) async {
|
||||
const endpoint = "${ApiEndpoints.uploadLogs}";
|
||||
logSafe("Posting logs... count=${logs.length}");
|
||||
|
||||
try {
|
||||
final response =
|
||||
await _postRequest(endpoint, logs, customTimeout: extendedTimeout);
|
||||
|
||||
if (response == null) {
|
||||
logSafe("Post logs failed: null response", level: LogLevel.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
logSafe("Post logs response status: ${response.statusCode}");
|
||||
logSafe("Post logs response body: ${response.body}");
|
||||
|
||||
if (response.statusCode == 200 && response.body.isNotEmpty) {
|
||||
final json = jsonDecode(response.body);
|
||||
if (json['success'] == true) {
|
||||
logSafe("Logs posted successfully.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
logSafe("Failed to post logs: ${response.body}", level: LogLevel.warning);
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception during postLogsApi: $e", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Verify Document API
|
||||
static Future<bool> verifyDocumentApi({
|
||||
required String id,
|
||||
bool isVerify = true,
|
||||
}) async {
|
||||
final endpoint = "${ApiEndpoints.verifyDocument}/$id";
|
||||
final queryParams = {"isVerify": isVerify.toString()};
|
||||
logSafe("Verifying document with id: $id | isVerify: $isVerify");
|
||||
|
||||
try {
|
||||
final uri = Uri.parse("${ApiEndpoints.baseUrl}$endpoint")
|
||||
.replace(queryParameters: queryParams);
|
||||
|
||||
String? token = await _getToken();
|
||||
if (token == null) return false;
|
||||
|
||||
final headers = _headers(token);
|
||||
logSafe("POST (verify) $uri\nHeaders: $headers");
|
||||
|
||||
final response =
|
||||
await http.post(uri, headers: headers).timeout(extendedTimeout);
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
logSafe("Unauthorized VERIFY. Attempting token refresh...");
|
||||
if (await AuthService.refreshToken()) {
|
||||
return await verifyDocumentApi(id: id, isVerify: isVerify);
|
||||
}
|
||||
}
|
||||
|
||||
logSafe("Verify document response status: ${response.statusCode}");
|
||||
logSafe("Verify document response body: ${response.body}");
|
||||
|
||||
final json = jsonDecode(response.body);
|
||||
if (json['success'] == true) {
|
||||
logSafe("Document verify success: ${json['data']}");
|
||||
return true;
|
||||
} else {
|
||||
logSafe(
|
||||
"Failed to verify document: ${json['message'] ?? 'Unknown error'}",
|
||||
level: LogLevel.warning,
|
||||
);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logSafe("Exception during verifyDocumentApi: $e", level: LogLevel.error);
|
||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get Pre-Signed URL for Old Version
|
||||
static Future<String?> getPresignedUrlApi(String versionId) async {
|
||||
final endpoint = "${ApiEndpoints.getDocumentVersion}/$versionId";
|
||||
|
@ -2,16 +2,41 @@ import 'dart:io';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:marco/helpers/services/api_service.dart';
|
||||
|
||||
/// Global logger instance
|
||||
late final Logger appLogger;
|
||||
late final FileLogOutput fileLogOutput;
|
||||
Logger? _appLogger;
|
||||
late final FileLogOutput _fileLogOutput;
|
||||
|
||||
/// Store logs temporarily for API posting
|
||||
final List<Map<String, dynamic>> _logBuffer = [];
|
||||
|
||||
/// Lock flag to prevent concurrent posting
|
||||
bool _isPosting = false;
|
||||
|
||||
/// Flag to allow API posting only after login
|
||||
bool _canPostLogs = false;
|
||||
|
||||
/// Maximum number of logs before triggering API post
|
||||
const int _maxLogsBeforePost = 50;
|
||||
|
||||
/// Maximum logs in memory buffer
|
||||
const int _maxBufferSize = 50;
|
||||
|
||||
/// Enum → logger level mapping
|
||||
const _levelMap = {
|
||||
LogLevel.debug: Level.debug,
|
||||
LogLevel.info: Level.info,
|
||||
LogLevel.warning: Level.warning,
|
||||
LogLevel.error: Level.error,
|
||||
LogLevel.verbose: Level.verbose,
|
||||
};
|
||||
|
||||
/// Initialize logging
|
||||
Future<void> initLogging() async {
|
||||
fileLogOutput = FileLogOutput();
|
||||
_fileLogOutput = FileLogOutput();
|
||||
|
||||
appLogger = Logger(
|
||||
_appLogger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 0,
|
||||
printTime: true,
|
||||
@ -20,12 +45,18 @@ Future<void> initLogging() async {
|
||||
),
|
||||
output: MultiOutput([
|
||||
ConsoleOutput(),
|
||||
fileLogOutput,
|
||||
_fileLogOutput,
|
||||
]),
|
||||
level: Level.debug,
|
||||
);
|
||||
}
|
||||
|
||||
/// Enable API posting after login
|
||||
void enableRemoteLogging() {
|
||||
_canPostLogs = true;
|
||||
_postBufferedLogs(); // flush logs if any
|
||||
}
|
||||
|
||||
/// Safe logger wrapper
|
||||
void logSafe(
|
||||
String message, {
|
||||
@ -34,27 +65,60 @@ void logSafe(
|
||||
StackTrace? stackTrace,
|
||||
bool sensitive = false,
|
||||
}) {
|
||||
if (sensitive) return;
|
||||
if (sensitive || _appLogger == null) return;
|
||||
|
||||
switch (level) {
|
||||
case LogLevel.debug:
|
||||
appLogger.d(message, error: error, stackTrace: stackTrace);
|
||||
break;
|
||||
case LogLevel.warning:
|
||||
appLogger.w(message, error: error, stackTrace: stackTrace);
|
||||
break;
|
||||
case LogLevel.error:
|
||||
appLogger.e(message, error: error, stackTrace: stackTrace);
|
||||
break;
|
||||
case LogLevel.verbose:
|
||||
appLogger.v(message, error: error, stackTrace: stackTrace);
|
||||
break;
|
||||
default:
|
||||
appLogger.i(message, error: error, stackTrace: stackTrace);
|
||||
final loggerLevel = _levelMap[level] ?? Level.info;
|
||||
_appLogger!.log(loggerLevel, message, error: error, stackTrace: stackTrace);
|
||||
|
||||
// Buffer logs for API posting
|
||||
_logBuffer.add({
|
||||
"logLevel": level.name,
|
||||
"message": message,
|
||||
"timeStamp": DateTime.now().toUtc().toIso8601String(),
|
||||
"ipAddress": "this is test IP", // TODO: real IP
|
||||
"userAgent": "FlutterApp/1.0", // TODO: device_info_plus
|
||||
"details": error?.toString() ?? stackTrace?.toString(),
|
||||
});
|
||||
|
||||
if (_logBuffer.length >= _maxLogsBeforePost) {
|
||||
_postBufferedLogs();
|
||||
}
|
||||
}
|
||||
|
||||
/// Log output to file (safe path, no permission required)
|
||||
/// Post buffered logs to API
|
||||
Future<void> _postBufferedLogs() async {
|
||||
if (!_canPostLogs) return; // 🚫 skip if not logged in
|
||||
if (_isPosting || _logBuffer.isEmpty) return;
|
||||
|
||||
_isPosting = true;
|
||||
final logsToSend = List<Map<String, dynamic>>.from(_logBuffer);
|
||||
_logBuffer.clear();
|
||||
|
||||
try {
|
||||
final success = await ApiService.postLogsApi(logsToSend);
|
||||
if (!success) {
|
||||
_reinsertLogs(logsToSend, reason: "API call returned false");
|
||||
}
|
||||
} catch (e) {
|
||||
_reinsertLogs(logsToSend, reason: "API exception: $e");
|
||||
} finally {
|
||||
_isPosting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reinsert logs into buffer if posting fails
|
||||
void _reinsertLogs(List<Map<String, dynamic>> logs, {required String reason}) {
|
||||
_appLogger?.w("Failed to post logs, re-queuing. Reason: $reason");
|
||||
|
||||
if (_logBuffer.length + logs.length > _maxBufferSize) {
|
||||
_appLogger?.e("Buffer full. Dropping ${logs.length} logs to prevent crash.");
|
||||
return;
|
||||
}
|
||||
|
||||
_logBuffer.insertAll(0, logs);
|
||||
}
|
||||
|
||||
/// File-based log output (safe storage)
|
||||
class FileLogOutput extends LogOutput {
|
||||
File? _logFile;
|
||||
|
||||
@ -81,7 +145,6 @@ class FileLogOutput extends LogOutput {
|
||||
@override
|
||||
void output(OutputEvent event) async {
|
||||
await _init();
|
||||
|
||||
if (event.lines.isEmpty) return;
|
||||
|
||||
final logMessage = event.lines.join('\n') + '\n';
|
||||
@ -122,22 +185,5 @@ class FileLogOutput extends LogOutput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple log printer for file output
|
||||
class SimpleFileLogPrinter extends LogPrinter {
|
||||
@override
|
||||
List<String> log(LogEvent event) {
|
||||
final message = event.message.toString();
|
||||
|
||||
if (message.contains('[SENSITIVE]')) return [];
|
||||
|
||||
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
|
||||
final level = event.level.name.toUpperCase();
|
||||
final error = event.error != null ? ' | ERROR: ${event.error}' : '';
|
||||
final stack =
|
||||
event.stackTrace != null ? '\nSTACKTRACE:\n${event.stackTrace}' : '';
|
||||
return ['[$timestamp] [$level] $message$error$stack'];
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional enum for log levels
|
||||
/// Custom log levels
|
||||
enum LogLevel { debug, info, warning, error, verbose }
|
||||
|
@ -13,7 +13,7 @@ Future<void> main() async {
|
||||
|
||||
await initLogging();
|
||||
logSafe("App starting...");
|
||||
|
||||
enableRemoteLogging();
|
||||
try {
|
||||
await initializeApp();
|
||||
logSafe("App initialized successfully.");
|
||||
@ -73,9 +73,11 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isOffline = _connectivityStatus.contains(ConnectivityResult.none);
|
||||
final bool isOffline =
|
||||
_connectivityStatus.contains(ConnectivityResult.none);
|
||||
return isOffline
|
||||
? const MaterialApp(debugShowCheckedModeBanner: false, home: OfflineScreen())
|
||||
? const MaterialApp(
|
||||
debugShowCheckedModeBanner: false, home: OfflineScreen())
|
||||
: const MyApp();
|
||||
}
|
||||
}
|
||||
|
@ -229,6 +229,81 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
|
||||
_buildDetailRow("Uploaded On", uploadDate),
|
||||
if (doc.updatedAt != null)
|
||||
_buildDetailRow("Last Updated On", updateDate),
|
||||
MySpacing.height(12),
|
||||
if (permissionController
|
||||
.hasPermission(Permissions.verifyDocument)) ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.check, color: Colors.white),
|
||||
label: MyText.bodyMedium(
|
||||
"Verify",
|
||||
color: Colors.white,
|
||||
fontWeight: 600,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
onPressed: () async {
|
||||
final success = await controller.verifyDocument(doc.id);
|
||||
if (success) {
|
||||
showAppSnackbar(
|
||||
title: "Success",
|
||||
message: "Document verified successfully",
|
||||
type: SnackbarType.success,
|
||||
);
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to verify document",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
label: MyText.bodyMedium(
|
||||
"Reject",
|
||||
color: Colors.white,
|
||||
fontWeight: 600,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
onPressed: () async {
|
||||
final success = await controller.rejectDocument(doc.id);
|
||||
if (success) {
|
||||
showAppSnackbar(
|
||||
title: "Rejected",
|
||||
message: "Document rejected successfully",
|
||||
type: SnackbarType.warning,
|
||||
);
|
||||
} else {
|
||||
showAppSnackbar(
|
||||
title: "Error",
|
||||
message: "Failed to reject document",
|
||||
type: SnackbarType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user