refactor user login:

1. opt request json type.
2. desktop and mobile use same loginDialog.
3. opt loginDialog UI style.
4. opt login request Exception catch.
This commit is contained in:
csf 2023-01-06 19:26:19 +09:00
parent 1867502ef7
commit 3e357159f3
9 changed files with 363 additions and 321 deletions

View File

@ -1367,7 +1367,7 @@ connect(BuildContext context, String id,
} }
} }
Future<Map<String, String>> getHttpHeaders() async { Map<String, String> getHttpHeaders() {
return { return {
'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
}; };

View File

@ -1,12 +1,22 @@
import 'package:flutter_hbb/models/peer_model.dart'; 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 { class UserPayload {
String name = ''; String name = '';
String email = ''; String email = '';
String note = ''; String note = '';
int? status; int? status;
String grp = ''; String grp = '';
bool is_admin = false; bool isAdmin = false;
UserPayload.fromJson(Map<String, dynamic> json) UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '', : name = json['name'] ?? '',
@ -14,7 +24,7 @@ class UserPayload {
note = json['note'] ?? '', note = json['note'] ?? '',
status = json['status'], status = json['status'],
grp = json['grp'] ?? '', grp = json['grp'] ?? '',
is_admin = json['is_admin'] == true; isAdmin = json['is_admin'] == true;
} }
class PeerPayload { class PeerPayload {
@ -37,3 +47,81 @@ class PeerPayload {
return Peer.fromJson({"id": p.id}); 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<String, dynamic> 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<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
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<String, dynamic> 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";
}
}

View File

@ -9,8 +9,6 @@ import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
import '../../desktop/pages/desktop_home_page.dart';
import '../../mobile/pages/settings_page.dart';
class AddressBook extends StatefulWidget { class AddressBook extends StatefulWidget {
final EdgeInsets? menuPadding; final EdgeInsets? menuPadding;
@ -41,21 +39,12 @@ class _AddressBookState extends State<AddressBook> {
} }
}); });
handleLogin() {
// TODO refactor login dialog for desktop and mobile
if (isDesktop) {
loginDialog();
} else {
showLogin(gFFI.dialogManager);
}
}
Future<Widget> buildBody(BuildContext context) async { Future<Widget> buildBody(BuildContext context) async {
return Obx(() { return Obx(() {
if (gFFI.userModel.userName.value.isEmpty) { if (gFFI.userModel.userName.value.isEmpty) {
return Center( return Center(
child: InkWell( child: InkWell(
onTap: handleLogin, onTap: loginDialog,
child: Text( child: Text(
translate("Login"), translate("Login"),
style: const TextStyle(decoration: TextDecoration.underline), style: const TextStyle(decoration: TextDecoration.underline),

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
@ -9,18 +10,21 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart'; import '../../common.dart';
final _kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0);
class _IconOP extends StatelessWidget { class _IconOP extends StatelessWidget {
final String icon; final String icon;
final double iconWidth; 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); : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 4.0), margin: margin,
child: SvgPicture.asset( child: SvgPicture.asset(
'assets/$icon.svg', 'assets/$icon.svg',
width: iconWidth, width: iconWidth,
@ -50,33 +54,33 @@ class ButtonOP extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row(children: [ return Row(children: [
Expanded( Container(
child: Container( height: height,
height: height, width: 200,
padding: _kMidButtonPadding, child: Obx(() => ElevatedButton(
child: Obx(() => ElevatedButton( style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( primary: curOP.value.isEmpty || curOP.value == op
primary: curOP.value.isEmpty || curOP.value == op ? primaryColor
? primaryColor : Colors.grey,
: Colors.grey, ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null,
onPressed: child: Row(
curOP.value.isEmpty || curOP.value == op ? onTap : null, children: [
child: Stack(children: [ SizedBox(
Center(child: Text('${translate("Continue with")} $op')), width: 30,
Align( child: _IconOP(
alignment: Alignment.centerLeft, icon: op,
child: SizedBox( iconWidth: iconWidth,
width: 120, margin: EdgeInsets.only(right: 5),
child: _IconOP( )),
icon: op, Expanded(
iconWidth: iconWidth, child: FittedBox(
)), fit: BoxFit.scaleDown,
), child: Center(
]), child: Text('${translate("Continue with")} $op')))),
)), ],
), ))),
) ),
]); ]);
} }
} }
@ -277,22 +281,25 @@ class LoginWidgetOP extends StatelessWidget {
children.removeLast(); children.removeLast();
} }
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Container(
mainAxisSize: MainAxisSize.min, width: 200,
mainAxisAlignment: MainAxisAlignment.spaceAround, child: Column(
children: children, mainAxisSize: MainAxisSize.min,
)); mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children,
)));
} }
} }
class LoginWidgetUserPass extends StatelessWidget { class LoginWidgetUserPass extends StatelessWidget {
final String username; final TextEditingController username;
final String pass; final TextEditingController pass;
final String usernameMsg; final String? usernameMsg;
final String passMsg; final String? passMsg;
final bool isInProgress; final bool isInProgress;
final RxString curOP; final RxString curOP;
final Function(String, String) onLogin; final RxBool autoLogin;
final Function() onLogin;
const LoginWidgetUserPass({ const LoginWidgetUserPass({
Key? key, Key? key,
required this.username, required this.username,
@ -301,129 +308,135 @@ class LoginWidgetUserPass extends StatelessWidget {
required this.passMsg, required this.passMsg,
required this.isInProgress, required this.isInProgress,
required this.curOP, required this.curOP,
required this.autoLogin,
required this.onLogin, required this.onLogin,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var userController = TextEditingController(text: username); return Padding(
var pwdController = TextEditingController(text: pass); padding: EdgeInsets.all(0),
return Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, 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: [ children: [
const SizedBox( Expanded(
height: 8.0, child: TextField(
), decoration: InputDecoration(
Container( labelText: title,
padding: _kMidButtonPadding, border: const OutlineInputBorder(),
child: Row( errorText: errorText),
children: [ controller: controller,
ConstrainedBox( focusNode: focusNode,
constraints: const BoxConstraints(minWidth: 100), autofocus: true,
child: Text( obscureText: obscureText,
'${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(),
),
),
],
), ),
), ),
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 /// common login dialog for desktop
/// call this directly /// call this directly
Future<bool> loginDialog() async { Future<bool?> loginDialog() async {
String username = ''; var username = TextEditingController();
var usernameMsg = ''; var password = TextEditingController();
String pass = '';
var passMsg = ''; String? usernameMsg;
String? passwordMsg;
var isInProgress = false; var isInProgress = false;
var completer = Completer<bool>(); final autoLogin = true.obs;
final RxString curOP = ''.obs; final RxString curOP = ''.obs;
gFFI.dialogManager.show((setState, close) { return gFFI.dialogManager.show<bool>((setState, close) {
cancel() { cancel() {
isInProgress = false; isInProgress = false;
completer.complete(false); close(false);
close();
} }
onLogin(String username0, String pass0) async { onLogin() async {
setState(() { setState(() {
usernameMsg = ''; usernameMsg = null;
passMsg = ''; passwordMsg = null;
isInProgress = true; isInProgress = true;
}); });
cancel() { cancel() {
@ -436,30 +449,44 @@ Future<bool> loginDialog() async {
} }
curOP.value = 'rustdesk'; curOP.value = 'rustdesk';
username = username0; if (username.text.isEmpty) {
pass = pass0;
if (username.isEmpty) {
usernameMsg = translate('Username missed'); usernameMsg = translate('Username missed');
cancel(); cancel();
return; return;
} }
if (pass.isEmpty) { if (password.text.isEmpty) {
passMsg = translate('Password missed'); passwordMsg = translate('Password missed');
cancel(); cancel();
return; return;
} }
try { try {
final resp = await gFFI.userModel.login(username, pass); final resp = await gFFI.userModel.login(LoginRequest(
if (resp.containsKey('error')) { username: username.text,
passMsg = resp['error']; password: password.text,
cancel(); id: await bind.mainGetMyId(),
return; 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, } on RequestException catch (err) {
// token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} passwordMsg = translate(err.cause);
debugPrint('$resp'); debugPrintStack(label: err.toString());
completer.complete(true); cancel();
return;
} catch (err) { } catch (err) {
passwordMsg = "Unknown Error";
debugPrintStack(label: err.toString()); debugPrintStack(label: err.toString());
cancel(); cancel();
return; return;
@ -469,53 +496,50 @@ Future<bool> loginDialog() async {
return CustomAlertDialog( return CustomAlertDialog(
title: Text(translate('Login')), title: Text(translate('Login')),
content: ConstrainedBox( contentBoxConstraints: BoxConstraints(minWidth: 400),
constraints: const BoxConstraints(minWidth: 500), content: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ const SizedBox(
const SizedBox( height: 8.0,
height: 8.0, ),
), LoginWidgetUserPass(
LoginWidgetUserPass( username: username,
username: username, pass: password,
pass: pass, usernameMsg: usernameMsg,
usernameMsg: usernameMsg, passMsg: passwordMsg,
passMsg: passMsg, isInProgress: isInProgress,
isInProgress: isInProgress, curOP: curOP,
curOP: curOP, autoLogin: autoLogin,
onLogin: onLogin, onLogin: onLogin,
), ),
const SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
Center( Center(
child: Text( child: Text(
translate('or'), translate('or'),
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
)), )),
const SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
LoginWidgetOP( LoginWidgetOP(
ops: [ ops: [
ConfigOP(op: 'Github', iconWidth: 20), ConfigOP(op: 'Github', iconWidth: 20),
ConfigOP(op: 'Google', iconWidth: 20), ConfigOP(op: 'Google', iconWidth: 20),
ConfigOP(op: 'Okta', iconWidth: 38), ConfigOP(op: 'Okta', iconWidth: 38),
], ],
curOP: curOP, curOP: curOP,
cbLogin: (String username) { cbLogin: (String username) {
gFFI.userModel.userName.value = username; gFFI.userModel.userName.value = username;
completer.complete(true); close(true);
close(); },
}, ),
), ],
],
),
), ),
actions: [msgBoxButton(translate('Close'), cancel)], actions: [msgBoxButton(translate('Close'), cancel)],
onCancel: cancel, onCancel: cancel,
); );
}); });
return completer.future;
} }

View File

@ -7,10 +7,9 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/address_book.dart';
import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peer_tab_page.dart';
import '../../common/widgets/peers_view.dart';
import '../../consts.dart'; import '../../consts.dart';
import '../../desktop/widgets/login.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import 'home_page.dart'; import 'home_page.dart';
@ -258,7 +257,7 @@ class _WebMenuState extends State<WebMenu> {
} }
if (value == 'login') { if (value == 'login') {
if (gFFI.userModel.userName.value.isEmpty) { if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager); loginDialog();
} else { } else {
gFFI.userModel.logOut(); gFFI.userModel.logOut();
} }

View File

@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
import '../../common.dart'; import '../../common.dart';
import '../../common/widgets/dialog.dart'; import '../../common/widgets/dialog.dart';
import '../../desktop/widgets/login.dart';
import '../../models/model.dart'; import '../../models/model.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
import '../widgets/dialog.dart'; import '../widgets/dialog.dart';
@ -300,7 +301,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
leading: Icon(Icons.person), leading: Icon(Icons.person),
onPressed: (context) { onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) { if (gFFI.userModel.userName.value.isEmpty) {
showLogin(gFFI.dialogManager); loginDialog();
} else { } else {
gFFI.userModel.logOut(); gFFI.userModel.logOut();
} }
@ -482,77 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) {
}, clickMaskDismiss: true, backDismiss: true); }, 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
? <Widget>[CircularProgressIndicator()]
: (error != ""
? <Widget>[
Text(translate(error),
style: TextStyle(color: Colors.red))
]
: <Widget>[])) +
<Widget>[
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 { class ScanButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -27,8 +27,7 @@ class AbModel {
abError.value = ""; abError.value = "";
final api = "${await bind.mainGetApiServer()}/api/ab/get"; final api = "${await bind.mainGetApiServer()}/api/ab/get";
try { try {
final resp = final resp = await http.post(Uri.parse(api), headers: getHttpHeaders());
await http.post(Uri.parse(api), headers: await getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body); Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) { if (json.containsKey('error')) {
@ -102,7 +101,7 @@ class AbModel {
Future<void> pushAb() async { Future<void> pushAb() async {
abLoading.value = true; abLoading.value = true;
final api = "${await bind.mainGetApiServer()}/api/ab"; final api = "${await bind.mainGetApiServer()}/api/ab";
var authHeaders = await getHttpHeaders(); var authHeaders = getHttpHeaders();
authHeaders['Content-Type'] = "application/json"; authHeaders['Content-Type'] = "application/json";
final peersJsonData = peers.map((e) => e.toJson()).toList(); final peersJsonData = peers.map((e) => e.toJson()).toList();
final body = jsonEncode({ final body = jsonEncode({

View File

@ -59,7 +59,7 @@ class GroupModel {
if (gFFI.userModel.isAdmin.isFalse) if (gFFI.userModel.isAdmin.isFalse)
'grp': gFFI.userModel.groupName.value, '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") { if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body); Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) { if (json.containsKey('error')) {
@ -110,7 +110,7 @@ class GroupModel {
'grp': gFFI.userModel.groupName.value, 'grp': gFFI.userModel.groupName.value,
'target_user': username '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") { if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body); Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) { if (json.containsKey('error')) {

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -45,7 +46,9 @@ class UserModel {
if (error != null) { if (error != null) {
throw error; throw error;
} }
await _parseUserInfo(data);
final user = UserPayload.fromJson(data);
await _parseAndUpdateUser(user);
} catch (e) { } catch (e) {
print('Failed to refreshCurrentUser: $e'); print('Failed to refreshCurrentUser: $e');
} finally { } finally {
@ -55,7 +58,6 @@ class UserModel {
Future<void> reset() async { Future<void> reset() async {
await bind.mainSetLocalOption(key: 'access_token', value: ''); await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: '');
await gFFI.abModel.reset(); await gFFI.abModel.reset();
await gFFI.groupModel.reset(); await gFFI.groupModel.reset();
userName.value = ''; userName.value = '';
@ -63,11 +65,10 @@ class UserModel {
statePeerTab.check(); statePeerTab.check();
} }
Future<void> _parseUserInfo(dynamic userinfo) async { Future<void> _parseAndUpdateUser(UserPayload user) async {
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo)); userName.value = user.name;
userName.value = userinfo['name'] ?? ''; groupName.value = user.grp;
groupName.value = userinfo['grp'] ?? ''; isAdmin.value = user.isAdmin;
isAdmin.value = userinfo['is_admin'] == true;
} }
Future<void> _updateOtherModels() async { Future<void> _updateOtherModels() async {
@ -85,7 +86,7 @@ class UserModel {
'id': await bind.mainGetMyId(), 'id': await bind.mainGetMyId(),
'uuid': await bind.mainGetUuid(), 'uuid': await bind.mainGetUuid(),
}, },
headers: await getHttpHeaders()) headers: getHttpHeaders())
.timeout(Duration(seconds: 2)); .timeout(Duration(seconds: 2));
} catch (e) { } catch (e) {
print("request /api/logout failed: err=$e"); print("request /api/logout failed: err=$e");
@ -95,26 +96,38 @@ class UserModel {
} }
} }
Future<Map<String, dynamic>> login(String userName, String pass) async { /// throw [RequestException]
Future<LoginResponse> login(LoginRequest loginRequest) async {
final url = await bind.mainGetApiServer(); 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<String, dynamic> body;
try { try {
final resp = await http.post(Uri.parse('$url/api/login'), body = jsonDecode(resp.body);
headers: {'Content-Type': 'application/json'}, } catch (e) {
body: jsonEncode({ print("jsonDecode resp body failed: ${e.toString()}");
'username': userName, rethrow;
'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();
} }
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;
} }
} }