From 6c32d76f78a5c16c6697ed1025726d3e3956e44f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 13 Oct 2024 22:17:23 +0800 Subject: [PATCH] :sparkles: Audit logs --- assets/locales/en_us.json | 3 +- assets/locales/zh_cn.json | 3 +- ios/Podfile.lock | 10 ++++ lib/models/audit_log.dart | 38 ++++++++++++ lib/models/audit_log.g.dart | 38 ++++++++++++ lib/router.dart | 9 +++ lib/screens/account.dart | 9 +++ lib/screens/account/audit_log.dart | 96 ++++++++++++++++++++++++++++++ pubspec.lock | 8 +++ pubspec.yaml | 1 + 10 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 lib/models/audit_log.dart create mode 100644 lib/models/audit_log.g.dart create mode 100644 lib/screens/account/audit_log.dart diff --git a/assets/locales/en_us.json b/assets/locales/en_us.json index f0849c7..e7eb83f 100644 --- a/assets/locales/en_us.json +++ b/assets/locales/en_us.json @@ -481,5 +481,6 @@ "authPreferences": "Auth preferences", "authPreferencesDesc": "Set the security behavior of your account", "authMaximumAuthSteps": "Maximum authentication steps", - "authMaximumAuthStepsDesc": "The maximum number of authentication steps when logging in, higher value is more secure, lower value is more convenient; default is 2" + "authMaximumAuthStepsDesc": "The maximum number of authentication steps when logging in, higher value is more secure, lower value is more convenient; default is 2", + "auditLog": "Audit log" } diff --git a/assets/locales/zh_cn.json b/assets/locales/zh_cn.json index 71ad5de..ea0ef37 100644 --- a/assets/locales/zh_cn.json +++ b/assets/locales/zh_cn.json @@ -477,5 +477,6 @@ "authPreferences": "安全偏好设置", "authPreferencesDesc": "调整账号的安全行为模式", "authMaximumAuthSteps": "最大认证步数", - "authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2" + "authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2", + "auditLog": "活动日志" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7307948..5061490 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -166,6 +166,9 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter + - flutter_udid (0.0.1): + - Flutter + - SAMKeychain - flutter_webrtc (0.11.3): - Flutter - WebRTC-SDK (= 125.6422.04) @@ -259,6 +262,7 @@ PODS: - PromisesObjC (= 2.4.0) - protocol_handler_ios (0.0.1): - Flutter + - SAMKeychain (1.5.3) - screen_brightness_ios (0.1.0): - Flutter - SDWebImage (5.19.7): @@ -316,6 +320,7 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`) @@ -364,6 +369,7 @@ SPEC REPOS: - nanopb - PromisesObjC - PromisesSwift + - SAMKeychain - SDWebImage - sqlite3 - SwiftyGif @@ -401,6 +407,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + flutter_udid: + :path: ".symlinks/plugins/flutter_udid/ios" flutter_webrtc: :path: ".symlinks/plugins/flutter_webrtc/ios" gal: @@ -480,6 +488,7 @@ SPEC CHECKSUMS: flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a @@ -501,6 +510,7 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990 + SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad diff --git a/lib/models/audit_log.dart b/lib/models/audit_log.dart new file mode 100644 index 0000000..3d34fbf --- /dev/null +++ b/lib/models/audit_log.dart @@ -0,0 +1,38 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:solian/models/account.dart'; + +part 'audit_log.g.dart'; + +@JsonSerializable() +class AuditEvent { + int id; + DateTime createdAt; + DateTime updatedAt; + DateTime? deletedAt; + String type; + String target; + String location; + String ipAddress; + String userAgent; + Account account; + int accountId; + + AuditEvent({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.type, + required this.target, + required this.location, + required this.ipAddress, + required this.userAgent, + required this.account, + required this.accountId, + }); + + static AuditEvent fromJson(Map json) => + _$AuditEventFromJson(json); + + Map toJson() => _$AuditEventToJson(this); +} diff --git a/lib/models/audit_log.g.dart b/lib/models/audit_log.g.dart new file mode 100644 index 0000000..669ffb2 --- /dev/null +++ b/lib/models/audit_log.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'audit_log.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AuditEvent _$AuditEventFromJson(Map json) => AuditEvent( + id: (json['id'] as num).toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + type: json['type'] as String, + target: json['target'] as String, + location: json['location'] as String, + ipAddress: json['ip_address'] as String, + userAgent: json['user_agent'] as String, + account: Account.fromJson(json['account'] as Map), + accountId: (json['account_id'] as num).toInt(), + ); + +Map _$AuditEventToJson(AuditEvent instance) => + { + 'id': instance.id, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'type': instance.type, + 'target': instance.target, + 'location': instance.location, + 'ip_address': instance.ipAddress, + 'user_agent': instance.userAgent, + 'account': instance.account.toJson(), + 'account_id': instance.accountId, + }; diff --git a/lib/router.dart b/lib/router.dart index 8755ec4..6021481 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -6,6 +6,7 @@ import 'package:solian/models/post.dart'; import 'package:solian/models/realm.dart'; import 'package:solian/screens/about.dart'; import 'package:solian/screens/account.dart'; +import 'package:solian/screens/account/audit_log.dart'; import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/preferences/notifications.dart'; import 'package:solian/screens/account/preferences/security.dart'; @@ -275,6 +276,14 @@ abstract class AppRouter { child: const AuthPreferencesScreen(), ), ), + GoRoute( + path: '/account/audit', + name: 'auditLog', + builder: (context, state) => TitleShell( + state: state, + child: const AuditLogScreen(), + ), + ), GoRoute( path: '/account/view/:name', name: 'accountProfilePage', diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 525c7eb..eb11efb 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -129,6 +129,15 @@ class _AccountScreenState extends State { AppRouter.instance.pushNamed('settings'); }, ), + if (auth.isAuthorized.value) + ListTile( + leading: const Icon(Icons.event_repeat), + contentPadding: const EdgeInsets.symmetric(horizontal: 34), + title: Text('auditLog'.tr), + onTap: () { + AppRouter.instance.pushNamed('auditLog'); + }, + ), if (auth.isAuthorized.value) ListTile( leading: const Icon(Icons.lock), diff --git a/lib/screens/account/audit_log.dart b/lib/screens/account/audit_log.dart new file mode 100644 index 0000000..7747520 --- /dev/null +++ b/lib/screens/account/audit_log.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:get/get.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:solian/exceptions/request.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/models/audit_log.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/widgets/relative_date.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; +import 'package:timeline_tile/timeline_tile.dart'; + +class AuditLogScreen extends StatefulWidget { + const AuditLogScreen({super.key}); + + @override + State createState() => _AuditLogScreenState(); +} + +class _AuditLogScreenState extends State { + bool _isBusy = true; + + int _totalEvent = 0; + List _events = List.empty(growable: true); + + Future _getEvents() async { + if (!_isBusy) setState(() => _isBusy = true); + + final AuthProvider auth = Get.find(); + final client = await auth.configureClient('id'); + final resp = + await client.get('/users/me/events?take=10&offset=${_events.length}'); + if (resp.statusCode != 200) { + context.showErrorDialog(RequestException(resp)); + } + + final result = PaginationResult.fromJson(resp.body); + + setState(() { + _totalEvent = result.count; + _events.addAll( + result.data?.map((x) => AuditEvent.fromJson(x)).toList() ?? + List.empty(), + ); + _isBusy = false; + }); + } + + @override + void initState() { + super.initState(); + _getEvents(); + } + + @override + Widget build(BuildContext context) { + return InfiniteList( + itemCount: _events.length, + isLoading: _isBusy, + onFetchData: () { + _getEvents(); + }, + itemBuilder: (context, idx) { + final element = _events[idx]; + return TimelineTile( + isFirst: idx == 0, + isLast: _events.length - 1 == idx, + alignment: TimelineAlign.start, + endChild: Container( + child: Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + element.type, + style: GoogleFonts.robotoMono(fontSize: 15), + ), + Row( + children: [ + RelativeDate(element.createdAt), + const Gap(6), + Text('·'), + const Gap(6), + RelativeDate(element.createdAt, isFull: true), + ], + ), + ], + ).paddingSymmetric(horizontal: 12, vertical: 8), + ).paddingOnly(left: 16), + ), + ).paddingSymmetric(horizontal: 18); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 6931c92..e03dc3b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2034,6 +2034,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.0" + timeline_tile: + dependency: "direct main" + description: + name: timeline_tile + sha256: "85ec2023c67137397c2812e3e848b2fb20b410b67cd9aff304bb5480c376fc0c" + url: "https://pub.dev" + source: hosted + version: "2.0.0" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 43953ea..3e47b9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -84,6 +84,7 @@ dependencies: in_app_review: ^2.0.9 syntax_highlight: ^0.4.0 flutter_udid: ^3.0.0 + timeline_tile: ^2.0.0 dev_dependencies: flutter_test: