feat(profile): implement user-initiated GDPR-compliant profile deletion
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Failing after 4m36s
Test Suite / flutter-quality (push) Failing after 40s

- 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:
Nils-Johan Gynther
2026-05-21 22:19:50 +02:00
parent 6ddb58dc7c
commit 8c9da36312
23 changed files with 776 additions and 34 deletions
@@ -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,
),
),
),
],
),
),