From cb2de52bee42b8333621379049951de0a942be86 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 3 Mar 2025 23:04:02 +0800 Subject: [PATCH] :sparkles: Key pairs --- assets/translations/en-US.json | 8 +- assets/translations/zh-CN.json | 8 +- assets/translations/zh-HK.json | 8 +- assets/translations/zh-TW.json | 8 +- .../my_database/drift_schema_v2.json | 2 +- lib/database/database.g.dart | 67 ++++++++++- lib/database/database.steps.dart | 9 ++ lib/database/keypair.dart | 2 + lib/main.dart | 1 + lib/providers/keypair.dart | 66 +++++++---- lib/router.dart | 19 +++- lib/screens/account.dart | 27 ++++- lib/screens/account/keypairs.dart | 106 ++++++++++++++++++ lib/types/keypair.dart | 6 +- lib/types/keypair.freezed.dart | 40 ++++++- lib/types/keypair.g.dart | 2 + .../my_database/generated/schema_v2.dart | 45 +++++++- 17 files changed, 369 insertions(+), 55 deletions(-) create mode 100644 lib/screens/account/keypairs.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 04e668c..e5e8056 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -753,5 +753,11 @@ "accountBadges": "Badges", "accountBadgesDescription": "View and manage your badges.", "badgeActivated": "Activated badge {}.", - "viewDetailedAttachment": "Details" + "viewDetailedAttachment": "Details", + "screenKeyPairs": "Key Pairs", + "accountKeyPairs": "Key Pairs", + "accountKeyPairsDescription": "Manage the key pairs which used to encrypt messages.", + "enrollNewKeyPair": "Enroll New One", + "enrollNewKeyPairDescription": "Generate a new key pair.", + "keyPairHasPrivateKey": "With private key" } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index a5881b8..dfa4c75 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -751,5 +751,11 @@ "accountBadges": "徽章", "accountBadgesDescription": "查看并管理你的徽章。", "badgeActivated": "已佩戴徽章 {}。", - "viewDetailedAttachment": "查看附件详情" + "viewDetailedAttachment": "查看附件详情", + "screenKeyPairs": "密钥对", + "accountKeyPairs": "密钥对", + "accountKeyPairsDescription": "管理用于加密信息的密钥对。", + "enrollNewKeyPair": "新建密钥对", + "enrollNewKeyPairDescription": "生成一对新密钥对。", + "keyPairHasPrivateKey": "有私钥" } diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index a322e9d..ee3f771 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -751,5 +751,11 @@ "accountBadges": "徽章", "accountBadgesDescription": "查看並管理你的徽章。", "badgeActivated": "已佩戴徽章 {}。", - "viewDetailedAttachment": "查看附件詳情" + "viewDetailedAttachment": "查看附件詳情", + "screenKeyPairs": "密鑰對", + "accountKeyPairs": "密鑰對", + "accountKeyPairsDescription": "管理用於加密信息的密鑰對。", + "enrollNewKeyPair": "新建密鑰對", + "enrollNewKeyPairDescription": "生成一對新密鑰對,覆蓋當前的;如果已有一個密鑰將會丟棄舊密鑰的私鑰。", + "keyPairHasPrivateKey": "有私鑰" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index dbc2327..356b069 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -751,5 +751,11 @@ "accountBadges": "徽章", "accountBadgesDescription": "查看並管理你的徽章。", "badgeActivated": "已佩戴徽章 {}。", - "viewDetailedAttachment": "查看附件詳情" + "viewDetailedAttachment": "查看附件詳情", + "screenKeyPairs": "密鑰對", + "accountKeyPairs": "密鑰對", + "accountKeyPairsDescription": "管理用於加密信息的密鑰對。", + "enrollNewKeyPair": "新建密鑰對", + "enrollNewKeyPairDescription": "生成一對新密鑰對,覆蓋當前的;如果已有一個密鑰將會丟棄舊密鑰的私鑰。", + "keyPairHasPrivateKey": "有私鑰" } diff --git a/drift_schemas/my_database/drift_schema_v2.json b/drift_schemas/my_database/drift_schema_v2.json index 5c0e304..74978a3 100644 --- a/drift_schemas/my_database/drift_schema_v2.json +++ b/drift_schemas/my_database/drift_schema_v2.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"sn_local_key_pair","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"sn_local_key_pair","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_active","getter_name":"isActive","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_active\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_active\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]} \ No newline at end of file diff --git a/lib/database/database.g.dart b/lib/database/database.g.dart index 40ef60a..cd6c0a1 100644 --- a/lib/database/database.g.dart +++ b/lib/database/database.g.dart @@ -567,8 +567,19 @@ class $SnLocalKeyPairTable extends SnLocalKeyPair late final GeneratedColumn privateKey = GeneratedColumn( 'private_key', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _isActiveMeta = + const VerificationMeta('isActive'); @override - List get $columns => [id, accountId, publicKey, privateKey]; + late final GeneratedColumn isActive = GeneratedColumn( + 'is_active', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_active" IN (0, 1))'), + defaultValue: Constant(false)); + @override + List get $columns => + [id, accountId, publicKey, privateKey, isActive]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -602,6 +613,10 @@ class $SnLocalKeyPairTable extends SnLocalKeyPair privateKey.isAcceptableOrUnknown( data['private_key']!, _privateKeyMeta)); } + if (data.containsKey('is_active')) { + context.handle(_isActiveMeta, + isActive.isAcceptableOrUnknown(data['is_active']!, _isActiveMeta)); + } return context; } @@ -619,6 +634,8 @@ class $SnLocalKeyPairTable extends SnLocalKeyPair .read(DriftSqlType.string, data['${effectivePrefix}public_key'])!, privateKey: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}private_key']), + isActive: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_active'])!, ); } @@ -634,11 +651,13 @@ class SnLocalKeyPairData extends DataClass final int accountId; final String publicKey; final String? privateKey; + final bool isActive; const SnLocalKeyPairData( {required this.id, required this.accountId, required this.publicKey, - this.privateKey}); + this.privateKey, + required this.isActive}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -648,6 +667,7 @@ class SnLocalKeyPairData extends DataClass if (!nullToAbsent || privateKey != null) { map['private_key'] = Variable(privateKey); } + map['is_active'] = Variable(isActive); return map; } @@ -659,6 +679,7 @@ class SnLocalKeyPairData extends DataClass privateKey: privateKey == null && nullToAbsent ? const Value.absent() : Value(privateKey), + isActive: Value(isActive), ); } @@ -670,6 +691,7 @@ class SnLocalKeyPairData extends DataClass accountId: serializer.fromJson(json['accountId']), publicKey: serializer.fromJson(json['publicKey']), privateKey: serializer.fromJson(json['privateKey']), + isActive: serializer.fromJson(json['isActive']), ); } @override @@ -680,6 +702,7 @@ class SnLocalKeyPairData extends DataClass 'accountId': serializer.toJson(accountId), 'publicKey': serializer.toJson(publicKey), 'privateKey': serializer.toJson(privateKey), + 'isActive': serializer.toJson(isActive), }; } @@ -687,12 +710,14 @@ class SnLocalKeyPairData extends DataClass {String? id, int? accountId, String? publicKey, - Value privateKey = const Value.absent()}) => + Value privateKey = const Value.absent(), + bool? isActive}) => SnLocalKeyPairData( id: id ?? this.id, accountId: accountId ?? this.accountId, publicKey: publicKey ?? this.publicKey, privateKey: privateKey.present ? privateKey.value : this.privateKey, + isActive: isActive ?? this.isActive, ); SnLocalKeyPairData copyWithCompanion(SnLocalKeyPairCompanion data) { return SnLocalKeyPairData( @@ -701,6 +726,7 @@ class SnLocalKeyPairData extends DataClass publicKey: data.publicKey.present ? data.publicKey.value : this.publicKey, privateKey: data.privateKey.present ? data.privateKey.value : this.privateKey, + isActive: data.isActive.present ? data.isActive.value : this.isActive, ); } @@ -710,13 +736,15 @@ class SnLocalKeyPairData extends DataClass ..write('id: $id, ') ..write('accountId: $accountId, ') ..write('publicKey: $publicKey, ') - ..write('privateKey: $privateKey') + ..write('privateKey: $privateKey, ') + ..write('isActive: $isActive') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, accountId, publicKey, privateKey); + int get hashCode => + Object.hash(id, accountId, publicKey, privateKey, isActive); @override bool operator ==(Object other) => identical(this, other) || @@ -724,7 +752,8 @@ class SnLocalKeyPairData extends DataClass other.id == this.id && other.accountId == this.accountId && other.publicKey == this.publicKey && - other.privateKey == this.privateKey); + other.privateKey == this.privateKey && + other.isActive == this.isActive); } class SnLocalKeyPairCompanion extends UpdateCompanion { @@ -732,12 +761,14 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { final Value accountId; final Value publicKey; final Value privateKey; + final Value isActive; final Value rowid; const SnLocalKeyPairCompanion({ this.id = const Value.absent(), this.accountId = const Value.absent(), this.publicKey = const Value.absent(), this.privateKey = const Value.absent(), + this.isActive = const Value.absent(), this.rowid = const Value.absent(), }); SnLocalKeyPairCompanion.insert({ @@ -745,6 +776,7 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { required int accountId, required String publicKey, this.privateKey = const Value.absent(), + this.isActive = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), accountId = Value(accountId), @@ -754,6 +786,7 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { Expression? accountId, Expression? publicKey, Expression? privateKey, + Expression? isActive, Expression? rowid, }) { return RawValuesInsertable({ @@ -761,6 +794,7 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { if (accountId != null) 'account_id': accountId, if (publicKey != null) 'public_key': publicKey, if (privateKey != null) 'private_key': privateKey, + if (isActive != null) 'is_active': isActive, if (rowid != null) 'rowid': rowid, }); } @@ -770,12 +804,14 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { Value? accountId, Value? publicKey, Value? privateKey, + Value? isActive, Value? rowid}) { return SnLocalKeyPairCompanion( id: id ?? this.id, accountId: accountId ?? this.accountId, publicKey: publicKey ?? this.publicKey, privateKey: privateKey ?? this.privateKey, + isActive: isActive ?? this.isActive, rowid: rowid ?? this.rowid, ); } @@ -795,6 +831,9 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { if (privateKey.present) { map['private_key'] = Variable(privateKey.value); } + if (isActive.present) { + map['is_active'] = Variable(isActive.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -808,6 +847,7 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { ..write('accountId: $accountId, ') ..write('publicKey: $publicKey, ') ..write('privateKey: $privateKey, ') + ..write('isActive: $isActive, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -1152,6 +1192,7 @@ typedef $$SnLocalKeyPairTableCreateCompanionBuilder = SnLocalKeyPairCompanion required int accountId, required String publicKey, Value privateKey, + Value isActive, Value rowid, }); typedef $$SnLocalKeyPairTableUpdateCompanionBuilder = SnLocalKeyPairCompanion @@ -1160,6 +1201,7 @@ typedef $$SnLocalKeyPairTableUpdateCompanionBuilder = SnLocalKeyPairCompanion Value accountId, Value publicKey, Value privateKey, + Value isActive, Value rowid, }); @@ -1183,6 +1225,9 @@ class $$SnLocalKeyPairTableFilterComposer ColumnFilters get privateKey => $composableBuilder( column: $table.privateKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get isActive => $composableBuilder( + column: $table.isActive, builder: (column) => ColumnFilters(column)); } class $$SnLocalKeyPairTableOrderingComposer @@ -1205,6 +1250,9 @@ class $$SnLocalKeyPairTableOrderingComposer ColumnOrderings get privateKey => $composableBuilder( column: $table.privateKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get isActive => $composableBuilder( + column: $table.isActive, builder: (column) => ColumnOrderings(column)); } class $$SnLocalKeyPairTableAnnotationComposer @@ -1227,6 +1275,9 @@ class $$SnLocalKeyPairTableAnnotationComposer GeneratedColumn get privateKey => $composableBuilder( column: $table.privateKey, builder: (column) => column); + + GeneratedColumn get isActive => + $composableBuilder(column: $table.isActive, builder: (column) => column); } class $$SnLocalKeyPairTableTableManager extends RootTableManager< @@ -1260,6 +1311,7 @@ class $$SnLocalKeyPairTableTableManager extends RootTableManager< Value accountId = const Value.absent(), Value publicKey = const Value.absent(), Value privateKey = const Value.absent(), + Value isActive = const Value.absent(), Value rowid = const Value.absent(), }) => SnLocalKeyPairCompanion( @@ -1267,6 +1319,7 @@ class $$SnLocalKeyPairTableTableManager extends RootTableManager< accountId: accountId, publicKey: publicKey, privateKey: privateKey, + isActive: isActive, rowid: rowid, ), createCompanionCallback: ({ @@ -1274,6 +1327,7 @@ class $$SnLocalKeyPairTableTableManager extends RootTableManager< required int accountId, required String publicKey, Value privateKey = const Value.absent(), + Value isActive = const Value.absent(), Value rowid = const Value.absent(), }) => SnLocalKeyPairCompanion.insert( @@ -1281,6 +1335,7 @@ class $$SnLocalKeyPairTableTableManager extends RootTableManager< accountId: accountId, publicKey: publicKey, privateKey: privateKey, + isActive: isActive, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/database/database.steps.dart b/lib/database/database.steps.dart index f41f1f6..e5972e9 100644 --- a/lib/database/database.steps.dart +++ b/lib/database/database.steps.dart @@ -53,6 +53,7 @@ final class Schema2 extends i0.VersionedSchema { _column_6, _column_7, _column_8, + _column_9, ], attachedDatabase: database, ), @@ -115,6 +116,8 @@ class Shape2 extends i0.VersionedTable { columnsByName['public_key']! as i1.GeneratedColumn; i1.GeneratedColumn get privateKey => columnsByName['private_key']! as i1.GeneratedColumn; + i1.GeneratedColumn get isActive => + columnsByName['is_active']! as i1.GeneratedColumn; } i1.GeneratedColumn _column_5(String aliasedName) => @@ -129,6 +132,12 @@ i1.GeneratedColumn _column_7(String aliasedName) => i1.GeneratedColumn _column_8(String aliasedName) => i1.GeneratedColumn('private_key', aliasedName, true, type: i1.DriftSqlType.string); +i1.GeneratedColumn _column_9(String aliasedName) => + i1.GeneratedColumn('is_active', aliasedName, false, + type: i1.DriftSqlType.bool, + defaultConstraints: i1.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_active" IN (0, 1))'), + defaultValue: const CustomExpression('0')); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, }) { diff --git a/lib/database/keypair.dart b/lib/database/keypair.dart index f0af5e0..05c4b5e 100644 --- a/lib/database/keypair.dart +++ b/lib/database/keypair.dart @@ -8,4 +8,6 @@ class SnLocalKeyPair extends Table { TextColumn get publicKey => text()(); TextColumn get privateKey => text().nullable()(); + + BoolColumn get isActive => boolean().withDefault(Constant(false))(); } diff --git a/lib/main.dart b/lib/main.dart index edf4f48..320ec5b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -309,6 +309,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { await notify.registerPushNotifications(); if (!mounted) return; final kp = context.read(); + await kp.reloadActive(); kp.listen(); if (!mounted) return; final sticker = context.read(); diff --git a/lib/providers/keypair.dart b/lib/providers/keypair.dart index f209c70..b1df38f 100644 --- a/lib/providers/keypair.dart +++ b/lib/providers/keypair.dart @@ -27,8 +27,6 @@ class KeyPairProvider { _dt = context.read(); _ua = context.read(); _ws = context.read(); - - reloadActive(); } void listen() { @@ -150,6 +148,7 @@ class KeyPairProvider { final kp = await (_dt.db.snLocalKeyPair.select() ..where((e) => e.accountId.equals(_ua.user!.id)) ..where((e) => e.privateKey.isNotNull()) + ..where((e) => e.isActive.equals(true)) ..limit(1)) .getSingleOrNull(); @@ -169,16 +168,42 @@ class KeyPairProvider { return activeKp; } - Future enrollNew() async { - if (!_ua.isAuthorized) throw Exception('Unauthorized'); + Future> listKeyPair() async { + final kps = await (_dt.db.snLocalKeyPair.select()).get(); + return kps + .map((e) => SnKeyPair( + id: e.id, + accountId: e.accountId, + publicKey: e.publicKey, + privateKey: e.privateKey, + isActive: e.isActive, + )) + .toList(); + } - final existsOne = await (_dt.db.snLocalKeyPair.select() - ..where((e) => e.accountId.equals(_ua.user!.id)) + Future activeKeyPair(String kpId) async { + final kp = await (_dt.db.snLocalKeyPair.select() + ..where((e) => e.id.equals(kpId)) ..where((e) => e.privateKey.isNotNull()) ..limit(1)) .getSingleOrNull(); + if (kp == null) return; - final id = existsOne?.id ?? const Uuid().v4(); + await _dt.db.transaction(() async { + await (_dt.db.update(_dt.db.snLocalKeyPair) + ..where((e) => e.isActive.equals(true))) + .write(SnLocalKeyPairCompanion(isActive: Value(false))); + + await (_dt.db.update(_dt.db.snLocalKeyPair) + ..where((e) => e.id.equals(kp.id))) + .write(SnLocalKeyPairCompanion(isActive: Value(true))); + }); + } + + Future enrollNew() async { + if (!_ua.isAuthorized) throw Exception('Unauthorized'); + + final id = const Uuid().v4(); final kp = await RSA.generate(2048); final kpMeta = SnKeyPair( id: id, @@ -189,20 +214,21 @@ class KeyPairProvider { // Save the keypair to the local database // If there is already one with private key, it will be overwritten - await _dt.db.snLocalKeyPair.insertOne( - SnLocalKeyPairCompanion.insert( - id: kpMeta.id, - accountId: kpMeta.accountId, - publicKey: kpMeta.publicKey, - privateKey: Value(kpMeta.privateKey), - ), - onConflict: DoUpdate( - (_) => SnLocalKeyPairCompanion.custom( - publicKey: Constant(kpMeta.publicKey), - privateKey: Constant(kpMeta.privateKey), + await _dt.db.transaction(() async { + await (_dt.db.update(_dt.db.snLocalKeyPair) + ..where((e) => e.isActive.equals(true))) + .write(SnLocalKeyPairCompanion(isActive: Value(false))); + + await _dt.db.snLocalKeyPair.insertOne( + SnLocalKeyPairCompanion.insert( + id: kpMeta.id, + accountId: kpMeta.accountId, + publicKey: kpMeta.publicKey, + privateKey: Value(kpMeta.privateKey), + isActive: Value(true), ), - ), - ); + ); + }); await reloadActive(autoEnroll: false); diff --git a/lib/router.dart b/lib/router.dart index b3abde9..7b61425 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -6,6 +6,7 @@ import 'package:surface/screens/account.dart'; import 'package:surface/screens/account/account_settings.dart'; import 'package:surface/screens/account/badges.dart'; import 'package:surface/screens/account/factor_settings.dart'; +import 'package:surface/screens/account/keypairs.dart'; import 'package:surface/screens/account/profile_page.dart'; import 'package:surface/screens/account/profile_edit.dart'; import 'package:surface/screens/account/publishers/publisher_edit.dart'; @@ -43,8 +44,8 @@ import 'package:surface/types/post.dart'; import 'package:surface/widgets/about.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; -Widget _fadeThroughTransition( - BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { +Widget _fadeThroughTransition(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { return FadeThroughTransition( animation: animation, secondaryAnimation: secondaryAnimation, @@ -86,13 +87,15 @@ final _appRoutes = [ name: 'postSearch', builder: (context, state) => PostSearchScreen( initialTags: state.uri.queryParameters['tags']?.split(','), - initialCategories: state.uri.queryParameters['categories']?.split(','), + initialCategories: + state.uri.queryParameters['categories']?.split(','), ), ), GoRoute( path: '/publishers/:name', name: 'postPublisher', - builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!), + builder: (context, state) => + PostPublisherScreen(name: state.pathParameters['name']!), ), GoRoute( path: '/:slug', @@ -119,6 +122,11 @@ final _appRoutes = [ name: 'accountWallet', builder: (context, state) => const WalletScreen(), ), + GoRoute( + path: '/keypairs', + name: 'accountKeyPairs', + builder: (context, state) => const KeyPairScreen(), + ), GoRoute( path: '/settings', name: 'accountSettings', @@ -222,7 +230,8 @@ final _appRoutes = [ GoRoute( path: '/:alias', name: 'realmDetail', - builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!), + builder: (context, state) => + RealmDetailScreen(alias: state.pathParameters['alias']!), ), ], ), diff --git a/lib/screens/account.dart b/lib/screens/account.dart index ff8f006..acd5dfa 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -45,7 +45,8 @@ class AccountScreen extends StatelessWidget { ? Stack( fit: StackFit.expand, children: [ - AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover), + AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), + fit: BoxFit.cover), Positioned( top: 0, left: 0, @@ -79,7 +80,9 @@ class AccountScreen extends StatelessWidget { ], ), body: SingleChildScrollView( - child: ua.isAuthorized ? _AuthorizedAccountScreen() : _UnauthorizedAccountScreen(), + child: ua.isAuthorized + ? _AuthorizedAccountScreen() + : _UnauthorizedAccountScreen(), ), ); } @@ -115,9 +118,11 @@ class _AuthorizedAccountScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ - Text(ua.user!.nick).textStyle(Theme.of(context).textTheme.titleLarge!), + Text(ua.user!.nick) + .textStyle(Theme.of(context).textTheme.titleLarge!), const Gap(4), - Text('@${ua.user!.name}').textStyle(Theme.of(context).textTheme.bodySmall!), + Text('@${ua.user!.name}') + .textStyle(Theme.of(context).textTheme.bodySmall!), ], ), Text( @@ -183,6 +188,16 @@ class _AuthorizedAccountScreen extends StatelessWidget { GoRouter.of(context).pushNamed('accountBadges'); }, ), + ListTile( + title: Text('accountKeyPairs').tr(), + subtitle: Text('accountKeyPairsDescription').tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + leading: const Icon(Symbols.key), + trailing: const Icon(Symbols.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed('accountKeyPairs'); + }, + ), ListTile( title: Text('accountSettings').tr(), subtitle: Text('accountSettingsSubtitle').tr(), @@ -236,7 +251,9 @@ class _UnauthorizedAccountScreen extends StatelessWidget { child: Icon(Symbols.waving_hand, size: 28), ), const Gap(8), - Text('accountIntroTitle').tr().textStyle(Theme.of(context).textTheme.titleLarge!), + Text('accountIntroTitle') + .tr() + .textStyle(Theme.of(context).textTheme.titleLarge!), Text('accountIntroSubtitle').tr(), ], ).padding(all: 20), diff --git a/lib/screens/account/keypairs.dart b/lib/screens/account/keypairs.dart new file mode 100644 index 0000000..8360452 --- /dev/null +++ b/lib/screens/account/keypairs.dart @@ -0,0 +1,106 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/providers/keypair.dart'; +import 'package:surface/types/keypair.dart'; +import 'package:surface/widgets/loading_indicator.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; + +class KeyPairScreen extends StatefulWidget { + const KeyPairScreen({super.key}); + + @override + State createState() => _KeyPairScreenState(); +} + +class _KeyPairScreenState extends State { + bool _isBusy = false; + List? _keyPairs; + + Future _loadKeyPairs() async { + setState(() => _isBusy = true); + final kps = await context.read().listKeyPair(); + setState(() { + _keyPairs = kps; + _isBusy = false; + }); + } + + @override + void initState() { + super.initState(); + _loadKeyPairs(); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: AppBar( + title: Text('screenKeyPairs').tr(), + ), + body: Column( + children: [ + LoadingIndicator(isActive: _isBusy), + ListTile( + leading: const Icon(Symbols.add), + title: Text('enrollNewKeyPair').tr(), + subtitle: Text('enrollNewKeyPairDescription').tr(), + onTap: () async { + await context.read().enrollNew(); + _loadKeyPairs(); + }, + ), + const Divider(height: 1), + if (_keyPairs != null) + Expanded( + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: RefreshIndicator( + onRefresh: _loadKeyPairs, + child: ListView.builder( + itemCount: _keyPairs!.length, + itemBuilder: (context, index) { + final kp = _keyPairs![index]; + return ListTile( + title: Text(kp.id.toUpperCase()), + subtitle: Row( + spacing: 8, + children: [ + if (kp.privateKey != null) + Text( + 'keyPairHasPrivateKey'.tr(), + ), + if (kp.privateKey != null) Text('·'), + Flexible( + flex: 1, + child: Text( + 'UID #${kp.accountId.toString().padLeft(8, '0')}', + style: GoogleFonts.robotoMono(), + ), + ), + ], + ), + trailing: IconButton( + icon: const Icon(Symbols.check), + onPressed: kp.isActive == true + ? null + : () async { + final k = context.read(); + await k.activeKeyPair(kp.id); + _loadKeyPairs(); + }, + ), + ); + }, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/types/keypair.dart b/lib/types/keypair.dart index 90b1c92..c993c0a 100644 --- a/lib/types/keypair.dart +++ b/lib/types/keypair.dart @@ -9,8 +9,10 @@ abstract class SnKeyPair with _$SnKeyPair { required String id, required int accountId, required String publicKey, + bool? isActive, String? privateKey, }) = _SnKeyPair; - factory SnKeyPair.fromJson(Map json) => _$SnKeyPairFromJson(json); -} \ No newline at end of file + factory SnKeyPair.fromJson(Map json) => + _$SnKeyPairFromJson(json); +} diff --git a/lib/types/keypair.freezed.dart b/lib/types/keypair.freezed.dart index 7fb67be..8f69314 100644 --- a/lib/types/keypair.freezed.dart +++ b/lib/types/keypair.freezed.dart @@ -18,6 +18,7 @@ mixin _$SnKeyPair { String get id; int get accountId; String get publicKey; + bool? get isActive; String? get privateKey; /// Create a copy of SnKeyPair @@ -40,6 +41,8 @@ mixin _$SnKeyPair { other.accountId == accountId) && (identical(other.publicKey, publicKey) || other.publicKey == publicKey) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && (identical(other.privateKey, privateKey) || other.privateKey == privateKey)); } @@ -47,11 +50,11 @@ mixin _$SnKeyPair { @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => - Object.hash(runtimeType, id, accountId, publicKey, privateKey); + Object.hash(runtimeType, id, accountId, publicKey, isActive, privateKey); @override String toString() { - return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, privateKey: $privateKey)'; + return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, isActive: $isActive, privateKey: $privateKey)'; } } @@ -60,7 +63,12 @@ abstract mixin class $SnKeyPairCopyWith<$Res> { factory $SnKeyPairCopyWith(SnKeyPair value, $Res Function(SnKeyPair) _then) = _$SnKeyPairCopyWithImpl; @useResult - $Res call({String id, int accountId, String publicKey, String? privateKey}); + $Res call( + {String id, + int accountId, + String publicKey, + bool? isActive, + String? privateKey}); } /// @nodoc @@ -78,6 +86,7 @@ class _$SnKeyPairCopyWithImpl<$Res> implements $SnKeyPairCopyWith<$Res> { Object? id = null, Object? accountId = null, Object? publicKey = null, + Object? isActive = freezed, Object? privateKey = freezed, }) { return _then(_self.copyWith( @@ -93,6 +102,10 @@ class _$SnKeyPairCopyWithImpl<$Res> implements $SnKeyPairCopyWith<$Res> { ? _self.publicKey : publicKey // ignore: cast_nullable_to_non_nullable as String, + isActive: freezed == isActive + ? _self.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, privateKey: freezed == privateKey ? _self.privateKey : privateKey // ignore: cast_nullable_to_non_nullable @@ -108,6 +121,7 @@ class _SnKeyPair implements SnKeyPair { {required this.id, required this.accountId, required this.publicKey, + this.isActive, this.privateKey}); factory _SnKeyPair.fromJson(Map json) => _$SnKeyPairFromJson(json); @@ -119,6 +133,8 @@ class _SnKeyPair implements SnKeyPair { @override final String publicKey; @override + final bool? isActive; + @override final String? privateKey; /// Create a copy of SnKeyPair @@ -146,6 +162,8 @@ class _SnKeyPair implements SnKeyPair { other.accountId == accountId) && (identical(other.publicKey, publicKey) || other.publicKey == publicKey) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && (identical(other.privateKey, privateKey) || other.privateKey == privateKey)); } @@ -153,11 +171,11 @@ class _SnKeyPair implements SnKeyPair { @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => - Object.hash(runtimeType, id, accountId, publicKey, privateKey); + Object.hash(runtimeType, id, accountId, publicKey, isActive, privateKey); @override String toString() { - return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, privateKey: $privateKey)'; + return 'SnKeyPair(id: $id, accountId: $accountId, publicKey: $publicKey, isActive: $isActive, privateKey: $privateKey)'; } } @@ -169,7 +187,12 @@ abstract mixin class _$SnKeyPairCopyWith<$Res> __$SnKeyPairCopyWithImpl; @override @useResult - $Res call({String id, int accountId, String publicKey, String? privateKey}); + $Res call( + {String id, + int accountId, + String publicKey, + bool? isActive, + String? privateKey}); } /// @nodoc @@ -187,6 +210,7 @@ class __$SnKeyPairCopyWithImpl<$Res> implements _$SnKeyPairCopyWith<$Res> { Object? id = null, Object? accountId = null, Object? publicKey = null, + Object? isActive = freezed, Object? privateKey = freezed, }) { return _then(_SnKeyPair( @@ -202,6 +226,10 @@ class __$SnKeyPairCopyWithImpl<$Res> implements _$SnKeyPairCopyWith<$Res> { ? _self.publicKey : publicKey // ignore: cast_nullable_to_non_nullable as String, + isActive: freezed == isActive + ? _self.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, privateKey: freezed == privateKey ? _self.privateKey : privateKey // ignore: cast_nullable_to_non_nullable diff --git a/lib/types/keypair.g.dart b/lib/types/keypair.g.dart index afdb885..349fb69 100644 --- a/lib/types/keypair.g.dart +++ b/lib/types/keypair.g.dart @@ -10,6 +10,7 @@ _SnKeyPair _$SnKeyPairFromJson(Map json) => _SnKeyPair( id: json['id'] as String, accountId: (json['account_id'] as num).toInt(), publicKey: json['public_key'] as String, + isActive: json['is_active'] as bool?, privateKey: json['private_key'] as String?, ); @@ -18,5 +19,6 @@ Map _$SnKeyPairToJson(_SnKeyPair instance) => 'id': instance.id, 'account_id': instance.accountId, 'public_key': instance.publicKey, + 'is_active': instance.isActive, 'private_key': instance.privateKey, }; diff --git a/test/drift/my_database/generated/schema_v2.dart b/test/drift/my_database/generated/schema_v2.dart index 988b770..2edd76e 100644 --- a/test/drift/my_database/generated/schema_v2.dart +++ b/test/drift/my_database/generated/schema_v2.dart @@ -465,8 +465,16 @@ class SnLocalKeyPair extends Table late final GeneratedColumn privateKey = GeneratedColumn( 'private_key', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn isActive = GeneratedColumn( + 'is_active', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_active" IN (0, 1))'), + defaultValue: const CustomExpression('0')); @override - List get $columns => [id, accountId, publicKey, privateKey]; + List get $columns => + [id, accountId, publicKey, privateKey, isActive]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -486,6 +494,8 @@ class SnLocalKeyPair extends Table .read(DriftSqlType.string, data['${effectivePrefix}public_key'])!, privateKey: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}private_key']), + isActive: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_active'])!, ); } @@ -501,11 +511,13 @@ class SnLocalKeyPairData extends DataClass final int accountId; final String publicKey; final String? privateKey; + final bool isActive; const SnLocalKeyPairData( {required this.id, required this.accountId, required this.publicKey, - this.privateKey}); + this.privateKey, + required this.isActive}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -515,6 +527,7 @@ class SnLocalKeyPairData extends DataClass if (!nullToAbsent || privateKey != null) { map['private_key'] = Variable(privateKey); } + map['is_active'] = Variable(isActive); return map; } @@ -526,6 +539,7 @@ class SnLocalKeyPairData extends DataClass privateKey: privateKey == null && nullToAbsent ? const Value.absent() : Value(privateKey), + isActive: Value(isActive), ); } @@ -537,6 +551,7 @@ class SnLocalKeyPairData extends DataClass accountId: serializer.fromJson(json['accountId']), publicKey: serializer.fromJson(json['publicKey']), privateKey: serializer.fromJson(json['privateKey']), + isActive: serializer.fromJson(json['isActive']), ); } @override @@ -547,6 +562,7 @@ class SnLocalKeyPairData extends DataClass 'accountId': serializer.toJson(accountId), 'publicKey': serializer.toJson(publicKey), 'privateKey': serializer.toJson(privateKey), + 'isActive': serializer.toJson(isActive), }; } @@ -554,12 +570,14 @@ class SnLocalKeyPairData extends DataClass {String? id, int? accountId, String? publicKey, - Value privateKey = const Value.absent()}) => + Value privateKey = const Value.absent(), + bool? isActive}) => SnLocalKeyPairData( id: id ?? this.id, accountId: accountId ?? this.accountId, publicKey: publicKey ?? this.publicKey, privateKey: privateKey.present ? privateKey.value : this.privateKey, + isActive: isActive ?? this.isActive, ); SnLocalKeyPairData copyWithCompanion(SnLocalKeyPairCompanion data) { return SnLocalKeyPairData( @@ -568,6 +586,7 @@ class SnLocalKeyPairData extends DataClass publicKey: data.publicKey.present ? data.publicKey.value : this.publicKey, privateKey: data.privateKey.present ? data.privateKey.value : this.privateKey, + isActive: data.isActive.present ? data.isActive.value : this.isActive, ); } @@ -577,13 +596,15 @@ class SnLocalKeyPairData extends DataClass ..write('id: $id, ') ..write('accountId: $accountId, ') ..write('publicKey: $publicKey, ') - ..write('privateKey: $privateKey') + ..write('privateKey: $privateKey, ') + ..write('isActive: $isActive') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, accountId, publicKey, privateKey); + int get hashCode => + Object.hash(id, accountId, publicKey, privateKey, isActive); @override bool operator ==(Object other) => identical(this, other) || @@ -591,7 +612,8 @@ class SnLocalKeyPairData extends DataClass other.id == this.id && other.accountId == this.accountId && other.publicKey == this.publicKey && - other.privateKey == this.privateKey); + other.privateKey == this.privateKey && + other.isActive == this.isActive); } class SnLocalKeyPairCompanion extends UpdateCompanion { @@ -599,12 +621,14 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { final Value accountId; final Value publicKey; final Value privateKey; + final Value isActive; final Value rowid; const SnLocalKeyPairCompanion({ this.id = const Value.absent(), this.accountId = const Value.absent(), this.publicKey = const Value.absent(), this.privateKey = const Value.absent(), + this.isActive = const Value.absent(), this.rowid = const Value.absent(), }); SnLocalKeyPairCompanion.insert({ @@ -612,6 +636,7 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { required int accountId, required String publicKey, this.privateKey = const Value.absent(), + this.isActive = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), accountId = Value(accountId), @@ -621,6 +646,7 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { Expression? accountId, Expression? publicKey, Expression? privateKey, + Expression? isActive, Expression? rowid, }) { return RawValuesInsertable({ @@ -628,6 +654,7 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { if (accountId != null) 'account_id': accountId, if (publicKey != null) 'public_key': publicKey, if (privateKey != null) 'private_key': privateKey, + if (isActive != null) 'is_active': isActive, if (rowid != null) 'rowid': rowid, }); } @@ -637,12 +664,14 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { Value? accountId, Value? publicKey, Value? privateKey, + Value? isActive, Value? rowid}) { return SnLocalKeyPairCompanion( id: id ?? this.id, accountId: accountId ?? this.accountId, publicKey: publicKey ?? this.publicKey, privateKey: privateKey ?? this.privateKey, + isActive: isActive ?? this.isActive, rowid: rowid ?? this.rowid, ); } @@ -662,6 +691,9 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { if (privateKey.present) { map['private_key'] = Variable(privateKey.value); } + if (isActive.present) { + map['is_active'] = Variable(isActive.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -675,6 +707,7 @@ class SnLocalKeyPairCompanion extends UpdateCompanion { ..write('accountId: $accountId, ') ..write('publicKey: $publicKey, ') ..write('privateKey: $privateKey, ') + ..write('isActive: $isActive, ') ..write('rowid: $rowid') ..write(')')) .toString();