支持clientid登录

This commit is contained in:
jyuesong
2022-01-19 17:40:32 +08:00
parent fe3e0f1d50
commit 6dbed6cf49
7 changed files with 306 additions and 108 deletions

View File

@@ -12,7 +12,10 @@ import 'package:qinglong_app/module/task/task_bean.dart';
import 'url.dart'; import 'url.dart';
class Api { class Api {
static Future<HttpResponse<LoginBean>> login(String userName, String passWord) async { static Future<HttpResponse<LoginBean>> login(
String userName,
String passWord,
) async {
return await Http.post<LoginBean>( return await Http.post<LoginBean>(
Url.login, Url.login,
{ {
@@ -22,6 +25,19 @@ class Api {
); );
} }
static Future<HttpResponse<LoginBean>> loginByClientId(
String id,
String secret,
) async {
return await Http.post<LoginBean>(
Url.loginByClientId,
{
"client_id": id,
"client_secret": secret,
},
);
}
static Future<HttpResponse<UserBean>> user() async { static Future<HttpResponse<UserBean>> user() async {
return await Http.get<UserBean>( return await Http.get<UserBean>(
Url.user, Url.user,

View File

@@ -1,36 +1,64 @@
import '../../main.dart';
import '../userinfo_viewmodel.dart';
class Url { class Url {
static const login = "/api/user/login"; static get login => "/api/user/login";
static const loginByClientId = "/open/auth/token";
static const user = "/api/user"; static const user = "/api/user";
static const tasks = "/api/crons";
static const runTasks = "/api/crons/run"; static get tasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/user/login" : "/api/crons";
static const stopTasks = "/api/crons/stop";
static const taskDetail = "/api/crons/"; static get runTasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/run" : "/api/crons/run";
static const addTask = "/api/crons";
static const pinTask = "/api/crons/pin"; static get stopTasks => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/stop" : "/api/crons/stop";
static const unpinTask = "/api/crons/unpin";
static const enableTask = "/api/crons/enable"; static get taskDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/" : "/api/crons/";
static const disableTask = "/api/crons/disable";
static const files = "/api/configs/files"; static get addTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons" : "/api/crons";
static const configContent = "/api/configs/";
static const saveFile = "/api/configs/save"; static get pinTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/pin" : "/api/crons/pin";
static const envs = "/api/envs";
static const addEnv = "/api/envs"; static get unpinTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/unpin" : "/api/crons/unpin";
static const delEnv = "/api/envs";
static const disableEnvs = "/api/envs/disable"; static get enableTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/enable" : "/api/crons/enable";
static const enableEnvs = "/api/envs/enable";
static const loginLog = "/api/user/login-log"; static get disableTask => getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/disable" : "/api/crons/disable";
static const taskLog = "/api/logs";
static const taskLogDetail = "/api/logs/"; static get files => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/files" : "/api/configs/files";
static const scripts = "/api/scripts/files";
static const scriptDetail = "/api/scripts/"; static get configContent => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/" : "/api/configs/";
static const dependencies = "/api/dependencies";
static const dependencyReinstall = "/api/dependencies/reinstall"; static get saveFile => getIt<UserInfoViewModel>().useSecretLogined ? "/open/configs/save" : "/api/configs/save";
static get envs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get addEnv => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get delEnv => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs" : "/api/envs";
static get disableEnvs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/disable" : "/api/envs/disable";
static get enableEnvs => getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/enable" : "/api/envs/enable";
static get loginLog => getIt<UserInfoViewModel>().useSecretLogined ? "/open/user/login-log" : "/api/user/login-log";
static get taskLog => getIt<UserInfoViewModel>().useSecretLogined ? "/open/logs" : "/api/logs";
static get taskLogDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/logs/" : "/api/logs/";
static get scripts => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts/files" : "/api/scripts/files";
static get scriptDetail => getIt<UserInfoViewModel>().useSecretLogined ? "/open/scripts/" : "/api/scripts/";
static get dependencies => getIt<UserInfoViewModel>().useSecretLogined ? "/open/dependencies" : "/api/dependencies";
static get dependencyReinstall => getIt<UserInfoViewModel>().useSecretLogined ? "/open/dependencies/reinstall" : "/api/dependencies/reinstall";
static intimeLog(String cronId) { static intimeLog(String cronId) {
return "/api/crons/$cronId/log"; return getIt<UserInfoViewModel>().useSecretLogined ? "/open/crons/$cronId/log" : "/api/crons/$cronId/log";
} }
static envMove(String envId) { static envMove(String envId) {
return "/api/envs/$envId/move"; return getIt<UserInfoViewModel>().useSecretLogined ? "/open/envs/$envId/move" : "/api/envs/$envId/move";
} }
} }

View File

@@ -3,3 +3,4 @@ String spHost = "host";
String spUserName = "username"; String spUserName = "username";
String spPassWord = "password"; String spPassWord = "password";
String spTheme = "dart_mode"; String spTheme = "dart_mode";
String spSecretLogined = "secret_logined";

View File

@@ -7,11 +7,13 @@ class UserInfoViewModel {
String? _host = ""; String? _host = "";
String? _userName; String? _userName;
String? _passWord; String? _passWord;
bool _useSecertLogined = false;
UserInfoViewModel() { UserInfoViewModel() {
String userInfoJson = SpUtil.getString(spUserInfo); String userInfoJson = SpUtil.getString(spUserInfo);
_userName = SpUtil.getString(spUserName); _userName = SpUtil.getString(spUserName);
_passWord = SpUtil.getString(spPassWord); _passWord = SpUtil.getString(spPassWord);
_useSecertLogined = SpUtil.getBool(spSecretLogined, defValue: false);
_host = SpUtil.getString(spHost, defValue: 'http://49.234.59.95:5700'); _host = SpUtil.getString(spHost, defValue: 'http://49.234.59.95:5700');
if (userInfoJson.isNotEmpty) { if (userInfoJson.isNotEmpty) {
_token = userInfoJson; _token = userInfoJson;
@@ -30,6 +32,11 @@ class UserInfoViewModel {
SpUtil.putString(spPassWord, password); SpUtil.putString(spPassWord, password);
} }
void useSecretLogin(bool use) {
_useSecertLogined = use;
SpUtil.putBool(spSecretLogined, _useSecertLogined);
}
void updateHost(String host) { void updateHost(String host) {
_host = host; _host = host;
SpUtil.putString(spHost, host); SpUtil.putString(spHost, host);
@@ -43,6 +50,8 @@ class UserInfoViewModel {
String? get passWord => _passWord; String? get passWord => _passWord;
bool get useSecretLogined => _useSecertLogined;
bool isLogined() { bool isLogined() {
return token != null && token!.isNotEmpty; return token != null && token!.isNotEmpty;
} }

View File

@@ -10,6 +10,7 @@ import 'package:qinglong_app/main.dart';
import 'package:qinglong_app/module/login/user_bean.dart'; import 'package:qinglong_app/module/login/user_bean.dart';
import 'package:qinglong_app/utils/extension.dart'; import 'package:qinglong_app/utils/extension.dart';
import 'package:qinglong_app/utils/utils.dart'; import 'package:qinglong_app/utils/utils.dart';
import 'package:flip_card/flip_card.dart';
import 'login_bean.dart'; import 'login_bean.dart';
@@ -24,6 +25,9 @@ class _LoginPageState extends ConsumerState<LoginPage> {
final TextEditingController _hostController = TextEditingController(text: getIt<UserInfoViewModel>().host); final TextEditingController _hostController = TextEditingController(text: getIt<UserInfoViewModel>().host);
final TextEditingController _userNameController = TextEditingController(); final TextEditingController _userNameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
final TextEditingController _cIdController = TextEditingController();
final TextEditingController _cSecretController = TextEditingController();
GlobalKey<FlipCardState> cardKey = GlobalKey<FlipCardState>();
bool rememberPassword = false; bool rememberPassword = false;
@@ -32,15 +36,32 @@ class _LoginPageState extends ConsumerState<LoginPage> {
super.initState(); super.initState();
if (getIt<UserInfoViewModel>().userName != null && getIt<UserInfoViewModel>().userName!.isNotEmpty) { if (getIt<UserInfoViewModel>().userName != null && getIt<UserInfoViewModel>().userName!.isNotEmpty) {
_userNameController.text = getIt<UserInfoViewModel>().userName!; if (getIt<UserInfoViewModel>().useSecretLogined) {
_cIdController.text = getIt<UserInfoViewModel>().userName!;
} else {
_userNameController.text = getIt<UserInfoViewModel>().userName!;
}
rememberPassword = true; rememberPassword = true;
} else { } else {
rememberPassword = false; rememberPassword = false;
} }
if (getIt<UserInfoViewModel>().passWord != null && getIt<UserInfoViewModel>().passWord!.isNotEmpty) { if (getIt<UserInfoViewModel>().passWord != null && getIt<UserInfoViewModel>().passWord!.isNotEmpty) {
_passwordController.text = getIt<UserInfoViewModel>().passWord!; if (getIt<UserInfoViewModel>().useSecretLogined) {
_cSecretController.text = getIt<UserInfoViewModel>().passWord!;
} else {
_passwordController.text = getIt<UserInfoViewModel>().passWord!;
}
} }
getIt<UserInfoViewModel>().updateToken(""); getIt<UserInfoViewModel>().updateToken("");
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
if (getIt<UserInfoViewModel>().useSecretLogined) {
cardKey.currentState?.toggleCard();
getIt<UserInfoViewModel>().useSecretLogin(!(cardKey.currentState?.isFront ?? true));
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
setState(() {});
});
}
});
} }
@override @override
@@ -128,51 +149,117 @@ class _LoginPageState extends ConsumerState<LoginPage> {
), ),
autofocus: false, autofocus: false,
), ),
const SizedBox( FlipCard(
height: 15, key: cardKey,
), fill: Fill.fillBack,
const Text( // Fill the back side of the card to make in the same size as the front.
"用户名:", direction: FlipDirection.HORIZONTAL,
style: TextStyle( front: Column(
fontSize: 16, mainAxisSize: MainAxisSize.min,
fontWeight: FontWeight.w600, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 15,
),
const Text(
"用户名:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _userNameController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入用户名",
),
autofocus: false,
),
const SizedBox(
height: 15,
),
const Text(
"密码:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入密码",
),
autofocus: false,
),
const SizedBox(
height: 10,
),
],
), ),
), back: Column(
TextField( mainAxisSize: MainAxisSize.min,
onChanged: (_) { mainAxisAlignment: MainAxisAlignment.start,
setState(() {}); crossAxisAlignment: CrossAxisAlignment.start,
}, children: [
controller: _userNameController, const SizedBox(
decoration: const InputDecoration( height: 15,
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), ),
hintText: "请输入用户名", const Text(
"client_id:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _cIdController,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入client_id",
),
autofocus: false,
),
const SizedBox(
height: 15,
),
const Text(
"client_secret:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _cSecretController,
obscureText: true,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入client_secret",
),
autofocus: false,
),
const SizedBox(
height: 10,
),
],
), ),
autofocus: false,
),
const SizedBox(
height: 15,
),
const Text(
"密码:",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
TextField(
onChanged: (_) {
setState(() {});
},
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5),
hintText: "请输入密码",
),
autofocus: false,
),
const SizedBox(
height: 10,
), ),
Row( Row(
children: [ children: [
@@ -184,11 +271,26 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}, },
), ),
const Text( const Text(
"记住密码", "记住密码/client",
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
), ),
), ),
const Spacer(),
GestureDetector(
onTap: () {
cardKey.currentState?.toggleCard();
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
setState(() {});
});
},
child: Text(
(cardKey.currentState?.isFront ?? true) ? "client_id登录" : "用户名密码登录",
style: const TextStyle(
fontSize: 14,
),
),
),
], ],
), ),
const SizedBox( const SizedBox(
@@ -199,31 +301,29 @@ class _LoginPageState extends ConsumerState<LoginPage> {
child: IgnorePointer( child: IgnorePointer(
ignoring: _hostController.text.isEmpty || _userNameController.text.isEmpty || _passwordController.text.isEmpty || isLoading, ignoring: _hostController.text.isEmpty || _userNameController.text.isEmpty || _passwordController.text.isEmpty || isLoading,
child: CupertinoButton( child: CupertinoButton(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 5, vertical: 5,
), ),
color: color: canClickLoginBtn() ? ref.watch(themeProvider).themeColor.buttonBgColor() : Theme.of(context).primaryColor.withOpacity(0.4),
(_hostController.text.isNotEmpty && _userNameController.text.isNotEmpty && _passwordController.text.isNotEmpty && !isLoading) child: isLoading
? ref.watch(themeProvider).themeColor.buttonBgColor() ? const CupertinoActivityIndicator()
: Theme.of(context).primaryColor.withOpacity(0.4), : const Text(
child: isLoading "登 录",
? const CupertinoActivityIndicator() style: TextStyle(
: const Text( fontSize: 16,
"登 录",
style: TextStyle(
fontSize: 16,
),
), ),
onPressed: () { ),
if (rememberPassword) { onPressed: () {
getIt<UserInfoViewModel>().updateUserName(_userNameController.text, _passwordController.text); Http.pushedLoginPage = false;
} Utils.hideKeyBoard(context);
getIt<UserInfoViewModel>().updateHost(_hostController.text);
Http.pushedLoginPage = false; if (cardKey.currentState?.isFront ?? true) {
Utils.hideKeyBoard(context);
getIt<UserInfoViewModel>().updateHost(_hostController.text);
login(_userNameController.text, _passwordController.text); login(_userNameController.text, _passwordController.text);
}), } else {
login(_cIdController.text, _cSecretController.text);
}
},
),
), ),
), ),
], ],
@@ -239,26 +339,44 @@ class _LoginPageState extends ConsumerState<LoginPage> {
bool isLoading = false; bool isLoading = false;
bool loginByUserName() {
return !getIt<UserInfoViewModel>().useSecretLogined;
}
Future<void> login(String userName, String password) async { Future<void> login(String userName, String password) async {
isLoading = true; isLoading = true;
setState(() {}); setState(() {});
HttpResponse<LoginBean> response = await Api.login(userName, password); HttpResponse<LoginBean> response;
if (loginByUserName()) {
response = await Api.login(userName, password);
} else {
response = await Api.loginByClientId(userName, password);
}
if (response.success) { if (response.success) {
getIt<UserInfoViewModel>().updateToken(response.bean?.token ?? ""); getIt<UserInfoViewModel>().updateToken(response.bean?.token ?? "");
getIt<UserInfoViewModel>().useSecretLogin(!loginByUserName());
if (rememberPassword) {
getIt<UserInfoViewModel>().updateUserName(userName, password);
}
HttpResponse<UserBean> userResponse = await Api.user(); if (!loginByUserName()) {
if (userResponse.success) { Navigator.of(context).pushReplacementNamed(Routes.routeHomePage);
if (userResponse.bean != null && userResponse.bean!.twoFactorActivated != null && userResponse.bean!.twoFactorActivated!) { } else {
("你已开启两步验证,App暂不支持").toast(); HttpResponse<UserBean> userResponse = await Api.user();
if (userResponse.success) {
if (userResponse.bean != null && userResponse.bean!.twoFactorActivated != null && userResponse.bean!.twoFactorActivated!) {
("你已开启两步验证,App暂不支持").toast();
isLoading = false;
setState(() {});
} else {
Navigator.of(context).pushReplacementNamed(Routes.routeHomePage);
}
} else {
(response.message ?? "请检查网络情况").toast();
isLoading = false; isLoading = false;
setState(() {}); setState(() {});
} else {
Navigator.of(context).pushReplacementNamed(Routes.routeHomePage);
} }
} else {
(response.message ?? "请检查网络情况").toast();
isLoading = false;
setState(() {});
} }
} else { } else {
(response.message ?? "请检查网络情况").toast(); (response.message ?? "请检查网络情况").toast();
@@ -266,4 +384,15 @@ class _LoginPageState extends ConsumerState<LoginPage> {
setState(() {}); setState(() {});
} }
} }
bool canClickLoginBtn() {
if (isLoading) return false;
if (_hostController.text.isEmpty) return false;
if (!loginByUserName()) {
return _cIdController.text.isNotEmpty && _cSecretController.text.isNotEmpty;
} else {
return _userNameController.text.isNotEmpty && _passwordController.text.isNotEmpty;
}
}
} }

View File

@@ -239,6 +239,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
flip_card:
dependency: "direct main"
description:
name: flip_card
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -450,6 +457,13 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
move_to_background:
dependency: "direct main"
description:
name: move_to_background
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:

View File

@@ -27,6 +27,7 @@ dependencies:
drag_and_drop_lists: ^0.3.2+2 drag_and_drop_lists: ^0.3.2+2
fluttertoast: ^8.0.8 fluttertoast: ^8.0.8
move_to_background: ^1.0.2 move_to_background: ^1.0.2
flip_card: ^0.6.0
dev_dependencies: dev_dependencies:
flutter_native_splash: ^1.3.3 flutter_native_splash: ^1.3.3