把项目移至 Flutter 目录下
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maxkey_flutter/maxkey/services/authn.service.dart';
|
||||
import 'package:maxkey_flutter/maxkey/services/image_captcha.service.dart';
|
||||
import 'package:maxkey_flutter/maxkey/services/time_based.service.dart';
|
||||
import 'package:maxkey_flutter/maxkey/services/users.service.dart';
|
||||
import 'package:maxkey_flutter/persistent.dart';
|
||||
import 'package:maxkey_flutter/utils.dart';
|
||||
|
||||
/// 会在需要刷新的时候通知
|
||||
class MaxKey with ChangeNotifier {
|
||||
final _dio = Dio(BaseOptions(
|
||||
baseUrl: MaxKeyPersistent.instance.baseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
));
|
||||
late final authnService = AuthnService(_dio);
|
||||
late final imageCaptchaService = ImageCaptchaService(_dio);
|
||||
late final timeBasedService = TimeBasedService(_dio);
|
||||
late final usersService = UsersService(_dio);
|
||||
|
||||
static MaxKey? _instance;
|
||||
static MaxKey get instance {
|
||||
_instance ??= MaxKey._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
MaxKey._() {
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onResponse: (response, handler) {
|
||||
final cookies = response.headers[HttpHeaders.setCookieHeader];
|
||||
if (cookies != null) {
|
||||
maxKeyCookies = List.from(cookies);
|
||||
}
|
||||
handler.next(response);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
LOGGER.e(error.type);
|
||||
LOGGER.e(error.response);
|
||||
if (error.type == DioExceptionType.badResponse) {
|
||||
SCAFFOLD_MESSENGER_KEY.currentState?.showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text("Please login again."),
|
||||
action: SnackBarAction(
|
||||
label: "Login",
|
||||
onPressed: () async {
|
||||
_dio.options.headers.remove(HttpHeaders.authorizationHeader);
|
||||
await MaxKeyPersistent.instance.clearToken();
|
||||
NAVIGATOR_KEY.currentContext?.pushReplacement(
|
||||
RoutePath.loginPage,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
void updateBaseUrl() {
|
||||
LOGGER.i("old baseUrl: ${_dio.options.baseUrl}");
|
||||
_dio.options.baseUrl = MaxKeyPersistent.instance.baseUrl;
|
||||
LOGGER.i("new baseUrl: ${_dio.options.baseUrl}");
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<String> maxKeyCookies = [];
|
||||
|
||||
Future<bool> maxKeyNetworkTest({String? host}) async {
|
||||
try {
|
||||
LOGGER.i(
|
||||
"[MaxKeyNetworkTest] GET: ${host == null ? MaxKeyPersistent.instance.baseUrl : "http://$host/sign"}",
|
||||
);
|
||||
|
||||
await _dio.get(
|
||||
host == null ? MaxKeyPersistent.instance.baseUrl : "http://$host/sign",
|
||||
);
|
||||
LOGGER.i("MaxKeyNetworkTest: true");
|
||||
return true;
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
LOGGER.i("MaxKeyNetworkTest: false");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:maxkey_flutter/persistent.dart';
|
||||
import 'package:maxkey_flutter/utils.dart';
|
||||
|
||||
typedef ExpectedErrorHandler = void Function(String msg);
|
||||
|
||||
class AuthnService {
|
||||
final Dio _dio;
|
||||
|
||||
AuthnService(this._dio);
|
||||
|
||||
/// 获取 state 类型
|
||||
Future<String?> get() async {
|
||||
try {
|
||||
LOGGER.i("GET: /login/get?_allow_anonymous=true");
|
||||
|
||||
final res = await _dio.get(
|
||||
"/login/get?_allow_anonymous=true",
|
||||
queryParameters: {"remember_me": true},
|
||||
);
|
||||
return res.data["data"]["state"];
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 短信验证码
|
||||
Future<bool> produceOtp({
|
||||
required ExpectedErrorHandler expectedErrorHandler,
|
||||
required String mobile,
|
||||
}) async {
|
||||
try {
|
||||
LOGGER.i("GET: /login/sendotp/$mobile?_allow_anonymous=true");
|
||||
|
||||
final res = await _dio.get(
|
||||
"/login/sendotp/$mobile?_allow_anonymous=true",
|
||||
queryParameters: {"mobile": mobile},
|
||||
);
|
||||
|
||||
if (res.data["code"] != 0) {
|
||||
final msg = res.data["message"] ?? "验证码发送失败";
|
||||
expectedErrorHandler(msg);
|
||||
LOGGER.w(msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 账密登录
|
||||
Future<bool> loginNormal({
|
||||
required ExpectedErrorHandler expectedErrorHandler,
|
||||
required String state,
|
||||
required String username,
|
||||
required String password,
|
||||
required String captcha,
|
||||
}) async {
|
||||
try {
|
||||
LOGGER.i("POST: /login/signin?_allow_anonymous=true");
|
||||
|
||||
final res = await _dio.post(
|
||||
"/login/signin?_allow_anonymous=true",
|
||||
data: {
|
||||
"authType": "app",
|
||||
"state": state,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"captcha": captcha,
|
||||
"remeberMe": true,
|
||||
},
|
||||
);
|
||||
|
||||
if (res.data["code"] != 0) {
|
||||
final msg = res.data["message"] ?? "登陆失败";
|
||||
expectedErrorHandler(msg);
|
||||
LOGGER.w(msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
await onlineAuth(
|
||||
token: res.data["data"]["token"],
|
||||
onlineTicket: res.data["data"]["ticket"],
|
||||
username: res.data["data"]["username"],
|
||||
);
|
||||
return true;
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> scanCode({
|
||||
required ExpectedErrorHandler expectedErrorHandler,
|
||||
required String code,
|
||||
}) async {
|
||||
final token = MaxKeyPersistent.instance.token;
|
||||
if (token == null) {
|
||||
expectedErrorHandler("未登录");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.i("POST: /login/scanCode");
|
||||
final res = await _dio.post(
|
||||
"/login/scanCode",
|
||||
data: {"jwtToken": token, "code": code},
|
||||
);
|
||||
|
||||
if (res.data["code"] != 0) {
|
||||
final msg = res.data["message"] ?? "扫码登陆失败";
|
||||
expectedErrorHandler(msg);
|
||||
LOGGER.w(msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> onlineAuth({
|
||||
required String token,
|
||||
required String onlineTicket,
|
||||
required String username,
|
||||
}) async {
|
||||
await MaxKeyPersistent.instance.setUser(username);
|
||||
await MaxKeyPersistent.instance.setToken(token);
|
||||
_dio.options.headers[HttpHeaders.authorizationHeader] = token;
|
||||
}
|
||||
|
||||
bool localAuth() {
|
||||
final token = MaxKeyPersistent.instance.token;
|
||||
if (token == null) return false;
|
||||
|
||||
_dio.options.headers[HttpHeaders.authorizationHeader] = token;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 登出并清除本地缓存的 token
|
||||
Future<void> logout() async {
|
||||
try {
|
||||
LOGGER.i("GET: /logout");
|
||||
await _dio.get("/logout");
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
|
||||
_dio.options.headers.remove(HttpHeaders.authorizationHeader);
|
||||
|
||||
await MaxKeyPersistent.instance.clearToken();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:maxkey_flutter/utils.dart';
|
||||
|
||||
class ImageCaptchaService {
|
||||
final Dio _dio;
|
||||
ImageCaptchaService(this._dio);
|
||||
|
||||
Future<Uint8List?> captcha({required String state}) async {
|
||||
try {
|
||||
LOGGER.i("GET: /captcha?_allow_anonymous=true");
|
||||
|
||||
final res = await _dio.get(
|
||||
"/captcha?_allow_anonymous=true",
|
||||
queryParameters: {"state": state},
|
||||
);
|
||||
|
||||
final String base64Image = res.data["data"]["image"];
|
||||
return base64.decode(base64Image.split(",")[1]);
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:maxkey_flutter/utils.dart';
|
||||
|
||||
class TimeBasedService {
|
||||
final Dio _dio;
|
||||
TimeBasedService(this._dio);
|
||||
|
||||
Future<bool> verify({required String totpCode}) async {
|
||||
try {
|
||||
LOGGER.i("GET: /config/verify?otp=$totpCode");
|
||||
|
||||
final res = await _dio.get(
|
||||
"/config/verify",
|
||||
queryParameters: {"otp": totpCode},
|
||||
);
|
||||
|
||||
if (res.data["code"] != 0) {
|
||||
LOGGER.w("验证失败:${res.data["message"]}");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.i("验证成功");
|
||||
return true;
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:maxkey_flutter/utils.dart';
|
||||
|
||||
class MaxKeyUser {
|
||||
final String displayName;
|
||||
final Uint8List? picture;
|
||||
|
||||
MaxKeyUser(this.displayName, this.picture);
|
||||
|
||||
factory MaxKeyUser.fromMap(Map map) {
|
||||
final String displayName = map["data"]["displayName"];
|
||||
final String? pictureBase64 = map["data"]["pictureBase64"];
|
||||
final picture = pictureBase64?.split(",")[1].base64ToBuf();
|
||||
return MaxKeyUser(displayName, picture);
|
||||
}
|
||||
}
|
||||
|
||||
class MaxKeyUserInfo {
|
||||
MaxKeyUserInfo({
|
||||
/// 姓名
|
||||
required this.displayName,
|
||||
|
||||
/// 登陆账号
|
||||
required this.username,
|
||||
|
||||
/// 性别
|
||||
required this.gender,
|
||||
|
||||
/// 员工编号
|
||||
required this.employeeNumber,
|
||||
|
||||
/// 手机号码
|
||||
required this.mobile,
|
||||
|
||||
/// 邮箱
|
||||
required this.email,
|
||||
|
||||
/// 用户类型
|
||||
required this.userType,
|
||||
|
||||
/// 用户状态
|
||||
required this.userState,
|
||||
|
||||
/// 状态
|
||||
required this.status,
|
||||
|
||||
/// 证件类型
|
||||
required this.idType,
|
||||
|
||||
/// 证件号码
|
||||
required this.idCardNo,
|
||||
|
||||
/// 婚姻状态
|
||||
required this.married,
|
||||
|
||||
/// 出生日期
|
||||
required this.birthDate,
|
||||
|
||||
/// 所属组织
|
||||
required this.organization,
|
||||
|
||||
/// 分支机构
|
||||
required this.division,
|
||||
|
||||
/// 部门编号
|
||||
required this.departmentId,
|
||||
|
||||
/// 部门名称
|
||||
required this.department,
|
||||
|
||||
/// 职位
|
||||
required this.jobTitle,
|
||||
|
||||
/// 级别
|
||||
required this.jobLevel,
|
||||
|
||||
/// 上级经理
|
||||
required this.manager,
|
||||
});
|
||||
|
||||
static MaxKeyUserInfo? fromMap(Map map) {
|
||||
return MaxKeyUserInfo(
|
||||
displayName: map["displayName"].toString(),
|
||||
username: map["username"].toString(),
|
||||
gender: switch (map["gender"]) { 1 => "女", 2 => "男", _ => _unknwon },
|
||||
employeeNumber: map["employeeNumber"]?.toString() ?? _unknwon,
|
||||
mobile: map["mobile"]?.toString() ?? _unknwon,
|
||||
email: map["email"]?.toString() ?? _unknwon,
|
||||
userType: switch (map["userType"]) {
|
||||
"EMPLOYEE" => "内部员工",
|
||||
"CONTRACTOR" => "承包商",
|
||||
"CUSTOMER" => "客户",
|
||||
"SUPPLIER" => "供应商",
|
||||
"PARTNER" => "合作伙伴",
|
||||
"EXTERNAL" => "外部用户",
|
||||
"INTERN" => "实习生",
|
||||
"TEMP" => "临时用户",
|
||||
"DEALER" => "经销商",
|
||||
_ => _unknwon,
|
||||
},
|
||||
userState: switch (map["userState"]) {
|
||||
"RESIDENT" => "在职",
|
||||
"WITHDRAWN" => "离职",
|
||||
"INACTIVE" => "停薪留职",
|
||||
"RETIREE" => "退休",
|
||||
_ => _unknwon,
|
||||
},
|
||||
status: switch (map["status"]) {
|
||||
1 => "活动",
|
||||
2 => "不活动",
|
||||
4 => "禁用",
|
||||
5 => "锁定",
|
||||
9 => "已删除",
|
||||
_ => _unknwon,
|
||||
},
|
||||
idType: switch (map["idType"]) {
|
||||
0 => "未知",
|
||||
1 => "身份证",
|
||||
2 => "护照",
|
||||
3 => "学生证",
|
||||
4 => "军人证",
|
||||
_ => _unknwon,
|
||||
},
|
||||
idCardNo: map["idCardNo"]?.toString() ?? _unknwon,
|
||||
married: switch (map["married"]) {
|
||||
0 => "未知",
|
||||
1 => "单身",
|
||||
2 => "已婚",
|
||||
3 => "离异",
|
||||
4 => "丧偶",
|
||||
_ => _unknwon,
|
||||
},
|
||||
birthDate: map["birthDate"]?.toString() ?? _unknwon,
|
||||
organization: map["organization"]?.toString() ?? _unknwon,
|
||||
division: map["division"]?.toString() ?? _unknwon,
|
||||
departmentId: map["departmentId"]?.toString() ?? _unknwon,
|
||||
department: map["deparment"]?.toString() ?? _unknwon,
|
||||
jobTitle: map["jobTitle"]?.toString() ?? _unknwon,
|
||||
jobLevel: map["jobLevel"]?.toString() ?? _unknwon,
|
||||
manager: map["manager"]?.toString() ?? _unknwon,
|
||||
);
|
||||
}
|
||||
|
||||
/// 姓名
|
||||
final String displayName;
|
||||
|
||||
/// 登陆账号
|
||||
final String username;
|
||||
|
||||
/// 性别
|
||||
final String gender;
|
||||
|
||||
/// 员工编号
|
||||
final String employeeNumber;
|
||||
|
||||
/// 手机号码
|
||||
final String mobile;
|
||||
|
||||
/// 邮箱
|
||||
final String email;
|
||||
|
||||
/// 用户类型
|
||||
final String userType;
|
||||
|
||||
/// 用户状态
|
||||
final String userState;
|
||||
|
||||
/// 状态
|
||||
final String status;
|
||||
|
||||
/// 证件类型
|
||||
final String idType;
|
||||
|
||||
/// 证件号码
|
||||
final String idCardNo;
|
||||
|
||||
/// 婚姻状态
|
||||
final String married;
|
||||
|
||||
/// 出生日期
|
||||
final String birthDate;
|
||||
|
||||
/// 所属组织
|
||||
final String organization;
|
||||
|
||||
/// 分支机构
|
||||
final String division;
|
||||
|
||||
/// 部门编号
|
||||
final String departmentId;
|
||||
|
||||
/// 部门名称
|
||||
final String department;
|
||||
|
||||
/// 职位
|
||||
final String jobTitle;
|
||||
|
||||
/// 级别
|
||||
final String jobLevel;
|
||||
|
||||
/// 上级经理
|
||||
final String manager;
|
||||
|
||||
static const String _unknwon = "未知";
|
||||
}
|
||||
|
||||
class UsersService {
|
||||
final Dio _dio;
|
||||
UsersService(this._dio);
|
||||
|
||||
Future<MaxKeyUser?> getBasicUserInfo() async {
|
||||
try {
|
||||
LOGGER.i("GET: /users/profile/get");
|
||||
|
||||
final res = await _dio.get("/users/profile/get");
|
||||
if (res.data["code"] != 0) {
|
||||
LOGGER.w(res.data["message"]);
|
||||
return null;
|
||||
}
|
||||
return MaxKeyUser.fromMap(res.data);
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<MaxKeyUserInfo?> getFullUserInfo() async {
|
||||
try {
|
||||
LOGGER.i("GET: /users/profile/get");
|
||||
|
||||
final res = await _dio.get("/users/profile/get");
|
||||
if (res.data["code"] != 0) {
|
||||
LOGGER.w(res.data["message"]);
|
||||
return null;
|
||||
}
|
||||
|
||||
return MaxKeyUserInfo.fromMap(res.data["data"]);
|
||||
} catch (err) {
|
||||
LOGGER.e(err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user