feat: user login/logout with UserModel

Signed-off-by: Kingtous <kingtous@qq.com>
This commit is contained in:
Kingtous 2022-07-27 14:29:47 +08:00
parent 98a01aefa6
commit 06cb05f796
7 changed files with 424 additions and 144 deletions

View File

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:contextmenu/contextmenu.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@ -588,7 +589,13 @@ class _ConnectionPageState extends State<ConnectionPage> {
svcIsUsingPublicServer.value = await gFFI.bind.mainIsUsingPublicServer();
}
handleLogin() {}
handleLogin() {
loginDialog().then((success) {
if (success) {
setState(() {});
}
});
}
Future<Widget> buildAddressBook(BuildContext context) async {
final token = await gFFI.getLocalOption('access_token');
@ -975,27 +982,6 @@ class _ConnectionPageState extends State<ConnectionPage> {
}
}
class AddressBookPage extends StatefulWidget {
const AddressBookPage({Key? key}) : super(key: key);
@override
State<AddressBookPage> createState() => _AddressBookPageState();
}
class _AddressBookPageState extends State<AddressBookPage> {
@override
void initState() {
// TODO: implement initState
final ab = gFFI.abModel.getAb();
super.initState();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
class WebMenu extends StatefulWidget {
@override
_WebMenuState createState() => _WebMenuState();

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
@ -110,9 +111,46 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.w500),
),
PopupMenuButton(
padding: EdgeInsets.all(4.0),
itemBuilder: (context) => [
FutureBuilder<Widget>(
future: buildPopupMenu(context),
builder: (context, snapshot) {
if (snapshot.hasError) {
print("${snapshot.error}");
}
if (snapshot.hasData) {
return snapshot.data!;
} else {
return Offstage();
}
})
],
),
TextFormField(
controller: model.serverId,
decoration: InputDecoration(
enabled: false,
),
),
],
),
),
),
],
),
);
}
Future<Widget> buildPopupMenu(BuildContext context) async {
var position;
return GestureDetector(
onTapDown: (detail) {
final x = detail.globalPosition.dx;
final y = detail.globalPosition.dy;
position = RelativeRect.fromLTRB(x, y, x, y);
},
onTap: () async {
final userName = await gFFI.userModel.getUserName();
var menu = [
genEnablePopupMenuItem(
translate("Enable Keyboard/Mouse"),
'enable-keyboard',
@ -130,7 +168,6 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
'enable-tunnel',
),
genAudioInputPopupMenuItem(),
// TODO: Audio Input
PopupMenuItem(
child: Text(translate("ID/Relay Server")),
value: 'custom-server',
@ -157,6 +194,15 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
translate("Start ID/relay service"),
'stop-rendezvous-service',
),
userName.isEmpty
? PopupMenuItem(
child: Text(translate("Login")),
value: 'login',
)
: PopupMenuItem(
child: Text("${translate("Logout")} $userName"),
value: 'logout',
),
PopupMenuItem(
child: Text(translate("Change ID")),
value: 'change-id',
@ -169,24 +215,14 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
child: Text(translate("About")),
value: 'about',
),
],
onSelected: onSelectMenu,
)
],
),
TextFormField(
controller: model.serverId,
decoration: InputDecoration(
enabled: false,
),
),
],
),
),
),
],
),
);
];
final v =
await showMenu(context: context, position: position, items: menu);
if (v != null) {
onSelectMenu(v);
}
},
child: Icon(Icons.more_vert_outlined));
}
buildPasswordBoard(BuildContext context) {
@ -323,6 +359,10 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
changeSocks5Proxy();
} else if (value == "about") {
about();
} else if (value == "logout") {
logOut();
} else if (value == "login") {
login();
}
}
@ -832,8 +872,7 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText:
proxyMsg.isNotEmpty ? proxyMsg : null),
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
controller: TextEditingController(text: proxy),
),
),
@ -941,9 +980,7 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
final appName = await gFFI.bind.mainGetAppName();
final license = await gFFI.bind.mainGetLicense();
final version = await gFFI.bind.mainGetVersion();
final linkStyle = TextStyle(
decoration: TextDecoration.underline
);
final linkStyle = TextStyle(decoration: TextDecoration.underline);
DialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text("About $appName"),
@ -960,16 +997,20 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
onTap: () {
launchUrlString("https://rustdesk.com/privacy");
},
child: Text("Privacy Statement", style: linkStyle,).marginSymmetric(vertical: 4.0)),
child: Text(
"Privacy Statement",
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
InkWell(
onTap: () {
launchUrlString("https://rustdesk.com");
}
,child: Text("Website",style: linkStyle,).marginSymmetric(vertical: 4.0)),
},
child: Text(
"Website",
style: linkStyle,
).marginSymmetric(vertical: 4.0)),
Container(
decoration: BoxDecoration(
color: Color(0xFF2c8cff)
),
decoration: BoxDecoration(color: Color(0xFF2c8cff)),
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8),
child: Row(
children: [
@ -977,13 +1018,16 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Copyright &copy; 2022 Purslane Ltd.\n$license", style: TextStyle(
color: Colors.white
),),
Text("Made with heart in this chaotic world!", style: TextStyle(
Text(
"Copyright &copy; 2022 Purslane Ltd.\n$license",
style: TextStyle(color: Colors.white),
),
Text(
"Made with heart in this chaotic world!",
style: TextStyle(
fontWeight: FontWeight.w800,
color: Colors.white
),)
color: Colors.white),
)
],
),
),
@ -1003,4 +1047,151 @@ class _DesktopHomePageState extends State<DesktopHomePage> with TrayListener {
);
});
}
void login() {
loginDialog().then((success) {
if (success) {
// refresh frame
setState(() {});
}
});
}
void logOut() {
gFFI.userModel.logOut().then((_) => {setState(() {})});
}
}
/// common login dialog for desktop
/// call this directly
Future<bool> loginDialog() async {
String userName = "";
var userNameMsg = "";
String pass = "";
var passMsg = "";
var isInProgress = false;
var completer = Completer<bool>();
DialogManager.show((setState, close) {
return CustomAlertDialog(
title: Text(translate("Login")),
content: ConstrainedBox(
constraints: BoxConstraints(minWidth: 500),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 8.0,
),
Row(
children: [
ConstrainedBox(
constraints: BoxConstraints(minWidth: 100),
child: Text(
"${translate('Username')}:",
textAlign: TextAlign.start,
).marginOnly(bottom: 16.0)),
SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
onChanged: (s) {
userName = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText: userNameMsg.isNotEmpty ? userNameMsg : null),
controller: TextEditingController(text: userName),
),
),
],
),
SizedBox(
height: 8.0,
),
Row(
children: [
ConstrainedBox(
constraints: BoxConstraints(minWidth: 100),
child: Text("${translate('Password')}:")
.marginOnly(bottom: 16.0)),
SizedBox(
width: 24.0,
),
Expanded(
child: TextField(
obscureText: true,
onChanged: (s) {
pass = s;
},
decoration: InputDecoration(
border: OutlineInputBorder(),
errorText: passMsg.isNotEmpty ? passMsg : null),
controller: TextEditingController(text: pass),
),
),
],
),
SizedBox(
height: 4.0,
),
Offstage(offstage: !isInProgress, child: LinearProgressIndicator())
],
),
),
actions: [
TextButton(
onPressed: () {
completer.complete(false);
close();
},
child: Text(translate("Cancel"))),
TextButton(
onPressed: () async {
setState(() {
userNameMsg = "";
passMsg = "";
isInProgress = true;
});
final cancel = () {
setState(() {
isInProgress = false;
});
};
userName = userName;
pass = pass;
if (userName.isEmpty) {
userNameMsg = translate("Username missed");
cancel();
return;
}
if (pass.isEmpty) {
passMsg = translate("Password missed");
cancel();
return;
}
try {
final resp = await gFFI.userModel.login(userName, pass);
if (resp.containsKey('error')) {
passMsg = resp['error'];
cancel();
return;
}
// {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);
} catch (err) {
print(err.toString());
cancel();
return;
}
close();
},
child: Text(translate("OK"))),
],
);
});
return completer.future;
}

View File

@ -8,6 +8,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/route_manager.dart';
import 'package:provider/provider.dart';
// import 'package:window_manager/window_manager.dart';
import 'common.dart';
@ -77,6 +78,8 @@ class App extends StatelessWidget {
ChangeNotifierProvider.value(value: gFFI.imageModel),
ChangeNotifierProvider.value(value: gFFI.cursorModel),
ChangeNotifierProvider.value(value: gFFI.canvasModel),
ChangeNotifierProvider.value(value: gFFI.abModel),
ChangeNotifierProvider.value(value: gFFI.userModel),
],
child: GetMaterialApp(
navigatorKey: globalKey,

View File

@ -140,4 +140,10 @@ class AbModel with ChangeNotifier {
return it.first['tags'] ?? [];
}
}
void clear() {
peers.clear();
tags.clear();
notifyListeners();
}
}

View File

@ -12,6 +12,7 @@ import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tuple/tuple.dart';
@ -811,6 +812,7 @@ class FFI {
late final ChatModel chatModel;
late final FileModel fileModel;
late final AbModel abModel;
late final UserModel userModel;
FFI() {
this.imageModel = ImageModel(WeakReference(this));
@ -821,6 +823,7 @@ class FFI {
this.chatModel = ChatModel(WeakReference(this));
this.fileModel = FileModel(WeakReference(this));
this.abModel = AbModel(WeakReference(this));
this.userModel = UserModel(WeakReference(this));
}
static FFI newFFI() {

View File

@ -0,0 +1,83 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'model.dart';
class UserModel extends ChangeNotifier {
var userName = "".obs;
WeakReference<FFI> parent;
UserModel(this.parent);
Future<String> getUserName() async {
if (userName.isNotEmpty) {
return userName.value;
}
final userInfo =
await parent.target?.bind.mainGetLocalOption(key: 'user_info') ?? "{}";
if (userInfo.trim().isEmpty) {
return "";
}
final m = jsonDecode(userInfo);
userName.value = m['name'] ?? '';
return userName.value;
}
Future<void> logOut() async {
debugPrint("start logout");
final bind = parent.target?.bind;
if (bind == null) {
return;
}
final url = await bind.mainGetApiServer();
final _ = await http.post(Uri.parse("$url/api/logout"),
body: {
"id": await bind.mainGetMyId(),
"uuid": await bind.mainGetUuid(),
},
headers: await _getHeaders());
await Future.wait([
bind.mainSetLocalOption(key: 'access_token', value: ''),
bind.mainSetLocalOption(key: 'user_info', value: ''),
bind.mainSetLocalOption(key: 'selected-tags', value: ''),
]);
parent.target?.abModel.clear();
userName.value = "";
notifyListeners();
}
Future<Map<String, String>>? _getHeaders() {
return parent.target?.getHttpHeaders();
}
Future<Map<String, dynamic>> login(String userName, String pass) async {
final bind = parent.target?.bind;
if (bind == null) {
return {"error": "no context"};
}
final url = await bind.mainGetApiServer();
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'] ?? "");
bind.mainSetLocalOption(
key: "user_info", value: jsonEncode(body['user']));
this.userName.value = body['user']?['name'] ?? "";
return body;
} catch (err) {
return {"error": "$err"};
}
}
}

View File

@ -21,10 +21,10 @@ use crate::start_server;
use crate::ui_interface;
use crate::ui_interface::{
change_id, check_connect_status, get_api_server, get_app_name, get_async_job_status,
get_connect_status, get_fav, get_lan_peers, get_license, get_local_option, get_options,
get_peer, get_socks, get_sound_inputs, get_version, has_rendezvous_service, is_ok_change_id,
post_request, set_local_option, set_options, set_socks, store_fav, test_if_valid_server,
using_public_server,
get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options,
get_peer, get_socks, get_sound_inputs, get_uuid, get_version, has_rendezvous_service,
is_ok_change_id, post_request, set_local_option, set_options, set_socks, store_fav,
test_if_valid_server, using_public_server,
};
fn initialize(app_dir: &str) {
@ -488,6 +488,14 @@ pub fn main_set_local_option(key: String, value: String) {
set_local_option(key, value)
}
pub fn main_get_my_id() -> String {
get_id()
}
pub fn main_get_uuid() -> String {
get_uuid()
}
/// FFI for **get** commands which are idempotent.
/// Return result in c string.
///