feat: Implement note encryption and synchronization features
- Added NoteEncryption class for encrypting and decrypting note content using AES-GCM. - Updated NoteRepository to handle synchronization of notes and categories with the server, including encryption of note data before sending. - Introduced SyncRequest and SyncResponse models for managing synchronization data. - Enhanced LocalVaultService to store and retrieve the encryption key. - Modified HomeScreen and SettingsScreen to trigger synchronization after note operations and manage API endpoint settings. - Added SyncStatusIndicator to provide visual feedback on synchronization status in the app title bar. - Created Category model to manage note categories with encryption support. - Updated note model to include UUID, server version, deletion status, and category ID. - Added necessary UI elements for displaying and managing the encryption key in SettingsScreen. - Updated dependencies in pubspec.yaml for cryptography and HTTP handling.
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
import 'package:notas/models/note.dart';
|
||||
import 'package:notas/models/category.dart';
|
||||
|
||||
// DTOs para sincronización con el servidor
|
||||
|
||||
class SyncRequest {
|
||||
SyncRequest({
|
||||
DateTime? lastSyncAt,
|
||||
required this.changes,
|
||||
}) : lastSyncAt = lastSyncAt ?? DateTime.utc(1970, 1, 1);
|
||||
|
||||
final DateTime lastSyncAt;
|
||||
final SyncChanges changes;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lastSyncAt': lastSyncAt.toIso8601String(),
|
||||
'changes': changes.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SyncChanges {
|
||||
const SyncChanges({
|
||||
this.categories = const [],
|
||||
this.notes = const [],
|
||||
});
|
||||
|
||||
final List<SyncCategoryPayload> categories;
|
||||
final List<SyncNotePayload> notes;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (categories.isNotEmpty)
|
||||
'categories': categories.map((c) => c.toJson()).toList(),
|
||||
if (notes.isNotEmpty)
|
||||
'notes': notes.map((n) => n.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SyncChangesResponse {
|
||||
const SyncChangesResponse({
|
||||
this.categories = const [],
|
||||
this.notes = const [],
|
||||
});
|
||||
|
||||
final List<SyncCategoryResponse> categories;
|
||||
final List<SyncNoteResponse> notes;
|
||||
|
||||
factory SyncChangesResponse.fromJson(Map<String, dynamic> json) {
|
||||
final List<dynamic> categoriesJson = json['categories'] as List<dynamic>? ?? [];
|
||||
final List<dynamic> notesJson = json['notes'] as List<dynamic>? ?? [];
|
||||
|
||||
return SyncChangesResponse(
|
||||
categories: categoriesJson
|
||||
.map((c) => SyncCategoryResponse.fromJson(c as Map<String, dynamic>))
|
||||
.toList(),
|
||||
notes: notesJson
|
||||
.map((n) => SyncNoteResponse.fromJson(n as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
class SyncCategoryPayload {
|
||||
const SyncCategoryPayload({
|
||||
required this.id,
|
||||
required this.encryptedName,
|
||||
required this.serverVersion,
|
||||
this.isDeleted = false,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String id; // uuid
|
||||
final String encryptedName;
|
||||
final int serverVersion;
|
||||
final bool isDeleted;
|
||||
final DateTime updatedAt;
|
||||
|
||||
factory SyncCategoryPayload.fromCategory(Category category) {
|
||||
return SyncCategoryPayload(
|
||||
id: category.uuid,
|
||||
encryptedName: category.encryptedName,
|
||||
serverVersion: category.serverVersion,
|
||||
isDeleted: category.isDeleted,
|
||||
updatedAt: category.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'encrypted_name': encryptedName,
|
||||
'serverVersion': serverVersion,
|
||||
'isDeleted': isDeleted,
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SyncNotePayload {
|
||||
const SyncNotePayload({
|
||||
required this.id,
|
||||
this.categoryId,
|
||||
required this.encryptedTitle,
|
||||
required this.encryptedBody,
|
||||
required this.serverVersion,
|
||||
this.position = 0,
|
||||
this.isDeleted = false,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String id; // uuid
|
||||
final String? categoryId;
|
||||
final String encryptedTitle;
|
||||
final String encryptedBody;
|
||||
final int serverVersion;
|
||||
final int position;
|
||||
final bool isDeleted;
|
||||
final DateTime updatedAt;
|
||||
|
||||
factory SyncNotePayload.fromNote(
|
||||
Note note, {
|
||||
required String encryptedTitle,
|
||||
required String encryptedBody,
|
||||
}) {
|
||||
return SyncNotePayload(
|
||||
id: note.uuid,
|
||||
categoryId: note.categoryId,
|
||||
encryptedTitle: encryptedTitle,
|
||||
encryptedBody: encryptedBody,
|
||||
serverVersion: note.serverVersion,
|
||||
position: note.index,
|
||||
isDeleted: note.isDeleted,
|
||||
updatedAt: note.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
if (categoryId != null) 'categoryId': categoryId,
|
||||
'encrypted_title': encryptedTitle,
|
||||
'encrypted_body': encryptedBody,
|
||||
'serverVersion': serverVersion,
|
||||
if (position != 0) 'position': position,
|
||||
if (isDeleted) 'isDeleted': isDeleted,
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SyncResponse {
|
||||
const SyncResponse({
|
||||
required this.serverTimestamp,
|
||||
required this.synced,
|
||||
required this.changes,
|
||||
});
|
||||
|
||||
final DateTime serverTimestamp;
|
||||
final bool synced;
|
||||
final SyncChangesResponse changes;
|
||||
|
||||
factory SyncResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SyncResponse(
|
||||
serverTimestamp:
|
||||
DateTime.parse(json['serverTimestamp'] as String),
|
||||
synced: json['synced'] as bool? ?? false,
|
||||
changes: SyncChangesResponse.fromJson(
|
||||
json['changes'] as Map<String, dynamic>? ?? {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncCategoryResponse {
|
||||
const SyncCategoryResponse({
|
||||
required this.id,
|
||||
required this.encryptedName,
|
||||
required this.serverVersion,
|
||||
this.isDeleted = false,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String id; // uuid
|
||||
final String encryptedName;
|
||||
final int serverVersion;
|
||||
final bool isDeleted;
|
||||
final DateTime updatedAt;
|
||||
|
||||
factory SyncCategoryResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SyncCategoryResponse(
|
||||
id: json['id'] as String,
|
||||
encryptedName: json['encrypted_name'] as String,
|
||||
serverVersion: json['serverVersion'] as int,
|
||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Category toCategory() {
|
||||
return Category(
|
||||
uuid: id,
|
||||
encryptedName: encryptedName,
|
||||
serverVersion: serverVersion,
|
||||
isDeleted: isDeleted,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SyncNoteResponse {
|
||||
const SyncNoteResponse({
|
||||
required this.id,
|
||||
this.categoryId,
|
||||
required this.encryptedTitle,
|
||||
required this.encryptedBody,
|
||||
required this.serverVersion,
|
||||
this.position = 0,
|
||||
this.isDeleted = false,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String id; // uuid
|
||||
final String? categoryId;
|
||||
final String encryptedTitle;
|
||||
final String encryptedBody;
|
||||
final int serverVersion;
|
||||
final int position;
|
||||
final bool isDeleted;
|
||||
final DateTime updatedAt;
|
||||
|
||||
factory SyncNoteResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SyncNoteResponse(
|
||||
id: json['id'] as String,
|
||||
categoryId: json['categoryId'] as String?,
|
||||
encryptedTitle: json['encrypted_title'] as String,
|
||||
encryptedBody: json['encrypted_body'] as String,
|
||||
serverVersion: json['serverVersion'] as int,
|
||||
position: json['position'] as int? ?? 0,
|
||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
Note toNote() {
|
||||
return Note(
|
||||
uuid: id,
|
||||
title: 'Encrypted', // placeholder, será descifrado por la app
|
||||
body: 'Encrypted', // placeholder, será descifrado por la app
|
||||
createdAt: updatedAt,
|
||||
updatedAt: updatedAt,
|
||||
index: position,
|
||||
serverVersion: serverVersion,
|
||||
isDeleted: isDeleted,
|
||||
categoryId: categoryId,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user