diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8d047dd0d..23847ab52 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1367,7 +1367,7 @@ connect(BuildContext context, String id, } } -Future> getHttpHeaders() async { +Map getHttpHeaders() { return { 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' }; diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index 856f88d20..6a80f8269 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -1,12 +1,22 @@ import 'package:flutter_hbb/models/peer_model.dart'; +class HttpType { + static const kAuthReqTypeAccount = "account"; + static const kAuthReqTypeMobile = "mobile"; + static const kAuthReqTypeSMSCode = "sms_code"; + static const kAuthReqTypeEmailCode = "email_code"; + + static const kAuthResTypeToken = "access_token"; + static const kAuthResTypeEmailCheck = "email_check"; +} + class UserPayload { String name = ''; String email = ''; String note = ''; int? status; String grp = ''; - bool is_admin = false; + bool isAdmin = false; UserPayload.fromJson(Map json) : name = json['name'] ?? '', @@ -14,7 +24,7 @@ class UserPayload { note = json['note'] ?? '', status = json['status'], grp = json['grp'] ?? '', - is_admin = json['is_admin'] == true; + isAdmin = json['is_admin'] == true; } class PeerPayload { @@ -37,3 +47,81 @@ class PeerPayload { return Peer.fromJson({"id": p.id}); } } + +class LoginRequest { + String? username; + String? password; + String? id; + String? uuid; + bool? autoLogin; + String? type; + String? verificationCode; + String? deviceInfo; + + LoginRequest( + {this.username, + this.password, + this.id, + this.uuid, + this.autoLogin, + this.type, + this.verificationCode, + this.deviceInfo}); + + LoginRequest.fromJson(Map json) { + username = json['username']; + password = json['password']; + id = json['id']; + uuid = json['uuid']; + autoLogin = json['autoLogin']; + type = json['type']; + verificationCode = json['verificationCode']; + deviceInfo = json['deviceInfo']; + } + + Map toJson() { + final Map data = {}; + data['username'] = username ?? ''; + data['password'] = password ?? ''; + data['id'] = id ?? ''; + data['uuid'] = uuid ?? ''; + data['autoLogin'] = autoLogin ?? ''; + data['type'] = type ?? ''; + data['verificationCode'] = verificationCode ?? ''; + data['deviceInfo'] = deviceInfo ?? ''; + return data; + } +} + +class LoginResponse { + String? access_token; + String? type; + UserPayload? user; + + LoginResponse({this.access_token, this.type, this.user}); + + LoginResponse.fromJson(Map json) { + access_token = json['access_token']; + type = json['type']; + print("user: ${json['user']}"); + print("user id: ${json['user']['id']}"); + print("user name: ${json['user']['name']}"); + print("user email: ${json['user']['id']}"); + print("user note: ${json['user']['note']}"); + print("user status: ${json['user']['status']}"); + print("user grp: ${json['user']['grp']}"); + print("user is_admin: ${json['user']['is_admin']}"); + user = json['user'] != null ? UserPayload.fromJson(json['user']) : null; + } +} + +class RequestException implements Exception { + int statusCode; + String cause; + RequestException(this.statusCode, this.cause); + + @override + String toString() { + return "RequestException, statusCode: $statusCode, error: $cause"; + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index fbeca25b2..3e7f46814 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -9,8 +9,6 @@ import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; import '../../common.dart'; -import '../../desktop/pages/desktop_home_page.dart'; -import '../../mobile/pages/settings_page.dart'; class AddressBook extends StatefulWidget { final EdgeInsets? menuPadding; @@ -41,21 +39,12 @@ class _AddressBookState extends State { } }); - handleLogin() { - // TODO refactor login dialog for desktop and mobile - if (isDesktop) { - loginDialog(); - } else { - showLogin(gFFI.dialogManager); - } - } - Future buildBody(BuildContext context) async { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( child: InkWell( - onTap: handleLogin, + onTap: loginDialog, child: Text( translate("Login"), style: const TextStyle(decoration: TextDecoration.underline), diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index 62e6ebc53..8092dfed6 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -9,18 +10,21 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; -final _kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0); - class _IconOP extends StatelessWidget { final String icon; final double iconWidth; - const _IconOP({Key? key, required this.icon, required this.iconWidth}) + final EdgeInsets margin; + const _IconOP( + {Key? key, + required this.icon, + required this.iconWidth, + this.margin = const EdgeInsets.symmetric(horizontal: 4.0)}) : super(key: key); @override Widget build(BuildContext context) { return Container( - margin: const EdgeInsets.symmetric(horizontal: 4.0), + margin: margin, child: SvgPicture.asset( 'assets/$icon.svg', width: iconWidth, @@ -50,33 +54,33 @@ class ButtonOP extends StatelessWidget { @override Widget build(BuildContext context) { return Row(children: [ - Expanded( - child: Container( - height: height, - padding: _kMidButtonPadding, - child: Obx(() => ElevatedButton( - style: ElevatedButton.styleFrom( - primary: curOP.value.isEmpty || curOP.value == op - ? primaryColor - : Colors.grey, - ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), - onPressed: - curOP.value.isEmpty || curOP.value == op ? onTap : null, - child: Stack(children: [ - Center(child: Text('${translate("Continue with")} $op')), - Align( - alignment: Alignment.centerLeft, - child: SizedBox( - width: 120, - child: _IconOP( - icon: op, - iconWidth: iconWidth, - )), - ), - ]), - )), - ), - ) + Container( + height: height, + width: 200, + child: Obx(() => ElevatedButton( + style: ElevatedButton.styleFrom( + primary: curOP.value.isEmpty || curOP.value == op + ? primaryColor + : Colors.grey, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null, + child: Row( + children: [ + SizedBox( + width: 30, + child: _IconOP( + icon: op, + iconWidth: iconWidth, + margin: EdgeInsets.only(right: 5), + )), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Center( + child: Text('${translate("Continue with")} $op')))), + ], + ))), + ), ]); } } @@ -277,22 +281,25 @@ class LoginWidgetOP extends StatelessWidget { children.removeLast(); } return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: children, - )); + child: Container( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + ))); } } class LoginWidgetUserPass extends StatelessWidget { - final String username; - final String pass; - final String usernameMsg; - final String passMsg; + final TextEditingController username; + final TextEditingController pass; + final String? usernameMsg; + final String? passMsg; final bool isInProgress; final RxString curOP; - final Function(String, String) onLogin; + final RxBool autoLogin; + final Function() onLogin; const LoginWidgetUserPass({ Key? key, required this.username, @@ -301,129 +308,135 @@ class LoginWidgetUserPass extends StatelessWidget { required this.passMsg, required this.isInProgress, required this.curOP, + required this.autoLogin, required this.onLogin, }) : super(key: key); @override Widget build(BuildContext context) { - var userController = TextEditingController(text: username); - var pwdController = TextEditingController(text: pass); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Padding( + padding: EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8.0), + DialogTextField( + title: '${translate("Username")}:', + controller: username, + autoFocus: true, + errorText: usernameMsg), + DialogTextField( + title: '${translate("Password")}:', + obscureText: true, + controller: pass, + errorText: passMsg), + Obx(() => CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Remember me"), + ), + value: autoLogin.value, + onChanged: (v) { + if (v == null) return; + autoLogin.value = v; + }, + )), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + const SizedBox(height: 12.0), + FittedBox( + child: + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + height: 38, + width: 200, + child: Obx(() => ElevatedButton( + child: Text( + translate('Login'), + style: TextStyle(fontSize: 16), + ), + onPressed: + curOP.value.isEmpty || curOP.value == 'rustdesk' + ? () { + onLogin(); + } + : null, + )), + ), + ])), + ], + )); + } +} + +class DialogTextField extends StatelessWidget { + final String title; + final bool autoFocus; + final bool obscureText; + final String? errorText; + final TextEditingController controller; + final FocusNode focusNode = FocusNode(); + + DialogTextField( + {Key? key, + this.autoFocus = false, + this.obscureText = false, + this.errorText, + required this.title, + required this.controller}) + : super(key: key) { + // todo mobile requestFocus, on mobile, widget will reload every time the text changes + if (autoFocus && isDesktop) { + Timer(Duration(milliseconds: 200), () => focusNode.requestFocus()); + } + } + + @override + Widget build(BuildContext context) { + return Row( children: [ - const SizedBox( - height: 8.0, - ), - Container( - padding: _kMidButtonPadding, - child: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - '${translate("Username")}:', - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: usernameMsg.isNotEmpty ? usernameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: title, + border: const OutlineInputBorder(), + errorText: errorText), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, ), ), - const SizedBox( - height: 8.0, - ), - Container( - padding: _kMidButtonPadding, - child: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, - ), - ), - ], - ), - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()), - const SizedBox( - height: 12.0, - ), - Row(children: [ - Expanded( - child: Container( - height: 38, - padding: _kMidButtonPadding, - child: Obx(() => ElevatedButton( - style: curOP.value.isEmpty || curOP.value == 'rustdesk' - ? null - : ElevatedButton.styleFrom( - primary: Colors.grey, - ), - child: Text( - translate('Login'), - style: TextStyle(fontSize: 16), - ), - onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk' - ? () { - onLogin(userController.text, pwdController.text); - } - : null, - )), - ), - ), - ]), ], - ); + ).paddingSymmetric(vertical: 4.0); } } /// common login dialog for desktop /// call this directly -Future loginDialog() async { - String username = ''; - var usernameMsg = ''; - String pass = ''; - var passMsg = ''; +Future loginDialog() async { + var username = TextEditingController(); + var password = TextEditingController(); + + String? usernameMsg; + String? passwordMsg; var isInProgress = false; - var completer = Completer(); + final autoLogin = true.obs; final RxString curOP = ''.obs; - gFFI.dialogManager.show((setState, close) { + return gFFI.dialogManager.show((setState, close) { cancel() { isInProgress = false; - completer.complete(false); - close(); + close(false); } - onLogin(String username0, String pass0) async { + onLogin() async { setState(() { - usernameMsg = ''; - passMsg = ''; + usernameMsg = null; + passwordMsg = null; isInProgress = true; }); cancel() { @@ -436,30 +449,44 @@ Future loginDialog() async { } curOP.value = 'rustdesk'; - username = username0; - pass = pass0; - if (username.isEmpty) { + if (username.text.isEmpty) { usernameMsg = translate('Username missed'); cancel(); return; } - if (pass.isEmpty) { - passMsg = translate('Password missed'); + if (password.text.isEmpty) { + passwordMsg = translate('Password missed'); cancel(); return; } try { - final resp = await gFFI.userModel.login(username, pass); - if (resp.containsKey('error')) { - passMsg = resp['error']; - cancel(); - return; + final resp = await gFFI.userModel.login(LoginRequest( + username: username.text, + password: password.text, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin.value, + type: HttpType.kAuthReqTypeAccount)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + case HttpType.kAuthResTypeEmailCheck: + break; } - // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, - // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} - debugPrint('$resp'); - completer.complete(true); + } on RequestException catch (err) { + passwordMsg = translate(err.cause); + debugPrintStack(label: err.toString()); + cancel(); + return; } catch (err) { + passwordMsg = "Unknown Error"; debugPrintStack(label: err.toString()); cancel(); return; @@ -469,53 +496,50 @@ Future loginDialog() async { return CustomAlertDialog( title: Text(translate('Login')), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - LoginWidgetUserPass( - username: username, - pass: pass, - usernameMsg: usernameMsg, - passMsg: passMsg, - isInProgress: isInProgress, - curOP: curOP, - onLogin: onLogin, - ), - const SizedBox( - height: 8.0, - ), - Center( - child: Text( - translate('or'), - style: TextStyle(fontSize: 16), - )), - const SizedBox( - height: 8.0, - ), - LoginWidgetOP( - ops: [ - ConfigOP(op: 'Github', iconWidth: 20), - ConfigOP(op: 'Google', iconWidth: 20), - ConfigOP(op: 'Okta', iconWidth: 38), - ], - curOP: curOP, - cbLogin: (String username) { - gFFI.userModel.userName.value = username; - completer.complete(true); - close(); - }, - ), - ], - ), + contentBoxConstraints: BoxConstraints(minWidth: 400), + content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 8.0, + ), + LoginWidgetUserPass( + username: username, + pass: password, + usernameMsg: usernameMsg, + passMsg: passwordMsg, + isInProgress: isInProgress, + curOP: curOP, + autoLogin: autoLogin, + onLogin: onLogin, + ), + const SizedBox( + height: 8.0, + ), + Center( + child: Text( + translate('or'), + style: TextStyle(fontSize: 16), + )), + const SizedBox( + height: 8.0, + ), + LoginWidgetOP( + ops: [ + ConfigOP(op: 'Github', iconWidth: 20), + ConfigOP(op: 'Google', iconWidth: 20), + ConfigOP(op: 'Okta', iconWidth: 38), + ], + curOP: curOP, + cbLogin: (String username) { + gFFI.userModel.userName.value = username; + close(true); + }, + ), + ], ), actions: [msgBoxButton(translate('Close'), cancel)], onCancel: cancel, ); }); - return completer.future; } diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 957910324..b8104387e 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -7,10 +7,9 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; -import '../../common/widgets/address_book.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peers_view.dart'; import '../../consts.dart'; +import '../../desktop/widgets/login.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; @@ -258,7 +257,7 @@ class _WebMenuState extends State { } if (value == 'login') { if (gFFI.userModel.userName.value.isEmpty) { - showLogin(gFFI.dialogManager); + loginDialog(); } else { gFFI.userModel.logOut(); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 9637ecb40..0764f8247 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; +import '../../desktop/widgets/login.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -300,7 +301,7 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { - showLogin(gFFI.dialogManager); + loginDialog(); } else { gFFI.userModel.logOut(); } @@ -482,77 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) { }, clickMaskDismiss: true, backDismiss: true); } -void showLogin(OverlayDialogManager dialogManager) { - final passwordController = TextEditingController(); - final nameController = TextEditingController(); - var loading = false; - var error = ''; - dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate('Login')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - TextField( - autofocus: true, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Username'), - ), - controller: nameController, - ), - PasswordWidget(controller: passwordController, autoFocus: false), - ]), - actions: (loading - ? [CircularProgressIndicator()] - : (error != "" - ? [ - Text(translate(error), - style: TextStyle(color: Colors.red)) - ] - : [])) + - [ - TextButton( - style: flatButtonStyle, - onPressed: loading - ? null - : () { - close(); - setState(() { - loading = false; - }); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: loading - ? null - : () async { - final name = nameController.text.trim(); - final pass = passwordController.text.trim(); - if (name != "" && pass != "") { - setState(() { - loading = true; - }); - final resp = await gFFI.userModel.login(name, pass); - setState(() { - loading = false; - }); - if (resp.containsKey('error')) { - error = resp['error']; - return; - } - } - close(); - }, - child: Text(translate('OK')), - ), - ], - ); - }); -} - class ScanButton extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index d8a0e8f99..175c8424b 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -27,8 +27,7 @@ class AbModel { abError.value = ""; final api = "${await bind.mainGetApiServer()}/api/ab/get"; try { - final resp = - await http.post(Uri.parse(api), headers: await getHttpHeaders()); + final resp = await http.post(Uri.parse(api), headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { @@ -102,7 +101,7 @@ class AbModel { Future pushAb() async { abLoading.value = true; final api = "${await bind.mainGetApiServer()}/api/ab"; - var authHeaders = await getHttpHeaders(); + var authHeaders = getHttpHeaders(); authHeaders['Content-Type'] = "application/json"; final peersJsonData = peers.map((e) => e.toJson()).toList(); final body = jsonEncode({ diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index f220d62f1..4d9fab0e4 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -59,7 +59,7 @@ class GroupModel { if (gFFI.userModel.isAdmin.isFalse) 'grp': gFFI.userModel.groupName.value, }); - final resp = await http.get(uri, headers: await getHttpHeaders()); + final resp = await http.get(uri, headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { @@ -110,7 +110,7 @@ class GroupModel { 'grp': gFFI.userModel.groupName.value, 'target_user': username }); - final resp = await http.get(uri, headers: await getHttpHeaders()); + final resp = await http.get(uri, headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e5d2c9e15..79a9778b0 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -45,7 +46,9 @@ class UserModel { if (error != null) { throw error; } - await _parseUserInfo(data); + + final user = UserPayload.fromJson(data); + await _parseAndUpdateUser(user); } catch (e) { print('Failed to refreshCurrentUser: $e'); } finally { @@ -55,7 +58,6 @@ class UserModel { Future reset() async { await bind.mainSetLocalOption(key: 'access_token', value: ''); - await bind.mainSetLocalOption(key: 'user_info', value: ''); await gFFI.abModel.reset(); await gFFI.groupModel.reset(); userName.value = ''; @@ -63,11 +65,10 @@ class UserModel { statePeerTab.check(); } - Future _parseUserInfo(dynamic userinfo) async { - bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo)); - userName.value = userinfo['name'] ?? ''; - groupName.value = userinfo['grp'] ?? ''; - isAdmin.value = userinfo['is_admin'] == true; + Future _parseAndUpdateUser(UserPayload user) async { + userName.value = user.name; + groupName.value = user.grp; + isAdmin.value = user.isAdmin; } Future _updateOtherModels() async { @@ -85,7 +86,7 @@ class UserModel { 'id': await bind.mainGetMyId(), 'uuid': await bind.mainGetUuid(), }, - headers: await getHttpHeaders()) + headers: getHttpHeaders()) .timeout(Duration(seconds: 2)); } catch (e) { print("request /api/logout failed: err=$e"); @@ -95,26 +96,38 @@ class UserModel { } } - Future> login(String userName, String pass) async { + /// throw [RequestException] + Future login(LoginRequest loginRequest) async { final url = await bind.mainGetApiServer(); + final resp = await http.post(Uri.parse('$url/api/login'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(loginRequest.toJson())); + + final Map body; try { - final resp = await http.post(Uri.parse('$url/api/login'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'username': userName, - 'password': pass, - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid() - })); - final body = jsonDecode(resp.body); - bind.mainSetLocalOption( - key: 'access_token', value: body['access_token'] ?? ''); - await _parseUserInfo(body['user']); - return body; - } catch (err) { - return {'error': '$err'}; - } finally { - await _updateOtherModels(); + body = jsonDecode(resp.body); + } catch (e) { + print("jsonDecode resp body failed: ${e.toString()}"); + rethrow; } + + if (resp.statusCode != 200) { + throw RequestException(resp.statusCode, body['error'] ?? ''); + } + + final LoginResponse loginResponse; + try { + loginResponse = LoginResponse.fromJson(body); + } catch (e) { + print("jsonDecode LoginResponse failed: ${e.toString()}"); + rethrow; + } + + if (loginResponse.user != null) { + await _parseAndUpdateUser(loginResponse.user!); + } + + await _updateOtherModels(); + return loginResponse; } }