feat: 文件存储安全加固 + 认证增强 + 媒体URL保护 + provider 重构
## 后端安全加固 - 新增 UserUploadPathResolver: 用户上传文件路径安全解析, 防目录穿越 - LocalReportFileStorage: 文件存储路径安全加固 - local_account_file_cleanup: 账号删除时文件清理逻辑增强 - AuthService: 认证逻辑增强 - file_endpoints / report_endpoints: 文件访问接口安全加固 - ai_chat_endpoints / doctor_endpoints: 接口安全调整 - Program.cs: 服务注册调整 ## 前端认证与媒体 - 新增 authenticated_network_image.dart: 带认证的图片加载组件 - auth_provider: 认证状态管理大幅增强(+173) - api_client: 网络客户端增强(+124) - chat_provider: 聊天 provider 重构(+76) - omron_device_provider: 蓝牙设备 provider 增强(+53) - sse_handler: SSE 处理增强(+35) - consultation_provider / data_providers / conversation_history_provider: 调整 ## 页面调整 - remaining_pages: 健康档案/饮食记录等页面增强(+115) - home_page / chat_messages_view: 主页微调 - doctor 端多页微调(consultations/dashboard/followups/patient_detail/profile/report_detail/reports) - report_pages / settings_pages / notification_prefs_page: 微调 - device_scan_page / diet_capture_page / admin_home_page: 微调 ## 测试 - 新增 file_path_security_tests: 文件路径安全测试 - 新增 protected_media_url_test: 媒体URL保护测试 - 新增 user_session_identity_test: 用户会话身份测试 - account_deletion_tests / application_service_tests / auth_tests: 更新
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../widgets/admin_drawer.dart';
|
||||
import '../../widgets/backoffice_ui.dart';
|
||||
import 'admin_doctors_page.dart';
|
||||
@@ -12,7 +13,11 @@ final adminPageProvider = NotifierProvider<AdminPageNotifier, String>(
|
||||
|
||||
class AdminPageNotifier extends Notifier<String> {
|
||||
@override
|
||||
String build() => 'doctors';
|
||||
String build() {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
return 'doctors';
|
||||
}
|
||||
|
||||
void set(String page) => state = page;
|
||||
}
|
||||
|
||||
|
||||
@@ -274,11 +274,12 @@ class _DeviceScanPageState extends ConsumerState<DeviceScanPage>
|
||||
if (await notifier.isDuplicateReading(device, reading)) return false;
|
||||
|
||||
final api = ref.read(apiClientProvider);
|
||||
await api.post('/api/health-records', data: reading.toHealthRecord());
|
||||
final records = <Map<String, dynamic>>[reading.toHealthRecord()];
|
||||
final heartRateRecord = reading.toHeartRateRecord();
|
||||
if (heartRateRecord != null) {
|
||||
await api.post('/api/health-records', data: heartRateRecord);
|
||||
records.add(heartRateRecord);
|
||||
}
|
||||
await api.post('/api/health-records/batch', data: records);
|
||||
await notifier.recordSuccessfulSync(device: device, reading: reading);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,10 @@ class DietFoodValidationException implements Exception {
|
||||
|
||||
class DietNotifier extends Notifier<DietState> {
|
||||
@override
|
||||
DietState build() => DietState();
|
||||
DietState build() {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
return DietState();
|
||||
}
|
||||
|
||||
void setImage(String path) {
|
||||
state = state.copyWith(imagePath: path);
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../widgets/backoffice_ui.dart';
|
||||
final _consListProvider = FutureProvider<List<Map<String, dynamic>>>((
|
||||
ref,
|
||||
) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/consultations');
|
||||
return (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../widgets/backoffice_ui.dart';
|
||||
import '../doctor/doctor_home_page.dart' show doctorPageProvider;
|
||||
|
||||
final _dashboardProvider = FutureProvider<Map<String, dynamic>?>((ref) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/dashboard');
|
||||
return res.data['data'] as Map<String, dynamic>?;
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../widgets/app_toast.dart';
|
||||
import '../../widgets/backoffice_ui.dart';
|
||||
|
||||
final _ptsSimple = FutureProvider<List<Map<String, dynamic>>>((ref) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/patients-simple');
|
||||
return (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
@@ -50,7 +51,9 @@ class _DoctorFollowUpEditPageState
|
||||
_titleCtrl.text = d['title'] ?? '';
|
||||
_notesCtrl.text = d['notes'] ?? '';
|
||||
_pid = d['userId']?.toString();
|
||||
final at = DateTime.tryParse(d['scheduledAt']?.toString() ?? '');
|
||||
final at = DateTime.tryParse(
|
||||
d['scheduledAt']?.toString() ?? '',
|
||||
)?.toLocal();
|
||||
if (at != null) {
|
||||
_date = at;
|
||||
_time = TimeOfDay.fromDateTime(at);
|
||||
@@ -261,7 +264,7 @@ class _DoctorFollowUpEditPageState
|
||||
final data = {
|
||||
'userId': _pid,
|
||||
'title': _titleCtrl.text.trim(),
|
||||
'scheduledAt': at.toIso8601String(),
|
||||
'scheduledAt': at.toUtc().toIso8601String(),
|
||||
'notes': _notesCtrl.text.trim().isEmpty ? null : _notesCtrl.text.trim(),
|
||||
};
|
||||
if (isEdit) {
|
||||
@@ -294,6 +297,7 @@ final _fupDetailForEdit = FutureProvider.family<Map<String, dynamic>?, String>((
|
||||
ref,
|
||||
id,
|
||||
) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/follow-ups');
|
||||
final items = (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../utils/backoffice_formatters.dart';
|
||||
import '../../widgets/backoffice_ui.dart';
|
||||
|
||||
final _fupRefresh = FutureProvider<String?>((ref) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/follow-ups');
|
||||
final items = (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
@@ -21,7 +22,11 @@ final _fupList = NotifierProvider<FupListN, List<Map<String, dynamic>>>(
|
||||
|
||||
class FupListN extends Notifier<List<Map<String, dynamic>>> {
|
||||
@override
|
||||
List<Map<String, dynamic>> build() => [];
|
||||
List<Map<String, dynamic>> build() {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
return [];
|
||||
}
|
||||
|
||||
void replace(List<Map<String, dynamic>> v) => state = v;
|
||||
void markDone(String id) => state = state
|
||||
.map((f) => f['id'] == id ? {...f, 'status': 'Completed'} : f)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../widgets/doctor_drawer.dart';
|
||||
import '../../widgets/backoffice_ui.dart';
|
||||
import 'doctor_dashboard_page.dart';
|
||||
@@ -97,6 +98,10 @@ final doctorPageProvider = NotifierProvider<DoctorPageNotifier, String>(
|
||||
|
||||
class DoctorPageNotifier extends Notifier<String> {
|
||||
@override
|
||||
String build() => 'dashboard';
|
||||
String build() {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
return 'dashboard';
|
||||
}
|
||||
|
||||
void set(String page) => state = page;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../../widgets/backoffice_ui.dart';
|
||||
|
||||
final _patientDetailProvider =
|
||||
FutureProvider.family<Map<String, dynamic>?, String>((ref, id) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/patients/$id');
|
||||
return res.data['data'] as Map<String, dynamic>?;
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../widgets/app_toast.dart';
|
||||
import '../../widgets/backoffice_ui.dart';
|
||||
|
||||
final _docProfileProvider = FutureProvider<Map<String, dynamic>?>((ref) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/profile');
|
||||
return res.data['data'] as Map<String, dynamic>?;
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../widgets/backoffice_ui.dart';
|
||||
|
||||
final _reportDetailProvider =
|
||||
FutureProvider.family<Map<String, dynamic>?, String>((ref, id) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/reports/$id');
|
||||
return res.data['data'] as Map<String, dynamic>?;
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../widgets/backoffice_ui.dart';
|
||||
final _reportsProvider = FutureProvider<List<Map<String, dynamic>>>((
|
||||
ref,
|
||||
) async {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/doctor/reports');
|
||||
return (res.data['data'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
|
||||
@@ -85,6 +85,7 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
}
|
||||
|
||||
void _sendMessage() {
|
||||
if (ref.read(chatProvider).isStreaming) return;
|
||||
final text = _textCtrl.text.trim();
|
||||
final imagePath = _pickedImagePath;
|
||||
if (text.isEmpty && imagePath == null) return;
|
||||
@@ -425,6 +426,9 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
}
|
||||
|
||||
Widget _buildInputBar() {
|
||||
final isStreaming = ref.watch(
|
||||
chatProvider.select((state) => state.isStreaming),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 10),
|
||||
child: Container(
|
||||
@@ -490,7 +494,9 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: _sendMessage,
|
||||
onTap: isStreaming
|
||||
? () => ref.read(chatProvider.notifier).stopGenerating()
|
||||
: _sendMessage,
|
||||
child: Ink(
|
||||
width: 38,
|
||||
height: 38,
|
||||
@@ -498,8 +504,8 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
gradient: AppColors.primaryGradient,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.send,
|
||||
child: Icon(
|
||||
isStreaming ? Icons.stop_rounded : LucideIcons.send,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
),
|
||||
|
||||
@@ -6,12 +6,12 @@ import '../../../core/app_colors.dart';
|
||||
import '../../../core/app_design_tokens.dart';
|
||||
import '../../../core/app_module_visuals.dart';
|
||||
import '../../../core/app_theme.dart';
|
||||
import '../../../core/api_client.dart' show baseUrl;
|
||||
import '../../../core/navigation_provider.dart';
|
||||
import '../../../providers/chat_provider.dart';
|
||||
import '../../../providers/data_providers.dart';
|
||||
import '../../../widgets/ai_content.dart';
|
||||
import '../../../widgets/app_toast.dart';
|
||||
import '../../../widgets/authenticated_network_image.dart';
|
||||
|
||||
ChatMessage messageAtDisplayIndex(List<ChatMessage> messages, int index) =>
|
||||
messages[index];
|
||||
@@ -1045,8 +1045,8 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
child: localPath != null
|
||||
? Image.file(File(localPath), fit: BoxFit.cover)
|
||||
: imageUrl != null
|
||||
? Image.network(
|
||||
_mediaUrl(imageUrl),
|
||||
? AuthenticatedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, e, s) => Container(
|
||||
width: 80,
|
||||
@@ -1122,7 +1122,11 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
|
||||
static void _showFullImage(BuildContext context, String? path) {
|
||||
if (path == null) return;
|
||||
final resolvedPath = _mediaUrl(path);
|
||||
final isNetwork =
|
||||
path.startsWith('http://') ||
|
||||
path.startsWith('https://') ||
|
||||
path.startsWith('/uploads/') ||
|
||||
path.startsWith('/api/');
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => Dialog(
|
||||
@@ -1134,9 +1138,12 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: InteractiveViewer(
|
||||
child: resolvedPath.startsWith('http')
|
||||
? Image.network(resolvedPath, fit: BoxFit.contain)
|
||||
: Image.file(File(resolvedPath), fit: BoxFit.contain),
|
||||
child: isNetwork
|
||||
? AuthenticatedNetworkImage(
|
||||
imageUrl: path,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
: Image.file(File(path), fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -1160,12 +1167,6 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static String _mediaUrl(String path) {
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
||||
if (path.startsWith('/uploads/')) return '$baseUrl$path';
|
||||
return path;
|
||||
}
|
||||
|
||||
/// 处理 AI 回复里的 markdown 链接点击:
|
||||
/// - app://diet → 触发拍照/相册选择,跳到饮食拍照流程
|
||||
/// - app://report → 跳到报告列表(用户可在那里上传新报告)
|
||||
|
||||
@@ -27,6 +27,7 @@ class DietRecordListPage extends ConsumerStatefulWidget {
|
||||
class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
List<Map<String, dynamic>> _data = [];
|
||||
bool _loading = true;
|
||||
String? _loadError;
|
||||
int _trendDays = 7;
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
|
||||
@@ -37,12 +38,22 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final records = await ref.read(dietServiceProvider).getRecords();
|
||||
if (mounted) {
|
||||
try {
|
||||
final records = await ref.read(dietServiceProvider).getRecords();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_data = records;
|
||||
_loading = false;
|
||||
_loadError = null;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
if (_data.isEmpty) {
|
||||
setState(() => _loadError = '网络异常或服务暂时不可用,请稍后重试');
|
||||
} else {
|
||||
AppToast.show(context, '刷新失败,已保留当前记录', type: AppToastType.error);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +115,19 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
body: _loadError == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: AppErrorState(
|
||||
title: '饮食记录加载失败',
|
||||
subtitle: _loadError,
|
||||
onRetry: () {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_loadError = null;
|
||||
});
|
||||
_refresh();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -390,33 +413,67 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
|
||||
void _showEditDialog(String id, num cal) {
|
||||
final ctrl = TextEditingController(text: '$cal');
|
||||
showDialog(
|
||||
var saving = false;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('修改热量'),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: '热量(千卡)'),
|
||||
barrierDismissible: !saving,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: const Text('修改热量'),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
enabled: !saving,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: '热量(千卡)'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: saving ? null : () => Navigator.pop(ctx),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: saving
|
||||
? null
|
||||
: () async {
|
||||
final calories = int.tryParse(ctrl.text.trim());
|
||||
if (calories == null || calories < 0) {
|
||||
AppToast.show(
|
||||
context,
|
||||
'请输入正确的热量',
|
||||
type: AppToastType.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setDialogState(() => saving = true);
|
||||
try {
|
||||
await ref.read(dietServiceProvider).updateRecord(id, {
|
||||
'totalCalories': calories,
|
||||
});
|
||||
if (!ctx.mounted) return;
|
||||
Navigator.pop(ctx);
|
||||
await _refresh();
|
||||
} catch (_) {
|
||||
if (!ctx.mounted) return;
|
||||
setDialogState(() => saving = false);
|
||||
AppToast.show(
|
||||
context,
|
||||
'保存失败,请检查网络后重试',
|
||||
type: AppToastType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: saving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(ctx);
|
||||
await ref.read(dietServiceProvider).updateRecord(id, {
|
||||
'totalCalories': int.tryParse(ctrl.text) ?? 0,
|
||||
});
|
||||
_refresh();
|
||||
},
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
).whenComplete(ctrl.dispose);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2546,7 +2603,7 @@ class _FollowUpItem extends StatelessWidget {
|
||||
|
||||
String _formatDateTime(String? iso) {
|
||||
if (iso == null) return '';
|
||||
final dt = DateTime.tryParse(iso);
|
||||
final dt = DateTime.tryParse(iso)?.toLocal();
|
||||
if (dt == null) return iso;
|
||||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import '../../widgets/enterprise_widgets.dart';
|
||||
import '../../widgets/app_error_state.dart';
|
||||
import '../../widgets/app_empty_state.dart';
|
||||
import '../../widgets/app_toast.dart';
|
||||
import '../../widgets/authenticated_network_image.dart';
|
||||
|
||||
const _reportPageColor = AppColors.report;
|
||||
const _reportPageSoft = Color(0xFFF0F0FF);
|
||||
@@ -202,6 +203,7 @@ class ReportNotifier extends Notifier<ReportState> {
|
||||
|
||||
@override
|
||||
ReportState build() {
|
||||
ref.watch(userSessionIdentityProvider);
|
||||
ref.onDispose(() => _pollTimer?.cancel());
|
||||
Future.microtask(() => loadReports());
|
||||
return ReportState();
|
||||
@@ -237,7 +239,7 @@ class ReportNotifier extends Notifier<ReportState> {
|
||||
title: title,
|
||||
type: m['fileType']?.toString() ?? 'Image',
|
||||
uploadedAt:
|
||||
DateTime.tryParse(m['createdAt']?.toString() ?? '') ??
|
||||
DateTime.tryParse(m['createdAt']?.toString() ?? '')?.toLocal() ??
|
||||
DateTime.now(),
|
||||
fileUrl: m['fileUrl']?.toString(),
|
||||
hasAnalysis: m['aiSummary'] != null,
|
||||
@@ -249,7 +251,9 @@ class ReportNotifier extends Notifier<ReportState> {
|
||||
doctorComment: m['doctorComment']?.toString(),
|
||||
doctorRecommendation: m['doctorRecommendation']?.toString(),
|
||||
doctorName: m['doctorName']?.toString(),
|
||||
reviewedAt: DateTime.tryParse(m['reviewedAt']?.toString() ?? ''),
|
||||
reviewedAt: DateTime.tryParse(
|
||||
m['reviewedAt']?.toString() ?? '',
|
||||
)?.toLocal(),
|
||||
);
|
||||
}).toList();
|
||||
state = state.copyWith(
|
||||
@@ -1053,8 +1057,8 @@ class _ReportOriginalPageState extends ConsumerState<ReportOriginalPage> {
|
||||
child: InteractiveViewer(
|
||||
minScale: 0.7,
|
||||
maxScale: 4,
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
child: AuthenticatedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
key: ValueKey('$imageUrl-$_reloadToken'),
|
||||
fit: BoxFit.contain,
|
||||
loadingBuilder: (context, child, progress) {
|
||||
|
||||
@@ -21,7 +21,8 @@ final notificationPrefsProvider =
|
||||
class NotificationPrefsNotifier extends Notifier<NotificationPrefsViewState> {
|
||||
@override
|
||||
NotificationPrefsViewState build() {
|
||||
Future.microtask(load);
|
||||
final session = ref.watch(userSessionIdentityProvider);
|
||||
if (session != null) Future.microtask(load);
|
||||
return const NotificationPrefsViewState(loading: true);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../core/app_design_tokens.dart';
|
||||
import '../../core/app_theme.dart';
|
||||
import '../../core/navigation_provider.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../providers/omron_device_provider.dart';
|
||||
|
||||
class SettingsPage extends ConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
@@ -172,6 +173,7 @@ class SettingsPage extends ConsumerWidget {
|
||||
);
|
||||
if (ok == true) {
|
||||
await ref.read(apiClientProvider).delete('/api/user/account');
|
||||
await ref.read(omronDeviceProvider.notifier).clearCurrentAccountData();
|
||||
await ref.read(authProvider.notifier).logout();
|
||||
goRoute(ref, 'login');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user