diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c8a157 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build output. +build/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a307539 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +A simple command-line application. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/bin/artserver.dart b/bin/artserver.dart new file mode 100644 index 0000000..4816177 --- /dev/null +++ b/bin/artserver.dart @@ -0,0 +1,46 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_hotreload/shelf_hotreload.dart'; +import 'db/mongo_config.dart'; +import 'routes/init.dart'; +import 'utils/cors.dart'; + +void main(List arguments) async { + await DBSetup.init(); + withHotreload(() => createServer(arguments)); +} + +Future createServer(List args) { + handler(shelf.Request request) { + print(request.url); + return InitRoute().handler(request); + } + + var parser = ArgParser() + ..addOption( + 'port', + abbr: 'p', + ); + var result = parser.parse( + args, + ); + var portStr = result['port'] ?? Platform.environment['PORT'] ?? '8080'; + var port = int.tryParse( + portStr, + ); + + if (port == null) { + stdout.writeln( + 'Could not parse port value "$portStr" into a number.', + ); + exitCode = 64; + } + var updateHandler = const shelf.Pipeline() + .addMiddleware(corsHeaders()) + .addMiddleware(shelf.logRequests()) + .addHandler(handler); + return io.serve(updateHandler, '192.168.1.162', 8080); +} diff --git a/bin/controllers/art.controller.dart b/bin/controllers/art.controller.dart new file mode 100644 index 0000000..78778b1 --- /dev/null +++ b/bin/controllers/art.controller.dart @@ -0,0 +1,167 @@ +import 'dart:convert'; + +import 'package:mongo_dart/mongo_dart.dart'; +import 'package:shelf/shelf.dart'; +import '../utils/extensions.dart'; +import '../db/mongo_config.dart'; +import 'image.controller.dart'; + +class ArtController { + Future getAllArts( + Request request, + ) async { + try { + final pipeline = AggregationPipelineBuilder() + .addStage(Lookup( + from: 'users', + localField: 'userId', + foreignField: '_id', + as: "seller")) + .addStage(Match({ + 'status': {"\$ne": "SOLD"} + })) + .build(); + var result = await DBSetup().artsRef.aggregateToStream(pipeline).toList(); + + return Response.ok( + jsonEncode(result), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future getArtById(Request request, String artId) async { + try { + var result = await DBSetup() + .artsRef + .findOne({"_id": ObjectId.fromHexString(artId)}); + if (result == null) { + return Response.notFound({"message": "No art found with this Id"}); + } + return Response.ok( + jsonEncode(result), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future createArt( + Request request, + ) async { + if (!request.isMultipart) { + return Response.badRequest( + body: jsonEncode({"message": "Please send in form data"})); + } + try { + List data; + try { + data = await request.multipartFormData.toList(); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({'message': "Error in multipart conversion:$e"})); + } + Map body = {}; + + for (var element in data) { + var key = element.name; + if (element.name == 'file') { + var imageBytes = (await element.part.readBytes()); + var res = await ImageController().uploadImage(imageBytes, + DateTime.now().millisecondsSinceEpoch.toString() + ".jpg"); + print('Image Upload Response - $res'); + if (res is String) { + body['image'] = res; + } else { + return Response.internalServerError(); + } + } else { + var value = await element.part.readString(); + body[key] = value; + } + } + print("Body for create art - $body"); + if (body.values.contains(null) || body.isEmpty) { + return Response.badRequest( + body: jsonEncode({"message": "Missing required fields"}), + ); + } + var newArt = { + "userId": ObjectId.fromHexString(body['userId']), + "title": body['title'], + 'status': body['status'] ?? "AVAILABLE", + "amount": double.tryParse(body['amount']) ?? 0.0, + "description": body['description'], + 'createdAt': DateTime.now().toIso8601String(), + 'updatedAt': DateTime.now().toIso8601String(), + 'image': body['image'] + }; + var dbRes = await DBSetup().artsRef.insertOne( + newArt, + ); + if (dbRes.hasWriteErrors) { + return Response.internalServerError( + body: jsonEncode({"message": "Art already exists"}), + ); + } + + return Response.ok(jsonEncode(dbRes.document)); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future updateStatus(Request request, String artId) async { + try { + var body = + Map.from(jsonDecode(await request.readAsString())); + var isArtExists = await DBSetup() + .artsRef + .findOne({"_id": ObjectId.fromHexString(artId)}); + print("Is Art Exists - ${isArtExists != null} and id - $artId"); + if (isArtExists != null) { + isArtExists['status'] = body['status']; + await DBSetup().artsRef.replaceOne( + {"_id": ObjectId.fromHexString(artId)}, + isArtExists, + ); + return Response.ok(jsonEncode({"message": "Updated art successfully"})); + } else { + return Response.notFound(jsonEncode({"message": "Art not exist"})); + } + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future getArtsPerUser(Request request, String userId) async { + try { + final pipeline = AggregationPipelineBuilder() + .addStage(Lookup( + from: 'users', + localField: 'userId', + foreignField: '_id', + as: "seller")) + .addStage(Match({'userId': ObjectId.fromHexString(userId)})) + .build(); + var result = await DBSetup().artsRef.aggregateToStream(pipeline).toList(); + + return Response.ok( + jsonEncode(result), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } +} diff --git a/bin/controllers/bid.controller.dart b/bin/controllers/bid.controller.dart new file mode 100644 index 0000000..5547e15 --- /dev/null +++ b/bin/controllers/bid.controller.dart @@ -0,0 +1,235 @@ +import 'dart:convert'; + +import 'package:mongo_dart/mongo_dart.dart'; +import 'package:shelf/shelf.dart'; + +import '../db/mongo_config.dart'; + +class BidController { + Future createBid(Request request) async { + try { + var body = jsonDecode(await request.readAsString()); + var bid = { + "artId": ObjectId.fromHexString(body["artId"]), + "userId": ObjectId.fromHexString(body["userId"]), + "amount": body["amount"], + 'status': body['status'], + 'sellerId': ObjectId.fromHexString(body['sellerId']), + "createdAt": DateTime.now().toIso8601String(), + "updatedAt": DateTime.now().toIso8601String() + }; + if (bid.values.contains(null)) { + return Response.badRequest( + body: jsonEncode({"message": "Missing required fields"}), + ); + } + var db = DBSetup().bidRef; + var query = { + "artId": ObjectId.fromHexString(body["artId"]), + "userId": ObjectId.fromHexString(body["userId"]), + }; + var isExists = await db.findOne(query); + print("Is Bid already exists - ${isExists != null}"); + if (isExists != null) { + isExists['amount'] = body['amount']; + isExists['updatedAt'] = DateTime.now().toIso8601String(); + var result = await db.replaceOne(query, isExists); + return Response.ok( + jsonEncode(result.isSuccess), + ); + } + var result = await db.insertOne(bid); + + return Response.ok( + jsonEncode(result.document), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future getBidForUserAndArt(Request request) async { + try { + var body = jsonDecode(await request.readAsString()); + final pipeline = AggregationPipelineBuilder() + .addStage(Lookup( + from: 'arts', + localField: 'artId', + foreignField: '_id', + as: "art")) + .addStage(Lookup( + from: 'users', + localField: 'userId', + foreignField: '_id', + as: "user")) + .addStage(Lookup( + from: 'users', + localField: 'sellerId', + foreignField: '_id', + as: "seller")) + .addStage(Match({"userId": ObjectId.fromHexString(body['userId'])})) + .addStage(Match({'artId': ObjectId.fromHexString(body['artId'])})) + .build(); + + var result = await DBSetup().bidRef.aggregateToStream(pipeline).toList(); + return Response.ok(jsonEncode(result)); + } on MongoDartError catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": e.message}), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future getBidsForArt(Request request, String artId) async { + try { + final pipeline = AggregationPipelineBuilder() + .addStage(Lookup( + from: 'arts', + localField: 'artId', + foreignField: '_id', + as: "art")) + .addStage(Lookup( + from: 'users', + localField: 'userId', + foreignField: '_id', + as: "user")) + .addStage(Lookup( + from: 'users', + localField: 'sellerId', + foreignField: '_id', + as: "seller")) + .addStage(Match({'artId': ObjectId.fromHexString(artId)})) + .build(); + + var result = await DBSetup().bidRef.aggregateToStream(pipeline).toList(); + print("Result is:$result"); + return Response.ok(jsonEncode(result)); + } on MongoDartError catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": e.message}), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future getBidsForUser(Request request, String userId) async { + try { + final pipeline = AggregationPipelineBuilder() + .addStage(Lookup( + from: 'arts', + localField: 'artId', + foreignField: '_id', + as: "art")) + .addStage(Lookup( + from: 'users', + localField: 'userId', + foreignField: '_id', + as: "user")) + .addStage(Lookup( + from: 'users', + localField: 'sellerId', + foreignField: '_id', + as: "seller")) + .addStage(Match({'userId': ObjectId.fromHexString(userId)})) + .build(); + + var result = await DBSetup().bidRef.aggregateToStream(pipeline).toList(); + return Response.ok(jsonEncode(result)); + } on MongoDartError catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": e.message}), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future getBidForMyArts(Request request, String userId) async { + try { + final pipeline = AggregationPipelineBuilder() + .addStage(Lookup( + from: 'arts', + localField: 'artId', + foreignField: '_id', + as: "art")) + .addStage(Lookup( + from: 'users', + localField: 'userId', + foreignField: '_id', + as: "user")) + .addStage(Lookup( + from: 'users', + localField: 'sellerId', + foreignField: '_id', + as: "seller")) + .addStage(Match({'sellerId': ObjectId.fromHexString(userId)})) + .addStage(Match({ + 'status': { + '\$nin': ['SOLD', 'OUTBID'] + }, + })) + .build(); + + var result = await DBSetup().bidRef.aggregateToStream(pipeline).toList(); + print('seller id - $userId & length - ${result.length}'); + + return Response.ok(jsonEncode(result)); + } on MongoDartError catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": e.message}), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future updateStatus(Request request, String bidId) async { + try { + var artsDb = DBSetup().artsRef; + var bidsDb = DBSetup().bidRef; + var body = + Map.from(jsonDecode(await request.readAsString())); + String artId = body['artId']; + var isBidExists = + await bidsDb.findOne({"_id": ObjectId.fromHexString(bidId)}); + print("Is Bid Exists - ${isBidExists != null} and id - $bidId"); + if (isBidExists != null) { + isBidExists['status'] = body['status']; + await bidsDb.replaceOne( + {"_id": ObjectId.fromHexString(bidId)}, + isBidExists, + ); + var newStatus = body['status']; + if (newStatus == 'SOLD') { + await bidsDb.updateMany({ + "artId": ObjectId.fromHexString(artId), + "status": {"\$ne": "SOLD"} + }, modify.set('status', 'OUTBID')); + } + await artsDb.updateOne({"_id": ObjectId.fromHexString(artId)}, + modify.set('status', body['status'])); + + return Response.ok(jsonEncode({"message": "Updated bid successfully"})); + } else { + return Response.notFound(jsonEncode({"message": "Bid not exist"})); + } + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } +} diff --git a/bin/controllers/image.controller.dart b/bin/controllers/image.controller.dart new file mode 100644 index 0000000..8573b4e --- /dev/null +++ b/bin/controllers/image.controller.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:shelf/shelf.dart'; + +class ImageController { + uploadImage(Uint8List imageData, String fileName) async { + try { + final _uploadDirectory = Directory('storage'); + if (await _uploadDirectory.exists() == false) { + await _uploadDirectory.create(); + } + File file = await File('${_uploadDirectory.path}/$fileName').create(); + print(await file.exists()); + var res = await file.writeAsBytes(imageData); + return res.path; + } catch (e) { + return {"message": "$e"}; + } + } + + Future getImage(Request request, String imageId) async { + try { + File file = File('storage/$imageId'); + return Response.ok(file.readAsBytesSync(), + headers: {'Content-type': 'image/jpg'}); + } catch (e) { + return Response.internalServerError(body: jsonEncode({'message': "$e"})); + } + } +} diff --git a/bin/controllers/seeder.dart b/bin/controllers/seeder.dart new file mode 100644 index 0000000..eb457de --- /dev/null +++ b/bin/controllers/seeder.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:shelf/shelf.dart'; + +import '../db/mongo_config.dart'; + +class Seeder { + createSeederForUser(Request request) async { + try { + var res = await DBSetup.db.createIndex( + 'users', + unique: true, + keys: {'username': 1, 'email': 1}, + ); + return Response.ok(jsonEncode(res)); + } catch (e) { + return Response.internalServerError(body: jsonEncode({'message': "$e"})); + } + } +} diff --git a/bin/controllers/user.controller.dart b/bin/controllers/user.controller.dart new file mode 100644 index 0000000..5c7b583 --- /dev/null +++ b/bin/controllers/user.controller.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; + +import 'package:mongo_dart/mongo_dart.dart'; +import 'package:shelf/shelf.dart'; + +import '../db/mongo_config.dart'; + +class UserController { + Future login(Request request) async { + var body = jsonDecode(await request.readAsString()); + try { + var result = await DBSetup().userRef.findOne( + {"email": body['email'], 'password': body['password']}, + ); + if (result != null) { + result.remove('password'); + return Response.ok( + jsonEncode(result), + ); + } else { + return Response.notFound( + jsonEncode({"message": "No user found"}), + ); + } + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future signUp(Request request) async { + try { + var body = jsonDecode(await request.readAsString()); + var user = { + 'username': body['username'], + 'email': body['email'], + 'phonenumber': body['phonenumber']?.toString(), + 'password': body['password'], + 'createdAt': DateTime.now().toIso8601String() + }; + if (user.values.contains(null)) { + return Response.badRequest( + body: jsonEncode({"message": "Missing required fields"}), + ); + } + var dbRes = await DBSetup().userRef.insertOne( + user, + ); + if (dbRes.hasWriteErrors) { + return Response.internalServerError( + body: jsonEncode({"message": "Users already exists"}), + ); + } + + return Response.ok(jsonEncode(dbRes.document)); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future getUserDetails(Request request, String userId) async { + try { + var result = await DBSetup() + .userRef + .findOne({"_id": ObjectId.fromHexString(userId)}); + if (result != null) { + result.remove('password'); + return Response.ok( + jsonEncode(result), + ); + } else { + return Response.notFound( + jsonEncode({"message": "No user found"}), + ); + } + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future getAllUsers( + Request request, + ) async { + try { + var result = await DBSetup().userRef.find().toList(); + var data = []; + for (var element in result) { + var user = element; + user.remove('password'); + data.add(user); + } + return Response.ok( + jsonEncode(data), + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } + + Future updateUser(Request request, String userId) async { + try { + var body = jsonDecode(await request.readAsString()); + var isUserExists = await DBSetup() + .userRef + .findOne({"_id": ObjectId.fromHexString(userId)}); + + if (isUserExists != null) { + var newUser = isUserExists; + newUser['username'] = body['username'] ?? isUserExists['username']; + newUser['phonenumber'] = body['phonenumber']?.toString() ?? + isUserExists['phonenumber']?.toString(); + print("New user will be :$newUser"); + var res = await DBSetup().userRef.replaceOne( + {"_id": ObjectId.fromHexString(userId)}, + newUser, + ); + print( + "Write errors:${res.writeError},document:${res.document},id:$userId"); + return Response.ok( + jsonEncode({"message": "Updated user successfully"})); + } else { + return Response.notFound( + jsonEncode({"message": "No user found"}), + ); + } + } catch (e) { + return Response.internalServerError( + body: jsonEncode({"message": "$e"}), + ); + } + } +} diff --git a/bin/db/mongo_config.dart b/bin/db/mongo_config.dart new file mode 100644 index 0000000..c4d1521 --- /dev/null +++ b/bin/db/mongo_config.dart @@ -0,0 +1,23 @@ +import 'package:mongo_dart/mongo_dart.dart'; + +class DBSetup { + DbCollection userRef = db.collection('users'); + DbCollection artsRef = db.collection('arts'); + DbCollection bidRef = db.collection('bids'); + + static late Db db; + + static const String mongoUrl = + 'mongodb+srv://shashiben:Shashiben7@cluster0.qrn8lm6.mongodb.net/?retryWrites=true&w=majority'; + static init() async { + try { + db = await Db.create(mongoUrl); + await db.open(); + print("Mongo is connected"); + } on MongoDartError catch (e) { + print('Mongo Exception: ${e.message}'); + } catch (e) { + print("Exception: $e"); + } + } +} diff --git a/bin/routes/arts.routes.dart b/bin/routes/arts.routes.dart new file mode 100644 index 0000000..a308fec --- /dev/null +++ b/bin/routes/arts.routes.dart @@ -0,0 +1,17 @@ +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +import '../controllers/art.controller.dart'; + +class ArtRoutes { + Handler get handler { + var router = Router(); + final _artController = ArtController(); + router.get('/user/', _artController.getArtsPerUser); + router.get('/', _artController.getAllArts); + router.get('/', _artController.getArtById); + router.put('/', _artController.updateStatus); + router.post('/', _artController.createArt); + return router; + } +} diff --git a/bin/routes/bid.routes.dart b/bin/routes/bid.routes.dart new file mode 100644 index 0000000..7bb6e66 --- /dev/null +++ b/bin/routes/bid.routes.dart @@ -0,0 +1,18 @@ +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +import '../controllers/bid.controller.dart'; + +class BidRoutes { + Handler get handler { + var router = Router(); + final BidController _bidController = BidController(); + router.post('/', _bidController.createBid); + router.post('/details', _bidController.getBidForUserAndArt); + router.get('/', _bidController.getBidsForUser); + router.get('/art/', _bidController.getBidsForArt); + router.get('/bidsForMyArt/', _bidController.getBidForMyArts); + router.put('/', _bidController.updateStatus); + return router; + } +} diff --git a/bin/routes/config_routes.dart b/bin/routes/config_routes.dart new file mode 100644 index 0000000..e0ec2d9 --- /dev/null +++ b/bin/routes/config_routes.dart @@ -0,0 +1,12 @@ +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +import '../controllers/seeder.dart'; + +class ConfigRoutes { + Handler get handler { + var router = Router(); + router.post('/create-index', Seeder().createSeederForUser); + return router; + } +} diff --git a/bin/routes/init.dart b/bin/routes/init.dart new file mode 100644 index 0000000..7508576 --- /dev/null +++ b/bin/routes/init.dart @@ -0,0 +1,21 @@ +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +import 'arts.routes.dart'; +import 'bid.routes.dart'; +import 'config_routes.dart'; +import 'storage.routes.dart'; +import 'user.routes.dart'; + +class InitRoute { + Handler get handler { + var router = Router(); + + router.mount('/user', UserRoutes().handler); + router.mount('/art', ArtRoutes().handler); + router.mount('/bid', BidRoutes().handler); + router.mount('/config', ConfigRoutes().handler); + router.mount('/storage', StorageRoutes().handler); + return router; + } +} diff --git a/bin/routes/storage.routes.dart b/bin/routes/storage.routes.dart new file mode 100644 index 0000000..248f31a --- /dev/null +++ b/bin/routes/storage.routes.dart @@ -0,0 +1,13 @@ +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +import '../controllers/image.controller.dart'; + +class StorageRoutes { + Handler get handler { + var router = Router(); + final ImageController _imageController = ImageController(); + router.get('/', _imageController.getImage); + return router; + } +} diff --git a/bin/routes/user.routes.dart b/bin/routes/user.routes.dart new file mode 100644 index 0000000..b85de53 --- /dev/null +++ b/bin/routes/user.routes.dart @@ -0,0 +1,18 @@ +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +import '../controllers/user.controller.dart'; + +class UserRoutes { + Handler get handler { + final UserController _userController = UserController(); + + var router = Router(); + router.post('/login', _userController.login); + router.post('/register', _userController.signUp); + router.get('/', _userController.getAllUsers); + router.get('/', _userController.getUserDetails); + router.put('/', _userController.updateUser); + return router; + } +} diff --git a/bin/utils/cors.dart b/bin/utils/cors.dart new file mode 100644 index 0000000..98f3d7f --- /dev/null +++ b/bin/utils/cors.dart @@ -0,0 +1,76 @@ +// ignore_for_file: constant_identifier_names + +import 'package:shelf/shelf.dart'; + +const ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'; +const ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'; +const ACCESS_CONTROL_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials'; +const ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers'; +const ACCESS_CONTROL_ALLOW_METHODS = 'Access-Control-Allow-Methods'; +const ACCESS_CONTROL_MAX_AGE = 'Access-Control-Max-Age'; + +const ORIGIN = 'origin'; + +const _defaultHeadersList = [ + 'accept', + 'accept-encoding', + 'authorization', + 'content-type', + 'dnt', + 'origin', + 'user-agent', +]; + +const _defaultMethodsList = [ + 'DELETE', + 'GET', + 'OPTIONS', + 'PATCH', + 'POST', + 'PUT' +]; + +Map _defaultHeaders = { + ACCESS_CONTROL_EXPOSE_HEADERS: '', + ACCESS_CONTROL_ALLOW_CREDENTIALS: 'true', + ACCESS_CONTROL_ALLOW_HEADERS: _defaultHeadersList.join(','), + ACCESS_CONTROL_ALLOW_METHODS: _defaultMethodsList.join(','), + ACCESS_CONTROL_MAX_AGE: '86400', +}; + +final _defaultHeadersAll = + _defaultHeaders.map((key, value) => MapEntry(key, [value])); + +typedef OriginChecker = bool Function(String origin); + +bool originAllowAll(String origin) => true; + +OriginChecker originOneOf(List origins) => + (origin) => origins.contains(origin); + +Middleware corsHeaders({ + Map? headers, + OriginChecker originChecker = originAllowAll, +}) { + final headersAll = headers?.map((key, value) => MapEntry(key, [value])); + return (Handler handler) { + return (Request request) async { + final origin = request.headers[ORIGIN]; + if (origin == null || !originChecker(origin)) { + return handler(request); + } + final _headers = >{ + ..._defaultHeadersAll, + ...?headersAll, + ACCESS_CONTROL_ALLOW_ORIGIN: [origin], + }; + + if (request.method == 'OPTIONS') { + return Response.ok(null, headers: _headers); + } + + final response = await handler(request); + return response.change(headers: {...response.headersAll, ..._headers}); + }; + }; +} diff --git a/bin/utils/extensions.dart b/bin/utils/extensions.dart new file mode 100644 index 0000000..4a0af6c --- /dev/null +++ b/bin/utils/extensions.dart @@ -0,0 +1,209 @@ +import 'package:http_parser/http_parser.dart'; +import 'package:shelf/shelf.dart'; +import 'package:string_scanner/string_scanner.dart'; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:mime/mime.dart'; + +extension ReadFormData on Request { + /// Whether this request has a multipart form body. + bool get isMultipartForm { + final rawContentType = headers['Content-Type']; + if (rawContentType == null) return false; + + final type = MediaType.parse(rawContentType); + return type.type == 'multipart' && type.subtype == 'form-data'; + } + + /// Reads invididual form data elements from this request. + Stream get multipartFormData { + return parts + .map((part) { + final rawDisposition = part.headers['content-disposition']; + if (rawDisposition == null) return null; + + final formDataParams = + _parseFormDataContentDisposition(rawDisposition); + if (formDataParams == null) return null; + + final name = formDataParams['name']; + if (name == null) return null; + + return FormData._(name, formDataParams['filename'], part); + }) + .where((data) => data != null) + .cast(); + } +} + +/// A [Multipart] subpart with a parsed [name] and [filename] values read from +/// its `content-disposition` header. +class FormData { + /// The name of this form data element. + /// + /// Names are usually unique, but this is not verified by this package. + final String name; + + /// An optional name describing the name of the file being uploaded. + final String? filename; + + final Multipart part; + + FormData._(this.name, this.filename, this.part); +} + +final _token = RegExp(r'[^()<>@,;:"\\/[\]?={} \t\x00-\x1F\x7F]+'); +final _whitespace = RegExp(r'(?:(?:\r\n)?[ \t]+)*'); +final _quotedString = RegExp(r'"(?:[^"\x00-\x1F\x7F]|\\.)*"'); +final _quotedPair = RegExp(r'\\(.)'); + +/// Parses a `content-disposition: form-data; arg1="val1"; ...` header. +Map? _parseFormDataContentDisposition(String header) { + final scanner = StringScanner(header); + + scanner + ..scan(_whitespace) + ..expect(_token); + if (scanner.lastMatch![0] != 'form-data') return null; + + final params = {}; + + while (scanner.scan(';')) { + scanner + ..scan(_whitespace) + ..scan(_token); + final key = scanner.lastMatch![0]!; + scanner.expect('='); + + String value; + if (scanner.scan(_token)) { + value = scanner.lastMatch![0]!; + } else { + scanner.expect(_quotedString, name: 'quoted string'); + final string = scanner.lastMatch![0]!; + + value = string + .substring(1, string.length - 1) + .replaceAllMapped(_quotedPair, (match) => match[1]!); + } + + scanner.scan(_whitespace); + params[key] = value; + } + + scanner.expectDone(); + return params; +} + +/// Extension methods to handle multipart requests. +/// +/// To check whether a request contains multipart data, use [isMultipart]. +/// Individual parts can the be red with [parts]. +extension ReadMultipartRequest on Request { + /// Whether this request has a multipart body. + /// + /// Requests are considered to have a multipart body if they have a + /// `Content-Type` header with a `multipart` type and a valid `boundary` + /// parameter as defined by section 5.1.1 of RFC 2046. + bool get isMultipart => _extractMultipartBoundary() != null; + + /// Reads parts of this multipart request. + /// + /// Each part is represented as a [MimeMultipart], which implements the + /// [Stream] interface to emit chunks of data. + /// Headers of a part are available through [MimeMultipart.headers]. + /// + /// Parts can be processed by listening to this stream, as shown in this + /// example: + /// + /// ```dart + /// await for (final part in request.parts) { + /// final headers = part.headers; + /// final content = utf8.decoder.bind(part).first; + /// } + /// ``` + /// + /// Listening to this stream will [read] this request, which may only be done + /// once. + /// + /// Throws a [StateError] if this is not a multipart request (as reported + /// through [isMultipart]). The stream will emit a [MimeMultipartException] + /// if the request does not contain a well-formed multipart body. + Stream get parts { + final boundary = _extractMultipartBoundary(); + if (boundary == null) { + throw StateError('Not a multipart request.'); + } + + return MimeMultipartTransformer(boundary) + .bind(read()) + .map((part) => Multipart(this, part)); + } + + /// Extracts the `boundary` parameter from the content-type header, if this is + /// a multipart request. + String? _extractMultipartBoundary() { + if (!headers.containsKey('Content-Type')) return null; + + final contentType = MediaType.parse(headers['Content-Type']!); + if (contentType.type != 'multipart') return null; + + return contentType.parameters['boundary']; + } +} + +/// An entry in a multipart request. +class Multipart extends MimeMultipart { + final Request _originalRequest; + final MimeMultipart _inner; + + @override + final Map headers; + + late final MediaType? _contentType = _parseContentType(); + + Encoding? get _encoding { + var contentType = _contentType; + if (contentType == null) return null; + if (!contentType.parameters.containsKey('charset')) return null; + return Encoding.getByName(contentType.parameters['charset']); + } + + Multipart(this._originalRequest, this._inner) + : headers = CaseInsensitiveMap.from(_inner.headers); + + MediaType? _parseContentType() { + final value = headers['content-type']; + if (value == null) return null; + + return MediaType.parse(value); + } + + /// Reads the content of this subpart as a single [Uint8List]. + Future readBytes() async { + final builder = BytesBuilder(); + await forEach(builder.add); + return builder.takeBytes(); + } + + /// Reads the content of this subpart as a string. + /// + /// The optional [encoding] parameter can be used to override the encoding + /// used. By default, the `content-type` header of this part will be used, + /// with a fallback to the `content-type` of the surrounding request and + /// another fallback to [utf8] if everything else fails. + Future readString([Encoding? encoding]) { + encoding ??= _encoding ?? _originalRequest.encoding ?? utf8; + return encoding.decodeStream(this); + } + + @override + StreamSubscription> listen(void Function(List data)? onData, + {void Function()? onDone, Function? onError, bool? cancelOnError}) { + return _inner.listen(onData, + onDone: onDone, onError: onError, cancelOnError: cancelOnError); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..bc2813a --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,327 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: "direct main" + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + basic_utils: + dependency: transitive + description: + name: basic_utils + url: "https://pub.dartlang.org" + source: hosted + version: "3.9.4" + bson: + dependency: transitive + description: + name: bson + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + buffer: + dependency: transitive + description: + name: buffer + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_methods: + dependency: transitive + description: + name: http_methods + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.0" + lints: + dependency: "direct dev" + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + mongo_dart: + dependency: "direct main" + description: + name: mongo_dart + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.4+1" + mongo_dart_query: + dependency: transitive + description: + name: mongo_dart_query + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.1" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.0" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + rational: + dependency: transitive + description: + name: rational + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + sasl_scram: + dependency: transitive + description: + name: sasl_scram + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" + saslprep: + dependency: transitive + description: + name: saslprep + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + shelf: + dependency: "direct main" + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + shelf_hotreload: + dependency: "direct main" + description: + name: shelf_hotreload + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + shelf_router: + dependency: "direct main" + description: + name: shelf_router + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + shelf_static: + dependency: "direct main" + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "8.3.0" + vy_string_utils: + dependency: transitive + description: + name: vy_string_utils + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" +sdks: + dart: ">=2.16.2 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..9b3f622 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,16 @@ +name: artserver +description: A simple command-line application. +version: 1.0.0 + +environment: + sdk: ">=2.16.2 <3.0.0" + +dev_dependencies: + lints: ^1.0.0 +dependencies: + args: ^2.3.1 + mongo_dart: ^0.7.4+1 + shelf: ^1.3.1 + shelf_hotreload: null + shelf_router: ^1.1.3 + shelf_static: ^1.1.1 diff --git a/storage/1655731742627.jpg b/storage/1655731742627.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655731742627.jpg differ diff --git a/storage/1655731952811.jpg b/storage/1655731952811.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655731952811.jpg differ diff --git a/storage/1655732037792.jpg b/storage/1655732037792.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655732037792.jpg differ diff --git a/storage/1655732125576.jpg b/storage/1655732125576.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655732125576.jpg differ diff --git a/storage/1655732260933.jpg b/storage/1655732260933.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655732260933.jpg differ diff --git a/storage/1655815451288.jpg b/storage/1655815451288.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655815451288.jpg differ diff --git a/storage/1655815521503.jpg b/storage/1655815521503.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655815521503.jpg differ diff --git a/storage/1655815758336.jpg b/storage/1655815758336.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655815758336.jpg differ diff --git a/storage/1655815821000.jpg b/storage/1655815821000.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655815821000.jpg differ diff --git a/storage/1655815853716.jpg b/storage/1655815853716.jpg new file mode 100644 index 0000000..a787799 Binary files /dev/null and b/storage/1655815853716.jpg differ diff --git a/storage/1655822247821.jpg b/storage/1655822247821.jpg new file mode 100644 index 0000000..86c9bc1 Binary files /dev/null and b/storage/1655822247821.jpg differ diff --git a/storage/1655822346674.jpg b/storage/1655822346674.jpg new file mode 100644 index 0000000..86c9bc1 Binary files /dev/null and b/storage/1655822346674.jpg differ diff --git a/storage/1655871341206.jpg b/storage/1655871341206.jpg new file mode 100644 index 0000000..8a712b0 Binary files /dev/null and b/storage/1655871341206.jpg differ diff --git a/storage/1655872402026.jpg b/storage/1655872402026.jpg new file mode 100644 index 0000000..3321017 Binary files /dev/null and b/storage/1655872402026.jpg differ diff --git a/storage/1655873083414.jpg b/storage/1655873083414.jpg new file mode 100644 index 0000000..9f6b754 Binary files /dev/null and b/storage/1655873083414.jpg differ diff --git a/storage/1655873094366.jpg b/storage/1655873094366.jpg new file mode 100644 index 0000000..337934a Binary files /dev/null and b/storage/1655873094366.jpg differ diff --git a/storage/1655873106791.jpg b/storage/1655873106791.jpg new file mode 100644 index 0000000..36f5a35 Binary files /dev/null and b/storage/1655873106791.jpg differ diff --git a/storage/1655873989600.jpg b/storage/1655873989600.jpg new file mode 100644 index 0000000..ee65e3c Binary files /dev/null and b/storage/1655873989600.jpg differ diff --git a/storage/1655874540727.jpg b/storage/1655874540727.jpg new file mode 100644 index 0000000..a330222 Binary files /dev/null and b/storage/1655874540727.jpg differ diff --git a/storage/1656082169419.jpg b/storage/1656082169419.jpg new file mode 100644 index 0000000..3627733 Binary files /dev/null and b/storage/1656082169419.jpg differ diff --git a/storage/1656167083717.jpg b/storage/1656167083717.jpg new file mode 100644 index 0000000..85ab6e5 Binary files /dev/null and b/storage/1656167083717.jpg differ diff --git a/storage/1656167146263.jpg b/storage/1656167146263.jpg new file mode 100644 index 0000000..8a712b0 Binary files /dev/null and b/storage/1656167146263.jpg differ diff --git a/storage/1656167300868.jpg b/storage/1656167300868.jpg new file mode 100644 index 0000000..0dcc7b2 Binary files /dev/null and b/storage/1656167300868.jpg differ diff --git a/storage/1659829745459.jpg b/storage/1659829745459.jpg new file mode 100644 index 0000000..351e893 Binary files /dev/null and b/storage/1659829745459.jpg differ diff --git a/storage/1659830799907.jpg b/storage/1659830799907.jpg new file mode 100644 index 0000000..39c039a Binary files /dev/null and b/storage/1659830799907.jpg differ diff --git a/storage/1659832467915.jpg b/storage/1659832467915.jpg new file mode 100644 index 0000000..475a0ff Binary files /dev/null and b/storage/1659832467915.jpg differ diff --git a/storage/1659832661772.jpg b/storage/1659832661772.jpg new file mode 100644 index 0000000..dc6e45b Binary files /dev/null and b/storage/1659832661772.jpg differ diff --git a/storage/1659835345435.jpg b/storage/1659835345435.jpg new file mode 100644 index 0000000..475a0ff Binary files /dev/null and b/storage/1659835345435.jpg differ diff --git a/storage/1659835799946.jpg b/storage/1659835799946.jpg new file mode 100644 index 0000000..475a0ff Binary files /dev/null and b/storage/1659835799946.jpg differ diff --git a/storage/1659836364618.jpg b/storage/1659836364618.jpg new file mode 100644 index 0000000..475a0ff Binary files /dev/null and b/storage/1659836364618.jpg differ diff --git a/storage/1659867625200.jpg b/storage/1659867625200.jpg new file mode 100644 index 0000000..475a0ff Binary files /dev/null and b/storage/1659867625200.jpg differ