user_model.dart 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:bot_toast/bot_toast.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_hbb/common/hbbs/hbbs.dart';
  6. import 'package:flutter_hbb/models/ab_model.dart';
  7. import 'package:get/get.dart';
  8. import '../common.dart';
  9. import '../utils/http_service.dart' as http;
  10. import 'model.dart';
  11. import 'platform_model.dart';
  12. bool refreshingUser = false;
  13. class UserModel {
  14. final RxString userName = ''.obs;
  15. final RxBool isAdmin = false.obs;
  16. final RxString networkError = ''.obs;
  17. bool get isLogin => userName.isNotEmpty;
  18. WeakReference<FFI> parent;
  19. UserModel(this.parent) {
  20. userName.listen((p0) {
  21. // When user name becomes empty, show login button
  22. // When user name becomes non-empty:
  23. // For _updateLocalUserInfo, network error will be set later
  24. // For login success, should clear network error
  25. networkError.value = '';
  26. });
  27. }
  28. void refreshCurrentUser() async {
  29. if (bind.isDisableAccount()) return;
  30. networkError.value = '';
  31. final token = bind.mainGetLocalOption(key: 'access_token');
  32. if (token == '') {
  33. await updateOtherModels();
  34. return;
  35. }
  36. _updateLocalUserInfo();
  37. final url = await bind.mainGetApiServer();
  38. final body = {
  39. 'id': await bind.mainGetMyId(),
  40. 'uuid': await bind.mainGetUuid()
  41. };
  42. if (refreshingUser) return;
  43. try {
  44. refreshingUser = true;
  45. final http.Response response;
  46. try {
  47. response = await http.post(Uri.parse('$url/api/currentUser'),
  48. headers: {
  49. 'Content-Type': 'application/json',
  50. 'Authorization': 'Bearer $token'
  51. },
  52. body: json.encode(body));
  53. } catch (e) {
  54. networkError.value = e.toString();
  55. rethrow;
  56. }
  57. refreshingUser = false;
  58. final status = response.statusCode;
  59. if (status == 401 || status == 400) {
  60. reset(resetOther: status == 401);
  61. return;
  62. }
  63. final data = json.decode(utf8.decode(response.bodyBytes));
  64. final error = data['error'];
  65. if (error != null) {
  66. throw error;
  67. }
  68. final user = UserPayload.fromJson(data);
  69. _parseAndUpdateUser(user);
  70. } catch (e) {
  71. debugPrint('Failed to refreshCurrentUser: $e');
  72. } finally {
  73. refreshingUser = false;
  74. await updateOtherModels();
  75. }
  76. }
  77. static Map<String, dynamic>? getLocalUserInfo() {
  78. final userInfo = bind.mainGetLocalOption(key: 'user_info');
  79. if (userInfo == '') {
  80. return null;
  81. }
  82. try {
  83. return json.decode(userInfo);
  84. } catch (e) {
  85. debugPrint('Failed to get local user info "$userInfo": $e');
  86. }
  87. return null;
  88. }
  89. _updateLocalUserInfo() {
  90. final userInfo = getLocalUserInfo();
  91. if (userInfo != null) {
  92. userName.value = userInfo['name'];
  93. }
  94. }
  95. Future<void> reset({bool resetOther = false}) async {
  96. await bind.mainSetLocalOption(key: 'access_token', value: '');
  97. await bind.mainSetLocalOption(key: 'user_info', value: '');
  98. if (resetOther) {
  99. await gFFI.abModel.reset();
  100. await gFFI.groupModel.reset();
  101. }
  102. userName.value = '';
  103. }
  104. _parseAndUpdateUser(UserPayload user) {
  105. userName.value = user.name;
  106. isAdmin.value = user.isAdmin;
  107. bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
  108. if (isWeb) {
  109. // ugly here, tmp solution
  110. bind.mainSetLocalOption(key: 'verifier', value: user.verifier ?? '');
  111. }
  112. }
  113. // update ab and group status
  114. static Future<void> updateOtherModels() async {
  115. await Future.wait([
  116. gFFI.abModel.pullAb(force: ForcePullAb.listAndCurrent, quiet: false),
  117. gFFI.groupModel.pull()
  118. ]);
  119. }
  120. Future<void> logOut({String? apiServer}) async {
  121. final tag = gFFI.dialogManager.showLoading(translate('Waiting'));
  122. try {
  123. final url = apiServer ?? await bind.mainGetApiServer();
  124. final authHeaders = getHttpHeaders();
  125. authHeaders['Content-Type'] = "application/json";
  126. await http
  127. .post(Uri.parse('$url/api/logout'),
  128. body: jsonEncode({
  129. 'id': await bind.mainGetMyId(),
  130. 'uuid': await bind.mainGetUuid(),
  131. }),
  132. headers: authHeaders)
  133. .timeout(Duration(seconds: 2));
  134. } catch (e) {
  135. debugPrint("request /api/logout failed: err=$e");
  136. } finally {
  137. await reset(resetOther: true);
  138. gFFI.dialogManager.dismissByTag(tag);
  139. }
  140. }
  141. /// throw [RequestException]
  142. Future<LoginResponse> login(LoginRequest loginRequest) async {
  143. final url = await bind.mainGetApiServer();
  144. final resp = await http.post(Uri.parse('$url/api/login'),
  145. body: jsonEncode(loginRequest.toJson()));
  146. final Map<String, dynamic> body;
  147. try {
  148. body = jsonDecode(utf8.decode(resp.bodyBytes));
  149. } catch (e) {
  150. debugPrint("login: jsonDecode resp body failed: ${e.toString()}");
  151. if (resp.statusCode != 200) {
  152. BotToast.showText(
  153. contentColor: Colors.red, text: 'HTTP ${resp.statusCode}');
  154. }
  155. rethrow;
  156. }
  157. if (resp.statusCode != 200) {
  158. throw RequestException(resp.statusCode, body['error'] ?? '');
  159. }
  160. if (body['error'] != null) {
  161. throw RequestException(0, body['error']);
  162. }
  163. return getLoginResponseFromAuthBody(body);
  164. }
  165. LoginResponse getLoginResponseFromAuthBody(Map<String, dynamic> body) {
  166. final LoginResponse loginResponse;
  167. try {
  168. loginResponse = LoginResponse.fromJson(body);
  169. } catch (e) {
  170. debugPrint("login: jsonDecode LoginResponse failed: ${e.toString()}");
  171. rethrow;
  172. }
  173. final isLogInDone = loginResponse.type == HttpType.kAuthResTypeToken &&
  174. loginResponse.access_token != null;
  175. if (isLogInDone && loginResponse.user != null) {
  176. _parseAndUpdateUser(loginResponse.user!);
  177. }
  178. return loginResponse;
  179. }
  180. static Future<List<dynamic>> queryOidcLoginOptions() async {
  181. try {
  182. final url = await bind.mainGetApiServer();
  183. if (url.trim().isEmpty) return [];
  184. final resp = await http.get(Uri.parse('$url/api/login-options'));
  185. final List<String> ops = [];
  186. for (final item in jsonDecode(resp.body)) {
  187. ops.add(item as String);
  188. }
  189. for (final item in ops) {
  190. if (item.startsWith('common-oidc/')) {
  191. return jsonDecode(item.substring('common-oidc/'.length));
  192. }
  193. }
  194. return ops
  195. .where((item) => item.startsWith('oidc/'))
  196. .map((item) => {'name': item.substring('oidc/'.length)})
  197. .toList();
  198. } catch (e) {
  199. debugPrint(
  200. "queryOidcLoginOptions: jsonDecode resp body failed: ${e.toString()}");
  201. return [];
  202. }
  203. }
  204. }