feat(profile): implement user-initiated GDPR-compliant profile deletion
- Add DELETE /users/me endpoint with cascading data removal - Implement frontend confirmation dialog and deletion flow - Add audit logging for deletion requests - Update localization files for new UI strings - Add scheduled cleanup service for AI traces - Document GDPR compliance in technical specification BREAKING CHANGE: Users can now permanently delete their profiles and associated data
This commit is contained in:
@@ -42,6 +42,14 @@ class ProfileRepository {
|
||||
return UserProfile.fromJson(data);
|
||||
}
|
||||
|
||||
Future<void> deleteMe() async {
|
||||
final token = await _ref.read(authStateProvider.future);
|
||||
await guardedApiCall(
|
||||
_ref,
|
||||
() => _apiClient.deleteJson(UserApiPaths.me, token: token),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> refreshCategories() async {
|
||||
final token = await _ref.read(authStateProvider.future);
|
||||
await guardedApiCall(
|
||||
|
||||
@@ -20,6 +20,7 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = true;
|
||||
bool _isSaving = false;
|
||||
bool _isDeleting = false;
|
||||
String? _error;
|
||||
UserProfile? _profile;
|
||||
|
||||
@@ -96,6 +97,61 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
context.go('/login');
|
||||
}
|
||||
|
||||
Future<void> _showDeleteProfileConfirmation() async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(context.l10n.profileDeleteConfirmTitle),
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: <Widget>[
|
||||
Text(context.l10n.profileDeleteConfirmMessage),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text(context.l10n.noLabel),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(context.l10n.deleteAction),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_deleteProfile();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteProfile() async {
|
||||
setState(() => _isDeleting = true);
|
||||
try {
|
||||
await ref.read(profileRepositoryProvider).deleteMe();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.profileDeletedMessage)),
|
||||
);
|
||||
await ref.read(authStateProvider.notifier).logout();
|
||||
if (!mounted) return;
|
||||
context.go('/login');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
buildCopyableErrorSnackBar(context, mapErrorToUserMessage(e, context)),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isDeleting = false);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildProfileForm(BuildContext context, ThemeData theme) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
@@ -286,6 +342,24 @@ class _ProfileScreenState extends ConsumerState<ProfileScreen> {
|
||||
label: Text(context.l10n.logoutAction),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: _isDeleting ? null : _showDeleteProfileConfirmation,
|
||||
icon: _isDeleting
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.delete_forever_outlined),
|
||||
label: Text(context.l10n.profileDeleteAction),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user