feat: add upload logic.

pull/6/head
DebuggerX 3 years ago
parent 317aa006e3
commit 048c54e080

@ -1,10 +1,10 @@
import 'package:angel3_migration/angel3_migration.dart';
import 'package:angel3_orm_postgres/angel3_orm_postgres.dart';
import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:angel3_configuration/angel3_configuration.dart';
import 'package:angel3_migration/angel3_migration.dart';
import 'package:angel3_migration_runner/angel3_migration_runner.dart';
import 'package:angel3_migration_runner/postgres.dart';
import 'package:angel3_orm_postgres/angel3_orm_postgres.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:dde_gesture_manager_api/src/config/plugins/orm.dart';
import 'package:file/local.dart';
import 'package:logging/logging.dart';
@ -28,6 +28,7 @@ void main(List<String> args) async {
var migrationRunner = PostgresMigrationRunner(connection, migrations: [
UserMigration(),
UserSeed(),
SchemeMigration(),
]);
await runMigrations(migrationRunner, args);
}

@ -7,6 +7,7 @@ class Apis {
static final system = SystemApis();
static final auth = AuthApis();
static final scheme = SchemeApis();
}
class AuthApis {
@ -15,6 +16,8 @@ class AuthApis {
String get loginOrSignup => [path, 'login_or_signup'].joinPath();
String confirmSignup({required StringParam accessKey}) => [path, 'confirm_sign_up', accessKey].joinPath();
String get status => [path, 'status'].joinPath();
}
class SystemApis {
@ -23,6 +26,14 @@ class SystemApis {
String get appVersion => [path, 'app-version'].joinPath();
}
class SchemeApis {
static final String path = '/scheme';
String get upload => [path, 'upload'].joinPath();
String get userUploads => [path, 'user', 'uploads'].joinPath();
}
final _paramsMap = {
'IntParam': IntParam.nameOnRoute,
'DoubleParam': DoubleParam.nameOnRoute,

@ -1,3 +1,4 @@
export 'src/models/user.dart';
export 'src/models/app_version.dart';
export 'src/models/login_success.dart';
export 'src/models/scheme.dart';

@ -9,7 +9,7 @@ Future<void> configureServer(Angel app) async {
allowCookie: false,
deserializer: (p) async => (UserQuery()..where!.id.equals(int.parse(p)))
.getOne(app.container!.make<orm.QueryExecutor>())
.then((value) => value.value),
.then((value) => value.isNotEmpty ? value.value : User(email: '')),
serializer: (p) => p.id ?? '',
);
await auth.configureServer(app);

@ -0,0 +1,35 @@
import 'package:angel3_orm/angel3_orm.dart';
import 'package:angel3_serialize/angel3_serialize.dart';
import 'package:dde_gesture_manager_api/src/models/base_model.dart';
import 'package:angel3_migration/angel3_migration.dart';
import 'package:optional/optional.dart';
part 'scheme.g.dart';
@serializable
@orm
abstract class _Scheme extends BaseModel {
@Column(isNullable: false, indexType: IndexType.unique)
@SerializableField(isNullable: false)
String? get uuid;
@Column(isNullable: false)
@SerializableField(isNullable: false)
String? get name;
@Column(isNullable: false, indexType: IndexType.standardIndex)
@SerializableField(isNullable: true, exclude: true)
int? uid;
@Column(type: ColumnType.text)
String? description;
@Column(isNullable: false, indexType: IndexType.standardIndex)
@SerializableField(defaultValue: false, isNullable: false)
bool? get shared;
@Column(type: ColumnType.jsonb)
@SerializableField()
@DefaultsTo([])
List? get gestures;
}

@ -20,5 +20,9 @@ abstract class _User extends BaseModel {
@SerializableField(isNullable: true, exclude: true)
String? get password;
@Column(isNullable: false)
@SerializableField(defaultValue: false)
bool? get blocked;
String secret(String salt) => base64.encode(Hmac(sha256, salt.codeUnits).convert((password ?? '').codeUnits).bytes);
}

@ -5,6 +5,7 @@ import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:dde_gesture_manager_api/apis.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart';
import 'package:mailer/mailer.dart';
import 'package:mailer/smtp_server.dart';
import 'package:uuid/uuid.dart';
@ -48,6 +49,8 @@ Future configureServer(Angel app) async {
return res.notFound();
} else if (user.value.password != userParams.password) {
return res.unauthorized();
} else if (user.value.blocked == true) {
return res.forbidden();
} else {
var angelAuth = req.container!.make<AngelAuth>();
await angelAuth.loginById(user.value.id!, req, res);
@ -75,4 +78,14 @@ Future configureServer(Angel app) async {
}
return res.render('sign_up_result.html', {'success': false});
});
app.get(
Apis.auth.status,
chain(
[
jwtMiddleware(),
(req, res) => req.user.blocked == false ? res.noContent() : res.forbidden(),
],
),
);
}

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_orm/angel3_orm.dart' as orm;
import 'package:dde_gesture_manager_api/models.dart';
import 'package:dde_gesture_manager_api/src/config/plugins/redis_cache.dart';
import 'package:neat_cache/neat_cache.dart';
@ -20,6 +21,16 @@ extension ResponseNoContent on ResponseContext {
statusCode = HttpStatus.unauthorized;
return close();
}
forbidden() {
statusCode = HttpStatus.forbidden;
return close();
}
unProcessableEntity() {
statusCode = HttpStatus.unprocessableEntity;
return close();
}
}
extension QueryWhereId on orm.Query {
@ -32,6 +43,10 @@ extension QueryExecutor on RequestContext {
orm.QueryExecutor get queryExecutor => container!.make<orm.QueryExecutor>();
}
extension RedisExecutor on RequestContext {
extension RedisClient on RequestContext {
Cache get cache => container!.make<RedisCache>().cache;
}
extension JWTUserInstance on RequestContext {
User get user => container!.make<User>();
}

@ -2,16 +2,15 @@ import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:dde_gesture_manager_api/models.dart';
import '../controllers/controller_extensions.dart';
RequestHandler jwtMiddleware() {
return (RequestContext req, ResponseContext res, {bool throwError = true}) async {
bool _reject(ResponseContext res) {
if (throwError) {
res.statusCode = 403;
throw AngelHttpException.forbidden();
} else {
return false;
res.forbidden();
}
return false;
}
if (req.container != null) {

@ -0,0 +1,60 @@
import 'dart:async';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:dde_gesture_manager_api/apis.dart';
import 'package:dde_gesture_manager_api/src/models/scheme.dart';
import 'package:dde_gesture_manager_api/src/routes/controllers/middlewares.dart';
import 'package:logging/logging.dart';
import 'controller_extensions.dart';
Future configureServer(Angel app) async {
final _log = Logger('scheme_controller');
app.post(
Apis.scheme.upload,
chain(
[
jwtMiddleware(),
(req, res) async {
try {
var scheme = SchemeSerializer.fromMap(req.bodyAsMap);
var schemeQuery = SchemeQuery();
schemeQuery.where!.uuid.equals(scheme.uuid!);
var one = await schemeQuery.getOne(req.queryExecutor);
schemeQuery = SchemeQuery();
schemeQuery.values.copyFrom(scheme);
schemeQuery.values.uid = int.parse(req.user.id!);
if (one.isEmpty) {
await schemeQuery.insert(req.queryExecutor);
} else {
schemeQuery.whereId = int.parse(one.value.id!);
await schemeQuery.updateOne(req.queryExecutor);
}
} catch (e) {
_log.severe(e);
return res.unProcessableEntity();
}
return res.noContent();
},
],
),
);
app.get(
Apis.scheme.userUploads,
chain(
[
jwtMiddleware(),
(req, res) async {
var schemeQuery = SchemeQuery();
schemeQuery.where!.uid.equals(int.parse(req.user.id!));
schemeQuery.orderBy(SchemeFields.updatedAt, descending: true);
return schemeQuery.get(req.queryExecutor).then((value) => value.map((e) => {
'name': e.name,
'description': e.description,
}).toList());
},
],
),
);
}

@ -2,6 +2,7 @@ import 'package:angel3_framework/angel3_framework.dart';
import 'package:file/file.dart';
import 'controllers/auth_controllers.dart' as auth_controllers;
import 'controllers/system_controllers.dart' as system_controllers;
import 'controllers/scheme_controllers.dart' as scheme_controllers;
/// Put your app routes here!
///
@ -22,6 +23,7 @@ AngelConfigurer configureServer(FileSystem fileSystem) {
// Typically, you want to mount controllers first, after any global middleware.
await app.configure(system_controllers.configureServerWithFileSystem(fileSystem));
await app.configure(auth_controllers.configureServer);
await app.configure(scheme_controllers.configureServer);
// Throw a 404 if no route matched the request.
app.fallback((req, res) => throw AngelHttpException.notFound());

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dde_gesture_manager/constants/sp_keys.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'package:dde_gesture_manager/models/scheme.dart' as AppScheme;
import 'package:dde_gesture_manager/utils/helper.dart';
import 'package:dde_gesture_manager/utils/notificator.dart';
import 'package:dde_gesture_manager_api/apis.dart';
@ -13,7 +14,12 @@ typedef T BeanBuilder<T>(Map res);
typedef T HandleRespBuild<T>(http.Response resp);
getStatusCodeFunc<int>(Map resp) => resp["statusCode"];
typedef int GetStatusCodeFunc(Map resp);
int getStatusCodeFunc(Map resp) => resp["statusCode"] as int;
BeanBuilder<List<T>> listRespBuilderWrap<T>(BeanBuilder<T> builder) =>
(Map resp) => (resp['list'] as List).map<T>((e) => builder(e)).toList();
class HttpErrorCode extends Error {
int statusCode;
@ -40,11 +46,13 @@ class Api {
}
static HandleRespBuild<T> _handleRespBuild<T>(BeanBuilder<T> builder) => (http.Response resp) {
if (builder == getStatusCodeFunc) return builder({"statusCode": resp.statusCode});
if (builder is GetStatusCodeFunc) return builder({"statusCode": resp.statusCode});
T res;
try {
res = builder(json.decode(resp.body));
var decodeBody = json.decode(utf8.decode(resp.bodyBytes));
res = decodeBody is Map ? builder(decodeBody) : builder({'list': decodeBody});
} catch (e) {
e.sout();
throw HttpErrorCode(resp.statusCode, message: resp.body);
}
return res;
@ -67,7 +75,7 @@ class Api {
path: path,
),
headers: <String, String>{
HttpHeaders.contentTypeHeader: ContentType.json.value,
HttpHeaders.contentTypeHeader: ContentType.json.toString(),
}..addAll(
ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}),
)
@ -98,7 +106,7 @@ class Api {
),
body: jsonEncode(body),
headers: <String, String>{
HttpHeaders.contentTypeHeader: ContentType.json.value,
HttpHeaders.contentTypeHeader: ContentType.json.toString(),
}..addAll(
ignoreToken ? {} : {HttpHeaders.authorizationHeader: 'Bearer ${H().sp.getString(SPKeys.accessToken)}'}),
)
@ -132,4 +140,24 @@ class Api {
ignoreToken: true,
ignoreErrorHandle: ignoreErrorHandle,
);
static Future<bool> checkAuthStatus() => _get<int>(Apis.auth.status, getStatusCodeFunc, ignoreErrorHandle: true)
.then((value) => value == HttpStatus.noContent);
static Future<bool> uploadScheme({required AppScheme.Scheme scheme, required bool share}) => _post(
Apis.scheme.upload,
getStatusCodeFunc,
body: SchemeSerializer.toMap(
Scheme(
name: scheme.name,
uuid: scheme.id,
description: scheme.description,
gestures: scheme.gestures,
shared: share,
),
),
).then((value) => value == HttpStatus.noContent);
static Future<List<Scheme>> userUploads() =>
_get(Apis.scheme.userUploads, listRespBuilderWrap(SchemeSerializer.fromMap));
}

@ -3,6 +3,7 @@ import 'package:dde_gesture_manager/constants/sp_keys.dart';
import 'package:dde_gesture_manager/constants/supported_locales.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'package:dde_gesture_manager/generated/codegen_loader.g.dart';
import 'package:dde_gesture_manager/http/api.dart';
import 'package:dde_gesture_manager/models/configs.dart';
import 'package:dde_gesture_manager/models/configs.provider.dart';
import 'package:dde_gesture_manager/models/settings.provider.dart';
@ -68,7 +69,18 @@ class MyApp extends StatelessWidget {
],
),
firstChild: Builder(builder: (context) {
Future.microtask(() => initEvents(context));
Future.microtask(() {
initEvents(context);
if (H().lastCheckAuthStatusTime != null &&
H().lastCheckAuthStatusTime!.difference(DateTime.now()) < Duration(minutes: 10)) return;
if (context.read<ConfigsProvider>().accessToken.notNull) {
Api.checkAuthStatus().then((value) {
if (!value) context.read<ConfigsProvider>().setProps(email: '', accessToken: '');
});
} else {
H().lastCheckAuthStatusTime = DateTime.now();
}
});
return Container();
}),
secondChild: HomePage(),

@ -131,7 +131,7 @@ class Scheme {
@ProviderModelProp()
List<GestureProp>? gestures;
bool get readOnly => uploaded == true || fromMarket == true || id == Uuid.NAMESPACE_NIL;
bool get readOnly => fromMarket == true || id == Uuid.NAMESPACE_NIL;
Scheme.parse(scheme) {
if (scheme is String) scheme = json.decode(scheme);

@ -1,6 +1,7 @@
import 'package:adaptive_scrollbar/adaptive_scrollbar.dart';
import 'package:dde_gesture_manager/constants/constants.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'package:dde_gesture_manager/http/api.dart';
import 'package:dde_gesture_manager/models/configs.provider.dart';
import 'package:dde_gesture_manager/models/content_layout.provider.dart';
import 'package:dde_gesture_manager/models/local_schemes_provider.dart';
@ -297,14 +298,13 @@ class GestureEditor extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(left: 10),
child: DButton.upload(
enabled: schemeProvider.uploaded == false,
enabled: schemeProvider.readOnly == false,
onTap: () async {
if (context.read<ConfigsProvider>().accessToken.isNull) {
return Notificator.showAlert(
title: LocaleKeys.info_login_for_upload_title.tr(),
description: LocaleKeys.info_login_for_upload_description.tr(),
).then((value) {
value.sout();
if (value == CustomButton.positiveButton) {
context
.read<ContentLayoutProvider>()
@ -317,7 +317,27 @@ class GestureEditor extends StatelessWidget {
description: LocaleKeys.info_upload_and_share_description.tr(),
positiveButtonTitle: LocaleKeys.str_share.tr(),
negativeButtonTitle: LocaleKeys.str_cancel.tr(),
);
).then((value) {
bool? _share;
if (value == CustomButton.positiveButton)
_share = true;
else if (value == CustomButton.negativeButton) _share = false;
if (_share != null) {
Api.uploadScheme(scheme: schemeProvider, share: _share).then((value) {
if (value) {
Notificator.success(context, title: LocaleKeys.info_upload_success.tr());
var localSchemesProvider = context.read<LocalSchemesProvider>();
var localSchemeEntry = localSchemesProvider.schemes!
.firstWhere((ele) => ele.scheme.id == schemeProvider.id);
localSchemeEntry.scheme.uploaded = true;
localSchemeEntry.save(localSchemesProvider);
} else {
Notificator.error(context, title: LocaleKeys.info_upload_failed.tr());
}
});
}
});
},
),
),

@ -1,10 +1,10 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:dde_gesture_manager/constants/constants.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'package:dde_gesture_manager/models/configs.provider.dart';
import 'package:dde_gesture_manager/models/content_layout.provider.dart';
import 'package:dde_gesture_manager/widgets/dde_button.dart';
import 'package:dde_gesture_manager/widgets/login.dart';
import 'package:dde_gesture_manager/widgets/me.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@ -82,33 +82,7 @@ class MarketOrMe extends StatelessWidget {
Widget buildMeContent(BuildContext context) {
var accessToken = context.watch<ConfigsProvider>().accessToken;
if (accessToken.isNull) return LoginWidget();
return Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.person, size: defaultButtonHeight),
Flexible(
child: AutoSizeText(
context.watch<ConfigsProvider>().email ?? '',
style: TextStyle(
fontSize: 18,
),
maxLines: 1,
),
),
DButton.logout(
enabled: true,
onTap: () => context.read<ConfigsProvider>().setProps(accessToken: '', email: ''),
),
],
),
],
),
);
return MeWidget();
}
Widget buildMarketContent(BuildContext context) {

@ -34,6 +34,8 @@ class H {
BuildContext get topContext => _topContext;
DateTime? lastCheckAuthStatusTime;
initTopContext(BuildContext context) {
_topContext = context;
}

@ -54,6 +54,12 @@ class _LoginWidgetState extends State<LoginWidget> {
title: LocaleKeys.info_sign_up_hint_title.tr(),
description: LocaleKeys.info_sign_up_hint_description.tr(),
);
else if (code == HttpStatus.forbidden)
Notificator.info(
context,
title: LocaleKeys.info_user_blocked_hint_title.tr(),
description: LocaleKeys.info_user_blocked_hint_description.tr(),
);
else
throw e;
}

@ -0,0 +1,69 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:dde_gesture_manager/constants/constants.dart';
import 'package:dde_gesture_manager/http/api.dart';
import 'package:dde_gesture_manager/models/configs.provider.dart';
import 'package:dde_gesture_manager_api/models.dart';
import 'package:flutter/material.dart';
import 'package:dde_gesture_manager/extensions.dart';
import 'dde_button.dart';
class MeWidget extends StatefulWidget {
const MeWidget({Key? key}) : super(key: key);
@override
_MeWidgetState createState() => _MeWidgetState();
}
class _MeWidgetState extends State<MeWidget> {
List<Scheme> uploads = [];
@override
void initState() {
super.initState();
Api.userUploads().then((value) {
if (mounted)
setState(() {
uploads = value;
});
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(Icons.person, size: defaultButtonHeight),
Flexible(
child: AutoSizeText(
context.watch<ConfigsProvider>().email ?? '',
style: TextStyle(
fontSize: 18,
),
maxLines: 1,
),
),
DButton.logout(
enabled: true,
onTap: () => context.read<ConfigsProvider>().setProps(accessToken: '', email: ''),
),
],
),
Text('我的上传'),
Container(
height: 400,
child: ListView.builder(
itemBuilder: (context, index) => Text(uploads[index].name ?? ''),
itemCount: uploads.length,
),
),
],
),
);
}
}

@ -138,6 +138,14 @@
"upload_and_share": {
"title": "Share the scheme at the same time?",
"description": "If you select [Share], other users can see this scheme and download it;\nIf you select [Cancel], you can still find this scheme in the [My Upload] list and share."
},
"user_blocked_hint": {
"title": "The account has been blocked!",
"description": "If you have any questions, please contact me using email."
},
"upload": {
"success": "Upload success ~",
"failed": "Upload failed.."
}
},
"me": {

@ -138,6 +138,14 @@
"upload_and_share": {
"title": "是否同时分享到方案市场?",
"description": "如果选择[分享],其他用户可以看到本方案并下载使用;\n如果选择[放弃],您仍可以稍后在[我的上传]列表中找到本方案进行操作。"
},
"user_blocked_hint": {
"title": "该账号已被封禁!",
"description": "如有疑问请通过发送邮件联系"
},
"upload": {
"success": "上传成功~",
"failed": "上传失败。。"
}
},
"me": {

Loading…
Cancel
Save