From c856f02e140c53d165529a1b12d0c5d1a7b15037 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 31 Oct 2024 15:16:32 +0100 Subject: [PATCH 01/37] Initial api server commit --- bin/running_on_dart.dart | 2 + docker-compose.yml | 4 +- lib/running_on_dart.dart | 2 + lib/src/api/api_server.dart | 35 +++++++ lib/src/api/jwt_middleware.dart | 13 +++ pubspec.lock | 180 +++++--------------------------- pubspec.yaml | 6 ++ 7 files changed, 86 insertions(+), 156 deletions(-) create mode 100644 lib/src/api/api_server.dart create mode 100644 lib/src/api/jwt_middleware.dart diff --git a/bin/running_on_dart.dart b/bin/running_on_dart.dart index 2b6206f..575e311 100644 --- a/bin/running_on_dart.dart +++ b/bin/running_on_dart.dart @@ -47,4 +47,6 @@ void main() async { )); await setupContainer(client); + + WebServer().startServer(); } diff --git a/docker-compose.yml b/docker-compose.yml index e831fe4..8db9f0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,8 +6,8 @@ services: container_name: running_on_dart env_file: - .env - links: - - db + ports: + - "8088:8088" depends_on: - db diff --git a/lib/running_on_dart.dart b/lib/running_on_dart.dart index 2dc8d04..080f796 100644 --- a/lib/running_on_dart.dart +++ b/lib/running_on_dart.dart @@ -15,3 +15,5 @@ export 'src/settings.dart'; export 'src/converter.dart'; export 'src/error_handler.dart'; export 'src/init.dart'; + +export 'src/api/api_server.dart' show WebServer; diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart new file mode 100644 index 0000000..6dfc1e1 --- /dev/null +++ b/lib/src/api/api_server.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; + +import 'package:injector/injector.dart'; +import 'package:running_on_dart/src/api/jwt_middleware.dart'; +import 'package:running_on_dart/src/modules/bot_start_duration.dart'; + +import 'package:shelf_router/shelf_router.dart' as shelf_router; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; + +class WebServer { + Future _handleBotInfo(shelf.Request request) async { + final botStartDuration = Injector.appInstance.get(); + + return shelf.Response.ok(jsonEncode({"ok": true, 'uptime': botStartDuration.startDate.toIso8601String()}), headers: {'Content-Type': 'application/json'}); + } + + Future _setupRouter() async { + return shelf_router.Router() + ..get("/api/info", _handleBotInfo) + ..get("/api/test", _authorized(_handleBotInfo)); + } + + shelf.Handler _authorized(shelf.Handler inner) => const shelf.Pipeline().addMiddleware(jwtMiddleware()).addHandler(inner); + + Future startServer() async { + final router = await _setupRouter(); + + final app = const shelf.Pipeline() + .addMiddleware(shelf.logRequests()) + .addHandler(router.call); + + await shelf_io.serve(app, "0.0.0.0", 8088); + } +} diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart new file mode 100644 index 0000000..0bc366b --- /dev/null +++ b/lib/src/api/jwt_middleware.dart @@ -0,0 +1,13 @@ +import 'package:shelf/shelf.dart' as shelf; + +shelf.Middleware jwtMiddleware() => (shelf.Handler handler) { + return (shelf.Request request) { + final authHeader = request.headers['Authorization']; + + if (authHeader == null) { + return shelf.Response.unauthorized(null); + } + + return handler(request); + }; +}; diff --git a/pubspec.lock b/pubspec.lock index a14364b..5f977dc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" collection: dependency: "direct main" description: @@ -105,14 +105,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - coverage: - dependency: transitive - description: - name: coverage - sha256: "4b03e11f6d5b8f6e5bb5e9f7889a56fe6c5cbe942da5378ea4d4d7f73ef9dfe5" - url: "https://pub.dev" - source: hosted - version: "1.11.0" crypto: dependency: transitive description: @@ -169,14 +161,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" fuzzy: dependency: "direct main" description: @@ -209,14 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" - http_multi_server: + http_methods: dependency: transitive description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "1.1.1" http_parser: dependency: transitive description: @@ -245,26 +229,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "99f282cb0e02edcbbf8c6b3bbc7c90b65635156c412e58f3975a7e55284ce685" - url: "https://pub.dev" - source: hosted - version: "0.20.0" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.dev" - source: hosted - version: "0.7.1" + version: "0.19.0" lints: dependency: "direct dev" description: @@ -285,10 +253,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" meta: dependency: transitive description: @@ -302,34 +270,18 @@ packages: description: path: "." ref: HEAD - resolved-ref: "82104b2934f7fa13a88e2cd596d4b4df932e6cd6" + resolved-ref: a31aaa35c975e8a6d17554d0241c11c297b7afb6 url: "https://github.com/nyxx-discord/migent.git" source: git - version: "1.2.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" + version: "1.1.0" nyxx: dependency: "direct main" description: name: nyxx - sha256: "3ee891f4098c0dd7b992dafc2b4e961b82bd076b3149a15408efda1e11c7c5aa" + sha256: "073c4f3a49269b4580c54aef32a472b3eae0e6f0e125eb1be2eeb5976210e886" url: "https://pub.dev" source: hosted - version: "6.5.2" + version: "6.5.0" nyxx_commands: dependency: "direct main" description: @@ -342,10 +294,10 @@ packages: dependency: "direct main" description: name: nyxx_extensions - sha256: f0b6dfc5ed2350c10f3018c7fc5fbaa7c01fbce6fee8b8fb14724f57cc0d67fe + sha256: "8016605fc131320bfb58362e5124a0fb1c23e51c26f1638098611fa8f93760aa" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.0" oauth2: dependency: transitive description: @@ -398,10 +350,10 @@ packages: dependency: "direct main" description: name: postgres - sha256: e9802d4c9d78e432c4d4e57be9a91d99cf1552d45f17b17792f8e595060376a2 + sha256: bc3a36f9960d822af1ac4c2e0a32c4e7a3e426d2ce4500c11afca40f53c34612 url: "https://pub.dev" source: hosted - version: "3.4.3" + version: "3.4.0" pub_semver: dependency: transitive description: @@ -451,53 +403,21 @@ packages: source: hosted version: "1.0.3" shelf: - dependency: transitive + dependency: "direct main" description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted version: "1.4.2" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.dev" - source: hosted - version: "1.1.3" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - source_maps: - dependency: transitive + shelf_router: + dependency: "direct main" description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + name: shelf_router + sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "1.1.4" source_span: dependency: transitive description: @@ -555,14 +475,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - test: - dependency: "direct dev" - description: - name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" - url: "https://pub.dev" - source: hosted - version: "1.25.8" test_api: dependency: transitive description: @@ -571,14 +483,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.3" - test_core: - dependency: transitive - description: - name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" - url: "https://pub.dev" - source: hosted - version: "0.6.5" typed_data: dependency: transitive description: @@ -595,14 +499,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" - url: "https://pub.dev" - source: hosted - version: "14.3.1" watcher: dependency: transitive description: @@ -615,34 +511,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" - url: "https://pub.dev" - source: hosted - version: "0.1.6" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.1.0" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 973d3bf..b7bc43e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,9 +27,15 @@ dependencies: git: https://github.com/TentacleOrg/Tentacle.git injector: ^4.0.0 + # directly used transitive dependencies dio: any built_collection: any + # api server + shelf: ^1.4.2 + shelf_router: ^1.1.4 + jaguar_jwt: ^3.0.0 + dev_dependencies: lints: ^5.0.0 test: ^1.25.8 From ae2ac60bdb6c5ad2061a662d5a0b1119430e09ae Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 31 Oct 2024 15:33:30 +0100 Subject: [PATCH 02/37] Initial jwt auth implementation --- lib/src/api/jwt_middleware.dart | 34 +++++++++++++++++++++++++++++++-- lib/src/api/utils.dart | 11 +++++++++++ pubspec.lock | 16 ++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 lib/src/api/utils.dart diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart index 0bc366b..6f8313c 100644 --- a/lib/src/api/jwt_middleware.dart +++ b/lib/src/api/jwt_middleware.dart @@ -1,11 +1,41 @@ +import 'dart:convert'; + +import 'package:jaguar_jwt/jaguar_jwt.dart'; +import 'package:running_on_dart/running_on_dart.dart'; +import 'package:running_on_dart/src/api/utils.dart'; import 'package:shelf/shelf.dart' as shelf; -shelf.Middleware jwtMiddleware() => (shelf.Handler handler) { +final jwtKey = getEnv("JWT_KEY"); + +class MissingPermissionsException implements Exception {} + +void validateClaims(JwtClaim jwt, List requiredPermissions) { + final claims = jwt.payload['permissions'] as List? ?? []; + + final valid = Set.of(claims).containsAll(requiredPermissions); + if (!valid) { + throw MissingPermissionsException(); + } +} + +shelf.Middleware jwtMiddleware([List requiredPermissions = const []]) => (shelf.Handler handler) { return (shelf.Request request) { final authHeader = request.headers['Authorization']; if (authHeader == null) { - return shelf.Response.unauthorized(null); + return createUnauthorizedResponse('Missing authorization header'); + } + + try { + final jwt = verifyJwtHS256Signature(authHeader, jwtKey); + + if (requiredPermissions.isNotEmpty) { + validateClaims(jwt, requiredPermissions); + } + } on JwtException catch (e) { + return createUnauthorizedResponse(e.message); + } on MissingPermissionsException { + return createForbiddenResponse(); } return handler(request); diff --git a/lib/src/api/utils.dart b/lib/src/api/utils.dart new file mode 100644 index 0000000..e87e726 --- /dev/null +++ b/lib/src/api/utils.dart @@ -0,0 +1,11 @@ +import 'dart:convert'; + +import 'package:shelf/shelf.dart' as shelf; + +shelf.Response createJsonErrorResponse(int errorCode, String errorMessage) { + return shelf.Response(errorCode, body: jsonEncode({"message": errorMessage}), headers: {"Content-Type": 'application/json'}); +} + +shelf.Response createUnauthorizedResponse(String errorMessage) => createJsonErrorResponse(400, errorMessage); + +shelf.Response createForbiddenResponse() => shelf.Response.forbidden(null); diff --git a/pubspec.lock b/pubspec.lock index 5f977dc..a491852 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" + auth_header: + dependency: transitive + description: + name: auth_header + sha256: "0a3938128b6124530de93ce1a20ccb58639195fe7952f638248ea1bc0e5408eb" + url: "https://pub.dev" + source: hosted + version: "3.0.1" boolean_selector: dependency: transitive description: @@ -233,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + jaguar_jwt: + dependency: "direct main" + description: + name: jaguar_jwt + sha256: c3ab24be5ba5f736f93eacfc94c4381bd93551a351ab5687e70c8d71a9916e8d + url: "https://pub.dev" + source: hosted + version: "3.0.0" lints: dependency: "direct dev" description: From 7a35eabe8540d1f202bf9cb3efb0e027531cca97 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 31 Oct 2024 16:00:37 +0100 Subject: [PATCH 03/37] Fix jwt parsing --- lib/src/api/jwt_middleware.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart index 6f8313c..d010280 100644 --- a/lib/src/api/jwt_middleware.dart +++ b/lib/src/api/jwt_middleware.dart @@ -9,6 +9,12 @@ final jwtKey = getEnv("JWT_KEY"); class MissingPermissionsException implements Exception {} +String generateJwtKey(String subject) { + final claimSet = JwtClaim(subject: subject, maxAge: Duration(days: 1)); + + return issueJwtHS256(claimSet, jwtKey); +} + void validateClaims(JwtClaim jwt, List requiredPermissions) { final claims = jwt.payload['permissions'] as List? ?? []; @@ -27,7 +33,7 @@ shelf.Middleware jwtMiddleware([List requiredPermissions = const []]) => } try { - final jwt = verifyJwtHS256Signature(authHeader, jwtKey); + final jwt = verifyJwtHS256Signature(authHeader.replaceFirst('Bearer ', ''), jwtKey); if (requiredPermissions.isNotEmpty) { validateClaims(jwt, requiredPermissions); From 39c14f4b50039efd6172bd1a663b0ac003046219 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 31 Oct 2024 17:07:17 +0100 Subject: [PATCH 04/37] Minor naming fixes --- lib/src/api/api_server.dart | 4 ++-- lib/src/api/jwt_middleware.dart | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index 6dfc1e1..0c5a848 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -17,8 +17,8 @@ class WebServer { Future _setupRouter() async { return shelf_router.Router() - ..get("/api/info", _handleBotInfo) - ..get("/api/test", _authorized(_handleBotInfo)); + ..get("/api/info", _handleBotInfo); + // ..get("/api/test", _authorized(_handleBotInfo)); } shelf.Handler _authorized(shelf.Handler inner) => const shelf.Pipeline().addMiddleware(jwtMiddleware()).addHandler(inner); diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart index d010280..d8fbb9d 100644 --- a/lib/src/api/jwt_middleware.dart +++ b/lib/src/api/jwt_middleware.dart @@ -10,15 +10,15 @@ final jwtKey = getEnv("JWT_KEY"); class MissingPermissionsException implements Exception {} String generateJwtKey(String subject) { - final claimSet = JwtClaim(subject: subject, maxAge: Duration(days: 1)); + final jwtClaim = JwtClaim(subject: subject, maxAge: Duration(days: 1)); - return issueJwtHS256(claimSet, jwtKey); + return issueJwtHS256(jwtClaim, jwtKey); } void validateClaims(JwtClaim jwt, List requiredPermissions) { - final claims = jwt.payload['permissions'] as List? ?? []; + final permissions = jwt.payload['permissions'] as List? ?? []; - final valid = Set.of(claims).containsAll(requiredPermissions); + final valid = Set.of(permissions).containsAll(requiredPermissions); if (!valid) { throw MissingPermissionsException(); } From acf46d11f3c70491e9759fd126240d3e78a394bb Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 1 Nov 2024 00:25:13 +0100 Subject: [PATCH 05/37] Implement oauth discord login --- lib/src/api/api_server.dart | 43 ++++++++++++++++++++++++----- lib/src/api/jwt_middleware.dart | 48 ++++++++++++++++----------------- lib/src/api/utils.dart | 3 ++- pubspec.lock | 8 ++++++ pubspec.yaml | 1 + 5 files changed, 70 insertions(+), 33 deletions(-) diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index 0c5a848..77829fb 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -1,34 +1,63 @@ import 'dart:convert'; import 'package:injector/injector.dart'; +import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/api/jwt_middleware.dart'; import 'package:running_on_dart/src/modules/bot_start_duration.dart'; +import 'package:shelf_cors_headers/shelf_cors_headers.dart'; import 'package:shelf_router/shelf_router.dart' as shelf_router; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:http/http.dart' as http; + +final clientId = getEnv('DISCORD_CLIENT_ID'); +final clientSecret = getEnv('DISCORD_CLIENT_SECRET'); +final clientRedirectUri = getEnv('DISCORD_REDIRECT_URI'); + class WebServer { Future _handleBotInfo(shelf.Request request) async { final botStartDuration = Injector.appInstance.get(); - return shelf.Response.ok(jsonEncode({"ok": true, 'uptime': botStartDuration.startDate.toIso8601String()}), headers: {'Content-Type': 'application/json'}); + return shelf.Response.ok(jsonEncode({"ok": true, 'uptime': botStartDuration.startDate.toIso8601String()}), + headers: {'Content-Type': 'application/json'}); + } + + Future _handleLogin(shelf.Request request) async { + final requestBody = jsonDecode(await request.readAsString()) as Map; + final authCode = requestBody['code']; + + await http.post(Uri.https('discord.com', '/api/oauth2/token'), body: { + 'client_id': clientId, + 'client_secret': clientSecret, + 'redirect_uri': clientRedirectUri, + 'grant_type': 'authorization_code', + 'code': authCode, + }, headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }); + + final jwtToken = generateJwtKey('test'); + + return shelf.Response.ok(jsonEncode({'token': jwtToken})); } Future _setupRouter() async { return shelf_router.Router() - ..get("/api/info", _handleBotInfo); - // ..get("/api/test", _authorized(_handleBotInfo)); + ..get("/api/info", _handleBotInfo) + ..post("/api/login", _handleLogin); + // ..get("/api/test", _authorized(_handleBotInfo)); } - shelf.Handler _authorized(shelf.Handler inner) => const shelf.Pipeline().addMiddleware(jwtMiddleware()).addHandler(inner); + shelf.Handler _authorized(shelf.Handler inner) => + const shelf.Pipeline().addMiddleware(jwtMiddleware()).addHandler(inner); Future startServer() async { final router = await _setupRouter(); - final app = const shelf.Pipeline() - .addMiddleware(shelf.logRequests()) - .addHandler(router.call); + final app = + const shelf.Pipeline().addMiddleware(shelf.logRequests()).addMiddleware(corsHeaders()).addHandler(router.call); await shelf_io.serve(app, "0.0.0.0", 8088); } diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart index d8fbb9d..b2bd375 100644 --- a/lib/src/api/jwt_middleware.dart +++ b/lib/src/api/jwt_middleware.dart @@ -1,11 +1,9 @@ -import 'dart:convert'; - import 'package:jaguar_jwt/jaguar_jwt.dart'; import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/api/utils.dart'; import 'package:shelf/shelf.dart' as shelf; -final jwtKey = getEnv("JWT_KEY"); +final jwtKey = getEnv("JWT_SECRET"); class MissingPermissionsException implements Exception {} @@ -25,25 +23,25 @@ void validateClaims(JwtClaim jwt, List requiredPermissions) { } shelf.Middleware jwtMiddleware([List requiredPermissions = const []]) => (shelf.Handler handler) { - return (shelf.Request request) { - final authHeader = request.headers['Authorization']; - - if (authHeader == null) { - return createUnauthorizedResponse('Missing authorization header'); - } - - try { - final jwt = verifyJwtHS256Signature(authHeader.replaceFirst('Bearer ', ''), jwtKey); - - if (requiredPermissions.isNotEmpty) { - validateClaims(jwt, requiredPermissions); - } - } on JwtException catch (e) { - return createUnauthorizedResponse(e.message); - } on MissingPermissionsException { - return createForbiddenResponse(); - } - - return handler(request); - }; -}; + return (shelf.Request request) { + final authHeader = request.headers['Authorization']; + + if (authHeader == null) { + return createUnauthorizedResponse('Missing authorization header'); + } + + try { + final jwt = verifyJwtHS256Signature(authHeader.replaceFirst('Bearer ', ''), jwtKey); + + if (requiredPermissions.isNotEmpty) { + validateClaims(jwt, requiredPermissions); + } + } on JwtException catch (e) { + return createUnauthorizedResponse(e.message); + } on MissingPermissionsException { + return createForbiddenResponse(); + } + + return handler(request); + }; + }; diff --git a/lib/src/api/utils.dart b/lib/src/api/utils.dart index e87e726..7a8e3c6 100644 --- a/lib/src/api/utils.dart +++ b/lib/src/api/utils.dart @@ -3,7 +3,8 @@ import 'dart:convert'; import 'package:shelf/shelf.dart' as shelf; shelf.Response createJsonErrorResponse(int errorCode, String errorMessage) { - return shelf.Response(errorCode, body: jsonEncode({"message": errorMessage}), headers: {"Content-Type": 'application/json'}); + return shelf.Response(errorCode, + body: jsonEncode({"message": errorMessage}), headers: {"Content-Type": 'application/json'}); } shelf.Response createUnauthorizedResponse(String errorMessage) => createJsonErrorResponse(400, errorMessage); diff --git a/pubspec.lock b/pubspec.lock index a491852..b7b4b6e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -426,6 +426,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.2" + shelf_cors_headers: + dependency: "direct main" + description: + name: shelf_cors_headers + sha256: a127c80f99bbef3474293db67a7608e3a0f1f0fcdb171dad77fa9bd2cd123ae4 + url: "https://pub.dev" + source: hosted + version: "0.1.5" shelf_router: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b7bc43e..a3feaa8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: # api server shelf: ^1.4.2 shelf_router: ^1.1.4 + shelf_cors_headers: ^0.1.5 jaguar_jwt: ^3.0.0 dev_dependencies: From e6aca2133cb5fbe7bec387341ca36fc146ca79b8 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 1 Nov 2024 00:26:26 +0100 Subject: [PATCH 06/37] pubspec.yaml missing new line at the end of file --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index a3feaa8..41600ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: running_on_dart -version: 4.9.0 +version: 4.10.0 description: Discord Bot for nyxx development homepage: https://github.com/nyxx-discord/running_on_dart repository: https://github.com/nyxx-discord/running_on_dart From 141f36e93f97ad137106915a52e64ff3965f67d7 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 1 Nov 2024 11:45:51 +0100 Subject: [PATCH 07/37] Improvements for generating jwt token --- lib/src/api/api_server.dart | 22 ++++++++++++++++++++-- lib/src/api/jwt_middleware.dart | 4 ++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index 77829fb..77f7a65 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:injector/injector.dart'; import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/api/jwt_middleware.dart'; +import 'package:running_on_dart/src/api/utils.dart'; import 'package:running_on_dart/src/modules/bot_start_duration.dart'; import 'package:shelf_cors_headers/shelf_cors_headers.dart'; @@ -28,7 +29,7 @@ class WebServer { final requestBody = jsonDecode(await request.readAsString()) as Map; final authCode = requestBody['code']; - await http.post(Uri.https('discord.com', '/api/oauth2/token'), body: { + final response = await http.post(Uri.https('discord.com', '/api/oauth2/token'), body: { 'client_id': clientId, 'client_secret': clientSecret, 'redirect_uri': clientRedirectUri, @@ -38,7 +39,24 @@ class WebServer { 'Content-Type': 'application/x-www-form-urlencoded', }); - final jwtToken = generateJwtKey('test'); + if (response.statusCode != 200) { + return createJsonErrorResponse(400, "Cannot login through discord. Try again"); + } + + final responseBody = jsonDecode(response.body) as Map; + final accessToken = responseBody['access_token']; + + final authorizedUserResponse = await http.get(Uri.https('discord.com', '/api/oauth2/@me'), + headers: {"Accept": 'application/json', 'Authorization': 'Bearer $accessToken'}); + + if (authorizedUserResponse.statusCode != 200) { + return createJsonErrorResponse(400, "Cannot fetch user data from discord!"); + } + + final authorizedUserResponseBody = jsonDecode(authorizedUserResponse.body) as Map; + + final jwtToken = generateJwtKey(authorizedUserResponseBody['user']['id'], + authorizedUserResponseBody['user']['global_name'] ?? authorizedUserResponseBody['user']['username']); return shelf.Response.ok(jsonEncode({'token': jwtToken})); } diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart index b2bd375..1436e4f 100644 --- a/lib/src/api/jwt_middleware.dart +++ b/lib/src/api/jwt_middleware.dart @@ -7,8 +7,8 @@ final jwtKey = getEnv("JWT_SECRET"); class MissingPermissionsException implements Exception {} -String generateJwtKey(String subject) { - final jwtClaim = JwtClaim(subject: subject, maxAge: Duration(days: 1)); +String generateJwtKey(String discordUserId, String userName) { + final jwtClaim = JwtClaim(subject: discordUserId, maxAge: Duration(days: 1), payload: {"name": userName}); return issueJwtHS256(jwtClaim, jwtKey); } From 02f647dcaf97ef296ca34069893cf79431d4ac1c Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Fri, 1 Nov 2024 20:12:22 +0100 Subject: [PATCH 08/37] Bot info collection improvements --- lib/src/api/api_server.dart | 7 ++- lib/src/api/jwt_middleware.dart | 7 +-- lib/src/api/utils.dart | 4 ++ lib/src/commands/info.dart | 64 +++++++---------------- lib/src/init.dart | 4 +- lib/src/modules/tag.dart | 1 + lib/src/services/bot_info.dart | 92 +++++++++++++++++++++++++++++++++ 7 files changed, 125 insertions(+), 54 deletions(-) create mode 100644 lib/src/services/bot_info.dart diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index 77f7a65..18bada5 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -4,7 +4,7 @@ import 'package:injector/injector.dart'; import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/api/jwt_middleware.dart'; import 'package:running_on_dart/src/api/utils.dart'; -import 'package:running_on_dart/src/modules/bot_start_duration.dart'; +import 'package:running_on_dart/src/services/bot_info.dart'; import 'package:shelf_cors_headers/shelf_cors_headers.dart'; import 'package:shelf_router/shelf_router.dart' as shelf_router; @@ -19,10 +19,9 @@ final clientRedirectUri = getEnv('DISCORD_REDIRECT_URI'); class WebServer { Future _handleBotInfo(shelf.Request request) async { - final botStartDuration = Injector.appInstance.get(); + final botInfo = await Injector.appInstance.get().getCurrentBotInfo(); - return shelf.Response.ok(jsonEncode({"ok": true, 'uptime': botStartDuration.startDate.toIso8601String()}), - headers: {'Content-Type': 'application/json'}); + return createOkResponse(botInfo.toJson()); } Future _handleLogin(shelf.Request request) async { diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart index 1436e4f..31d4d00 100644 --- a/lib/src/api/jwt_middleware.dart +++ b/lib/src/api/jwt_middleware.dart @@ -7,14 +7,15 @@ final jwtKey = getEnv("JWT_SECRET"); class MissingPermissionsException implements Exception {} -String generateJwtKey(String discordUserId, String userName) { - final jwtClaim = JwtClaim(subject: discordUserId, maxAge: Duration(days: 1), payload: {"name": userName}); +String generateJwtKey(String discordUserId, String userName, {List requiredPermissions = const []}) { + final jwtClaim = JwtClaim( + subject: discordUserId, maxAge: Duration(days: 1), payload: {"name": userName, 'perms': requiredPermissions}); return issueJwtHS256(jwtClaim, jwtKey); } void validateClaims(JwtClaim jwt, List requiredPermissions) { - final permissions = jwt.payload['permissions'] as List? ?? []; + final permissions = jwt.payload['perms'] as List? ?? []; final valid = Set.of(permissions).containsAll(requiredPermissions); if (!valid) { diff --git a/lib/src/api/utils.dart b/lib/src/api/utils.dart index 7a8e3c6..9ca35e8 100644 --- a/lib/src/api/utils.dart +++ b/lib/src/api/utils.dart @@ -7,6 +7,10 @@ shelf.Response createJsonErrorResponse(int errorCode, String errorMessage) { body: jsonEncode({"message": errorMessage}), headers: {"Content-Type": 'application/json'}); } +shelf.Response createOkResponse(Object? body) { + return shelf.Response.ok(jsonEncode(body), headers: {"Content-Type": 'application/json'}); +} + shelf.Response createUnauthorizedResponse(String errorMessage) => createJsonErrorResponse(400, errorMessage); shelf.Response createForbiddenResponse() => shelf.Response.forbidden(null); diff --git a/lib/src/commands/info.dart b/lib/src/commands/info.dart index 7369b19..36c0cba 100644 --- a/lib/src/commands/info.dart +++ b/lib/src/commands/info.dart @@ -2,71 +2,43 @@ import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; import 'package:nyxx_commands/nyxx_commands.dart'; import 'package:nyxx_extensions/nyxx_extensions.dart'; -import 'package:running_on_dart/src/modules/bot_start_duration.dart'; -import 'package:running_on_dart/src/modules/docs.dart'; -import 'package:running_on_dart/src/modules/reminder.dart'; -import 'package:running_on_dart/src/modules/tag.dart'; -import 'package:running_on_dart/src/settings.dart'; +import 'package:running_on_dart/src/services/bot_info.dart'; import 'package:running_on_dart/src/util/util.dart'; final info = ChatCommand( 'info', 'Get info about the bot', id('info', (ChatContext context) async { - final color = getRandomColor(); final currentUser = await context.client.user.get(); + final botInfo = await Injector.appInstance.get().getCurrentBotInfo(); - final startDate = Injector.appInstance.get().startDate; final startDateStr = - "${startDate.format(TimestampStyle.longDateTime)} (${startDate.format(TimestampStyle.relativeTime)})"; - - final docsUpdatedDate = Injector.appInstance.get().lastUpdate; - final docsUpdateStr = docsUpdatedDate != null - ? "${docsUpdatedDate.format(TimestampStyle.longDateTime)} (${docsUpdatedDate.format(TimestampStyle.relativeTime)})" + "${botInfo.uptime.format(TimestampStyle.longDateTime)} (${botInfo.uptime.format(TimestampStyle.relativeTime)})"; + final docsUpdateStr = botInfo.docsUpdate != null + ? "${botInfo.docsUpdate!.format(TimestampStyle.longDateTime)} (${botInfo.docsUpdate!.format(TimestampStyle.relativeTime)})" : "Never"; final embed = EmbedBuilder( - color: color, + color: getRandomColor(), author: EmbedAuthorBuilder( name: currentUser.username, iconUrl: currentUser.avatar.url, url: Uri.parse(ApiOptions.nyxxRepositoryUrl), ), footer: EmbedFooterBuilder( - text: 'nyxx ${ApiOptions.nyxxVersion}' - ' | ROD $version' - ' | Dart SDK ${getDartPlatform()}'), + text: 'nyxx ${botInfo.nyxxVersion}' + ' | ROD ${botInfo.version}' + ' | Dart SDK ${botInfo.dartPlatform}'), fields: [ - EmbedFieldBuilder(name: 'Cached guilds', value: context.client.guilds.cache.length.toString(), isInline: true), - EmbedFieldBuilder(name: 'Cached users', value: context.client.users.cache.length.toString(), isInline: true), - EmbedFieldBuilder( - name: 'Cached channels', value: context.client.channels.cache.length.toString(), isInline: true), - EmbedFieldBuilder( - name: 'Cached voice states', - value: context.client.guilds.cache.values - .map((g) => g.voiceStates.length) - .fold(0, (value, element) => value + element) - .toString(), - isInline: true), - EmbedFieldBuilder(name: 'Shard count', value: context.client.gateway.shards.length.toString(), isInline: true), - EmbedFieldBuilder( - name: 'Cached messages', - value: context.client.channels.cache.values - .whereType() - .map((c) => c.messages.cache.length) - .fold(0, (value, element) => value + element) - .toString(), - isInline: true), - EmbedFieldBuilder(name: 'Memory usage (current/RSS)', value: getCurrentMemoryString(), isInline: true), - EmbedFieldBuilder( - name: 'Tags in guild', - value: - Injector.appInstance.get().countCachedTags(context.guild?.id ?? context.user.id).toString(), - isInline: true), - EmbedFieldBuilder( - name: 'Current reminders', - value: Injector.appInstance.get().reminders.length.toString(), - isInline: true), + EmbedFieldBuilder(name: 'Cached guilds', value: botInfo.cachedGuilds.toString(), isInline: true), + EmbedFieldBuilder(name: 'Cached users', value: botInfo.cachedUsers.toString(), isInline: true), + EmbedFieldBuilder(name: 'Cached channels', value: botInfo.cachedChannels.toString(), isInline: true), + EmbedFieldBuilder(name: 'Cached voice states', value: botInfo.cachedVoiceStates.toString(), isInline: true), + EmbedFieldBuilder(name: 'Shard count', value: botInfo.shardCount.toString(), isInline: true), + EmbedFieldBuilder(name: 'Cached messages', value: botInfo.cachedMessages.toString(), isInline: true), + EmbedFieldBuilder(name: 'Memory usage (current/RSS)', value: botInfo.memoryUserString, isInline: true), + EmbedFieldBuilder(name: 'Tags in guild', value: botInfo.totalTagsCount.toString(), isInline: true), + EmbedFieldBuilder(name: 'Current reminders', value: botInfo.totalRemainderCount.toString(), isInline: true), EmbedFieldBuilder(name: 'Uptime', value: startDateStr, isInline: false), EmbedFieldBuilder(name: 'Docs Update', value: docsUpdateStr, isInline: false), ], diff --git a/lib/src/init.dart b/lib/src/init.dart index 485ffe1..0a9ac16 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -16,6 +16,7 @@ import 'package:running_on_dart/src/repository/jellyfin_config.dart'; import 'package:running_on_dart/src/repository/kavita.dart'; import 'package:running_on_dart/src/repository/reminder.dart'; import 'package:running_on_dart/src/repository/tag.dart'; +import 'package:running_on_dart/src/services/bot_info.dart'; import 'package:running_on_dart/src/services/db.dart'; import 'package:running_on_dart/src/modules/feature_settings.dart'; @@ -39,7 +40,8 @@ Future setupContainer(NyxxGateway client) async { ..registerSingleton(() => JellyfinModuleV2()) ..registerSingleton(() => MentionsMonitoringModule()) ..registerSingleton(() => KavitaModule()) - ..registerSingleton(() => EmojiReactModule()); + ..registerSingleton(() => EmojiReactModule()) + ..registerSingleton(() => BotInfoService()); await Injector.appInstance.get().init(); await Injector.appInstance.get().init(); diff --git a/lib/src/modules/tag.dart b/lib/src/modules/tag.dart index 98daf64..b9f89ab 100644 --- a/lib/src/modules/tag.dart +++ b/lib/src/modules/tag.dart @@ -38,6 +38,7 @@ class TagModule implements RequiresInitialization { } int countCachedTags(Snowflake targetId) => tags.where((tag) => tag.guildId == targetId).length; + int countTags() => tags.length; /// Get all the enabled tags in a guild. Iterable getGuildTags(Snowflake guildId) => tags.where((tag) => tag.guildId == guildId && tag.enabled); diff --git a/lib/src/services/bot_info.dart b/lib/src/services/bot_info.dart new file mode 100644 index 0000000..354041b --- /dev/null +++ b/lib/src/services/bot_info.dart @@ -0,0 +1,92 @@ +import 'package:injector/injector.dart'; +import 'package:nyxx/nyxx.dart'; +import 'package:running_on_dart/running_on_dart.dart'; +import 'package:running_on_dart/src/modules/bot_start_duration.dart'; + +import 'package:running_on_dart/src/settings.dart' as settings; +import 'package:running_on_dart/src/util/util.dart'; + +class BotInfo { + String get nyxxVersion => ApiOptions.nyxxVersion; + String get version => settings.version; + String get dartPlatform => getDartPlatform(); + String get memoryUserString => getCurrentMemoryString(); + + final int cachedGuilds; + final int cachedUsers; + final int cachedChannels; + final int cachedVoiceStates; + final int shardCount; + final int cachedMessages; + final int totalTagsCount; + final int totalRemainderCount; + final DateTime uptime; + final DateTime? docsUpdate; + + BotInfo( + {required this.cachedGuilds, + required this.cachedUsers, + required this.cachedChannels, + required this.cachedVoiceStates, + required this.shardCount, + required this.cachedMessages, + required this.totalTagsCount, + required this.totalRemainderCount, + required this.uptime, + required this.docsUpdate}); + + Map toJson() => { + 'nyxx_version': nyxxVersion, + 'version': version, + 'platform': dartPlatform, + 'memory_usage_string': memoryUserString, + 'cached_guilds': cachedGuilds, + 'cached_users': cachedUsers, + 'cached_voice_states': cachedVoiceStates, + 'shard_count': shardCount, + 'total_tags_count': totalTagsCount, + 'total_reminder_count': totalRemainderCount, + 'uptime': uptime.toIso8601String(), + 'docs_update': docsUpdate?.toIso8601String(), + }; +} + +class BotInfoService { + final NyxxGateway client = Injector.appInstance.get(); + final TagModule tagModule = Injector.appInstance.get(); + final ReminderModule reminderModule = Injector.appInstance.get(); + final BotStartDuration startDurationModule = Injector.appInstance.get(); + final DocsModule docsModule = Injector.appInstance.get(); + + Future getCurrentBotInfo() async { + final cachedGuilds = client.guilds.cache.length; + final cachedUsers = client.users.cache.length; + final cachedChannels = client.channels.cache.length; + final cachedVoiceStates = client.guilds.cache.values + .map((g) => g.voiceStates.length) + .fold(0, (value, element) => value + element) + .ceil(); + final shardCount = client.gateway.shards.length; + final cachedMessages = client.channels.cache.values + .whereType() + .map((c) => c.messages.cache.length) + .fold(0, (value, element) => value + element) + .ceil(); + final totalTags = tagModule.countTags(); + final totalReminders = reminderModule.reminders.length; + final botStartDateTime = startDurationModule.startDate; + final docsUpdateDateTime = docsModule.lastUpdate; + + return BotInfo( + cachedGuilds: cachedGuilds, + cachedUsers: cachedUsers, + cachedChannels: cachedChannels, + cachedVoiceStates: cachedVoiceStates, + shardCount: shardCount, + cachedMessages: cachedMessages, + totalTagsCount: totalTags, + totalRemainderCount: totalReminders, + uptime: botStartDateTime, + docsUpdate: docsUpdateDateTime); + } +} From a649cca8442d80afc9b517a6fcfdad1ec0183073 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 2 Nov 2024 03:36:24 +0100 Subject: [PATCH 09/37] Fix missing public bot info fields --- lib/src/services/bot_info.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/services/bot_info.dart b/lib/src/services/bot_info.dart index 354041b..6341186 100644 --- a/lib/src/services/bot_info.dart +++ b/lib/src/services/bot_info.dart @@ -40,6 +40,8 @@ class BotInfo { 'version': version, 'platform': dartPlatform, 'memory_usage_string': memoryUserString, + 'cached_channels': cachedChannels, + 'cached_messages': cachedMessages, 'cached_guilds': cachedGuilds, 'cached_users': cachedUsers, 'cached_voice_states': cachedVoiceStates, From 0a887fa24a962da648cc6a0613b358ad1ee9d619 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 2 Nov 2024 04:18:22 +0100 Subject: [PATCH 10/37] Improve handling of endpoint permissions --- lib/src/api/api_server.dart | 29 +++++++++++++++++++++++++---- lib/src/api/jwt_middleware.dart | 9 ++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index 18bada5..2263411 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:injector/injector.dart'; +import 'package:nyxx/nyxx.dart'; import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/api/jwt_middleware.dart'; import 'package:running_on_dart/src/api/utils.dart'; @@ -17,6 +18,14 @@ final clientId = getEnv('DISCORD_CLIENT_ID'); final clientSecret = getEnv('DISCORD_CLIENT_SECRET'); final clientRedirectUri = getEnv('DISCORD_REDIRECT_URI'); +enum WebApiPermission { + guilds('G'); + + final String name; + + const WebApiPermission(this.name); +} + class WebServer { Future _handleBotInfo(shelf.Request request) async { final botInfo = await Injector.appInstance.get().getCurrentBotInfo(); @@ -24,6 +33,18 @@ class WebServer { return createOkResponse(botInfo.toJson()); } + Future _handleGuildInfo(shelf.Request request) async { + final outputJson = Injector.appInstance.get().guilds.cache.values.map((guild) { + return { + "id": guild.id.toString(), + "name": guild.name, + "cached_members": guild.members.cache.length, + }; + }); + + return createOkResponse(outputJson.toList()); + } + Future _handleLogin(shelf.Request request) async { final requestBody = jsonDecode(await request.readAsString()) as Map; final authCode = requestBody['code']; @@ -63,12 +84,12 @@ class WebServer { Future _setupRouter() async { return shelf_router.Router() ..get("/api/info", _handleBotInfo) - ..post("/api/login", _handleLogin); - // ..get("/api/test", _authorized(_handleBotInfo)); + ..post("/api/login", _handleLogin) + ..get("/api/guilds", _authorized(_handleGuildInfo, [WebApiPermission.guilds])); } - shelf.Handler _authorized(shelf.Handler inner) => - const shelf.Pipeline().addMiddleware(jwtMiddleware()).addHandler(inner); + shelf.Handler _authorized(shelf.Handler inner, [List requiredRoles = const []]) => + shelf.Pipeline().addMiddleware(jwtMiddleware(requiredRoles.map((e) => e.name).toList())).addHandler(inner); Future startServer() async { final router = await _setupRouter(); diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart index 31d4d00..9167f0f 100644 --- a/lib/src/api/jwt_middleware.dart +++ b/lib/src/api/jwt_middleware.dart @@ -7,16 +7,15 @@ final jwtKey = getEnv("JWT_SECRET"); class MissingPermissionsException implements Exception {} -String generateJwtKey(String discordUserId, String userName, {List requiredPermissions = const []}) { - final jwtClaim = JwtClaim( - subject: discordUserId, maxAge: Duration(days: 1), payload: {"name": userName, 'perms': requiredPermissions}); +String generateJwtKey(String discordUserId, String userName, {List perms = const []}) { + final jwtClaim = + JwtClaim(subject: discordUserId, maxAge: Duration(days: 1), payload: {"name": userName, 'perms': perms}); return issueJwtHS256(jwtClaim, jwtKey); } void validateClaims(JwtClaim jwt, List requiredPermissions) { - final permissions = jwt.payload['perms'] as List? ?? []; - + final permissions = (jwt.payload['perms'] as List? ?? []).cast(); final valid = Set.of(permissions).containsAll(requiredPermissions); if (!valid) { throw MissingPermissionsException(); From 5044d9762af53f4f9c2377c46374f773d9855709 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sat, 2 Nov 2024 14:06:40 +0100 Subject: [PATCH 11/37] Send user data with token --- lib/src/api/api_server.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index 2263411..0cc75bd 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -75,10 +75,18 @@ class WebServer { final authorizedUserResponseBody = jsonDecode(authorizedUserResponse.body) as Map; - final jwtToken = generateJwtKey(authorizedUserResponseBody['user']['id'], - authorizedUserResponseBody['user']['global_name'] ?? authorizedUserResponseBody['user']['username']); - - return shelf.Response.ok(jsonEncode({'token': jwtToken})); + final userName = + authorizedUserResponseBody['user']['global_name'] ?? authorizedUserResponseBody['user']['username']; + final jwtToken = generateJwtKey(authorizedUserResponseBody['user']['id'], userName); + + return createOkResponse({ + 'token': jwtToken, + 'user': { + 'id': authorizedUserResponseBody['user']['id'], + 'name': userName, + 'avatar_hash': authorizedUserResponseBody['user']['avatar'], + }, + }); } Future _setupRouter() async { From 84f536521ef0eee549a4b918881f1201ad302a92 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Sun, 3 Nov 2024 00:51:51 +0100 Subject: [PATCH 12/37] Extend guild data --- lib/src/api/api_server.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index 0cc75bd..e6a3cf3 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -37,8 +37,11 @@ class WebServer { final outputJson = Injector.appInstance.get().guilds.cache.values.map((guild) { return { "id": guild.id.toString(), + "icon_hash": guild.iconHash, + "banner_hash": guild.bannerHash, "name": guild.name, "cached_members": guild.members.cache.length, + "cached_roles": guild.roles.cache.length, }; }); From 201d2a4df42b96aceb859aabfde299719002127d Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Mon, 18 Nov 2024 15:39:11 +0100 Subject: [PATCH 13/37] Initial commit --- lib/src/api/api_server.dart | 107 +++++----------- lib/src/api/jwt_middleware.dart | 47 ------- lib/src/services/bot_info.dart | 4 +- pubspec.lock | 210 +++++++++++++++++++++++++++----- pubspec.yaml | 4 +- templates/index.html | 10 ++ 6 files changed, 227 insertions(+), 155 deletions(-) delete mode 100644 lib/src/api/jwt_middleware.dart create mode 100644 templates/index.html diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index e6a3cf3..007fdc9 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -1,31 +1,20 @@ -import 'dart:convert'; +import 'dart:io'; import 'package:injector/injector.dart'; -import 'package:nyxx/nyxx.dart'; +import 'package:mustachex/mustachex.dart'; import 'package:running_on_dart/running_on_dart.dart'; -import 'package:running_on_dart/src/api/jwt_middleware.dart'; import 'package:running_on_dart/src/api/utils.dart'; import 'package:running_on_dart/src/services/bot_info.dart'; - import 'package:shelf_cors_headers/shelf_cors_headers.dart'; + import 'package:shelf_router/shelf_router.dart' as shelf_router; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; -import 'package:http/http.dart' as http; - final clientId = getEnv('DISCORD_CLIENT_ID'); final clientSecret = getEnv('DISCORD_CLIENT_SECRET'); final clientRedirectUri = getEnv('DISCORD_REDIRECT_URI'); -enum WebApiPermission { - guilds('G'); - - final String name; - - const WebApiPermission(this.name); -} - class WebServer { Future _handleBotInfo(shelf.Request request) async { final botInfo = await Injector.appInstance.get().getCurrentBotInfo(); @@ -33,74 +22,40 @@ class WebServer { return createOkResponse(botInfo.toJson()); } - Future _handleGuildInfo(shelf.Request request) async { - final outputJson = Injector.appInstance.get().guilds.cache.values.map((guild) { - return { - "id": guild.id.toString(), - "icon_hash": guild.iconHash, - "banner_hash": guild.bannerHash, - "name": guild.name, - "cached_members": guild.members.cache.length, - "cached_roles": guild.roles.cache.length, - }; - }); - - return createOkResponse(outputJson.toList()); - } - - Future _handleLogin(shelf.Request request) async { - final requestBody = jsonDecode(await request.readAsString()) as Map; - final authCode = requestBody['code']; + // Future _handleGuildInfo(shelf.Request request) async { + // final outputJson = Injector.appInstance.get().guilds.cache.values.map((guild) { + // return { + // "id": guild.id.toString(), + // "icon_hash": guild.iconHash, + // "banner_hash": guild.bannerHash, + // "name": guild.name, + // "cached_members": guild.members.cache.length, + // "cached_roles": guild.roles.cache.length, + // }; + // }); + // + // return createOkResponse(outputJson.toList()); + // } - final response = await http.post(Uri.https('discord.com', '/api/oauth2/token'), body: { - 'client_id': clientId, - 'client_secret': clientSecret, - 'redirect_uri': clientRedirectUri, - 'grant_type': 'authorization_code', - 'code': authCode, - }, headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }); - - if (response.statusCode != 200) { - return createJsonErrorResponse(400, "Cannot login through discord. Try again"); - } - - final responseBody = jsonDecode(response.body) as Map; - final accessToken = responseBody['access_token']; - - final authorizedUserResponse = await http.get(Uri.https('discord.com', '/api/oauth2/@me'), - headers: {"Accept": 'application/json', 'Authorization': 'Bearer $accessToken'}); - - if (authorizedUserResponse.statusCode != 200) { - return createJsonErrorResponse(400, "Cannot fetch user data from discord!"); - } - - final authorizedUserResponseBody = jsonDecode(authorizedUserResponse.body) as Map; + Future _setupRouter() async { + return shelf_router.Router() + ..get("/", (shelf.Request request) async { + final data = await Injector.appInstance.get().getCurrentBotInfo(); - final userName = - authorizedUserResponseBody['user']['global_name'] ?? authorizedUserResponseBody['user']['username']; - final jwtToken = generateJwtKey(authorizedUserResponseBody['user']['id'], userName); + var processor = MustachexProcessor(initialVariables: data.toJson()); - return createOkResponse({ - 'token': jwtToken, - 'user': { - 'id': authorizedUserResponseBody['user']['id'], - 'name': userName, - 'avatar_hash': authorizedUserResponseBody['user']['avatar'], - }, - }); - } + final templateData = await File("templates/index.html").readAsString(); + final rendered = await processor.process(templateData); - Future _setupRouter() async { - return shelf_router.Router() - ..get("/api/info", _handleBotInfo) - ..post("/api/login", _handleLogin) - ..get("/api/guilds", _authorized(_handleGuildInfo, [WebApiPermission.guilds])); + return shelf.Response.ok(rendered, headers: {"Content-Type": 'text/html'}); + }); + // ..get("/api/info", _handleBotInfo); + // ..post("/api/login", _handleLogin) + // ..get("/api/guilds", _authorized(_handleGuildInfo, [WebApiPermission.guilds])); } - shelf.Handler _authorized(shelf.Handler inner, [List requiredRoles = const []]) => - shelf.Pipeline().addMiddleware(jwtMiddleware(requiredRoles.map((e) => e.name).toList())).addHandler(inner); + shelf.Handler _authorized(shelf.Handler inner) => + shelf.Pipeline()/*.addMiddleware()*/.addHandler(inner); Future startServer() async { final router = await _setupRouter(); diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart deleted file mode 100644 index 9167f0f..0000000 --- a/lib/src/api/jwt_middleware.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:jaguar_jwt/jaguar_jwt.dart'; -import 'package:running_on_dart/running_on_dart.dart'; -import 'package:running_on_dart/src/api/utils.dart'; -import 'package:shelf/shelf.dart' as shelf; - -final jwtKey = getEnv("JWT_SECRET"); - -class MissingPermissionsException implements Exception {} - -String generateJwtKey(String discordUserId, String userName, {List perms = const []}) { - final jwtClaim = - JwtClaim(subject: discordUserId, maxAge: Duration(days: 1), payload: {"name": userName, 'perms': perms}); - - return issueJwtHS256(jwtClaim, jwtKey); -} - -void validateClaims(JwtClaim jwt, List requiredPermissions) { - final permissions = (jwt.payload['perms'] as List? ?? []).cast(); - final valid = Set.of(permissions).containsAll(requiredPermissions); - if (!valid) { - throw MissingPermissionsException(); - } -} - -shelf.Middleware jwtMiddleware([List requiredPermissions = const []]) => (shelf.Handler handler) { - return (shelf.Request request) { - final authHeader = request.headers['Authorization']; - - if (authHeader == null) { - return createUnauthorizedResponse('Missing authorization header'); - } - - try { - final jwt = verifyJwtHS256Signature(authHeader.replaceFirst('Bearer ', ''), jwtKey); - - if (requiredPermissions.isNotEmpty) { - validateClaims(jwt, requiredPermissions); - } - } on JwtException catch (e) { - return createUnauthorizedResponse(e.message); - } on MissingPermissionsException { - return createForbiddenResponse(); - } - - return handler(request); - }; - }; diff --git a/lib/src/services/bot_info.dart b/lib/src/services/bot_info.dart index 6341186..1950f13 100644 --- a/lib/src/services/bot_info.dart +++ b/lib/src/services/bot_info.dart @@ -1,7 +1,9 @@ import 'package:injector/injector.dart'; import 'package:nyxx/nyxx.dart'; -import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/modules/bot_start_duration.dart'; +import 'package:running_on_dart/src/modules/docs.dart'; +import 'package:running_on_dart/src/modules/reminder.dart'; +import 'package:running_on_dart/src/modules/tag.dart'; import 'package:running_on_dart/src/settings.dart' as settings; import 'package:running_on_dart/src/util/util.dart'; diff --git a/pubspec.lock b/pubspec.lock index b7b4b6e..4fa7999 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,14 +33,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" - auth_header: - dependency: transitive - description: - name: auth_header - sha256: "0a3938128b6124530de93ce1a20ccb58639195fe7952f638248ea1bc0e5408eb" - url: "https://pub.dev" - source: hosted - version: "3.0.1" boolean_selector: dependency: transitive description: @@ -93,10 +85,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: "direct main" description: @@ -113,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "4b03e11f6d5b8f6e5bb5e9f7889a56fe6c5cbe942da5378ea4d4d7f73ef9dfe5" + url: "https://pub.dev" + source: hosted + version: "1.11.0" crypto: dependency: transitive description: @@ -169,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" fuzzy: dependency: "direct main" description: @@ -209,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -237,18 +253,26 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "99f282cb0e02edcbbf8c6b3bbc7c90b65635156c412e58f3975a7e55284ce685" url: "https://pub.dev" source: hosted - version: "0.19.0" - jaguar_jwt: - dependency: "direct main" + version: "0.20.0" + io: + dependency: transitive description: - name: jaguar_jwt - sha256: c3ab24be5ba5f736f93eacfc94c4381bd93551a351ab5687e70c8d71a9916e8d + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" lints: dependency: "direct dev" description: @@ -269,10 +293,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.16+1" meta: dependency: transitive description: @@ -286,18 +310,50 @@ packages: description: path: "." ref: HEAD - resolved-ref: a31aaa35c975e8a6d17554d0241c11c297b7afb6 + resolved-ref: "82104b2934f7fa13a88e2cd596d4b4df932e6cd6" url: "https://github.com/nyxx-discord/migent.git" source: git - version: "1.1.0" + version: "1.2.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mustache_recase: + dependency: transitive + description: + name: mustache_recase + sha256: "63a339ebdc67305444e465519df27cbc5beb59fb69f7fe43c0f16c2b376a5d43" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + mustachex: + dependency: "direct main" + description: + name: mustachex + sha256: "6a9ea0861f3d8eb0db0862ee0dd3f1c6e4f623bb9fa47d13485c10f2fc946ca3" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" nyxx: dependency: "direct main" description: name: nyxx - sha256: "073c4f3a49269b4580c54aef32a472b3eae0e6f0e125eb1be2eeb5976210e886" + sha256: "3ee891f4098c0dd7b992dafc2b4e961b82bd076b3149a15408efda1e11c7c5aa" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.5.2" nyxx_commands: dependency: "direct main" description: @@ -310,10 +366,10 @@ packages: dependency: "direct main" description: name: nyxx_extensions - sha256: "8016605fc131320bfb58362e5124a0fb1c23e51c26f1638098611fa8f93760aa" + sha256: f0b6dfc5ed2350c10f3018c7fc5fbaa7c01fbce6fee8b8fb14724f57cc0d67fe url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" oauth2: dependency: transitive description: @@ -366,10 +422,10 @@ packages: dependency: "direct main" description: name: postgres - sha256: bc3a36f9960d822af1ac4c2e0a32c4e7a3e426d2ce4500c11afca40f53c34612 + sha256: e9802d4c9d78e432c4d4e57be9a91d99cf1552d45f17b17792f8e595060376a2 url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.3" pub_semver: dependency: transitive description: @@ -386,6 +442,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" retry: dependency: transitive description: @@ -434,6 +498,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.5" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" shelf_router: dependency: "direct main" description: @@ -442,6 +514,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -499,6 +603,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + url: "https://pub.dev" + source: hosted + version: "1.25.8" test_api: dependency: transitive description: @@ -507,6 +619,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.3" + test_core: + dependency: transitive + description: + name: test_core + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + url: "https://pub.dev" + source: hosted + version: "0.6.5" typed_data: dependency: transitive description: @@ -523,6 +643,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" watcher: dependency: transitive description: @@ -535,10 +663,34 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 41600ee..747a1f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,8 +34,8 @@ dependencies: # api server shelf: ^1.4.2 shelf_router: ^1.1.4 - shelf_cors_headers: ^0.1.5 - jaguar_jwt: ^3.0.0 + shelf_cors_headers: ^0.1.0 + mustachex: ^1.0.0 dev_dependencies: lints: ^5.0.0 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..8fc8d63 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,10 @@ + + + + + Test + + +

Memory usage: {{memory_usage_string}}

+ + From 843e30e2ab7a36c2acfed2f803e23d07f66972ad Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Mon, 18 Nov 2024 20:08:55 +0100 Subject: [PATCH 14/37] Implement session support --- lib/running_on_dart.dart | 2 +- lib/src/api/api_server.dart | 68 ------------------- lib/src/web_app/api_server.dart | 100 ++++++++++++++++++++++++++++ lib/src/{api => web_app}/utils.dart | 9 +++ pubspec.lock | 8 +++ pubspec.yaml | 1 + templates/index.html | 26 +++++++- 7 files changed, 144 insertions(+), 70 deletions(-) delete mode 100644 lib/src/api/api_server.dart create mode 100644 lib/src/web_app/api_server.dart rename lib/src/{api => web_app}/utils.dart (59%) diff --git a/lib/running_on_dart.dart b/lib/running_on_dart.dart index 080f796..25699cc 100644 --- a/lib/running_on_dart.dart +++ b/lib/running_on_dart.dart @@ -16,4 +16,4 @@ export 'src/converter.dart'; export 'src/error_handler.dart'; export 'src/init.dart'; -export 'src/api/api_server.dart' show WebServer; +export 'src/web_app/api_server.dart' show WebServer; diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart deleted file mode 100644 index 007fdc9..0000000 --- a/lib/src/api/api_server.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'dart:io'; - -import 'package:injector/injector.dart'; -import 'package:mustachex/mustachex.dart'; -import 'package:running_on_dart/running_on_dart.dart'; -import 'package:running_on_dart/src/api/utils.dart'; -import 'package:running_on_dart/src/services/bot_info.dart'; -import 'package:shelf_cors_headers/shelf_cors_headers.dart'; - -import 'package:shelf_router/shelf_router.dart' as shelf_router; -import 'package:shelf/shelf.dart' as shelf; -import 'package:shelf/shelf_io.dart' as shelf_io; - -final clientId = getEnv('DISCORD_CLIENT_ID'); -final clientSecret = getEnv('DISCORD_CLIENT_SECRET'); -final clientRedirectUri = getEnv('DISCORD_REDIRECT_URI'); - -class WebServer { - Future _handleBotInfo(shelf.Request request) async { - final botInfo = await Injector.appInstance.get().getCurrentBotInfo(); - - return createOkResponse(botInfo.toJson()); - } - - // Future _handleGuildInfo(shelf.Request request) async { - // final outputJson = Injector.appInstance.get().guilds.cache.values.map((guild) { - // return { - // "id": guild.id.toString(), - // "icon_hash": guild.iconHash, - // "banner_hash": guild.bannerHash, - // "name": guild.name, - // "cached_members": guild.members.cache.length, - // "cached_roles": guild.roles.cache.length, - // }; - // }); - // - // return createOkResponse(outputJson.toList()); - // } - - Future _setupRouter() async { - return shelf_router.Router() - ..get("/", (shelf.Request request) async { - final data = await Injector.appInstance.get().getCurrentBotInfo(); - - var processor = MustachexProcessor(initialVariables: data.toJson()); - - final templateData = await File("templates/index.html").readAsString(); - final rendered = await processor.process(templateData); - - return shelf.Response.ok(rendered, headers: {"Content-Type": 'text/html'}); - }); - // ..get("/api/info", _handleBotInfo); - // ..post("/api/login", _handleLogin) - // ..get("/api/guilds", _authorized(_handleGuildInfo, [WebApiPermission.guilds])); - } - - shelf.Handler _authorized(shelf.Handler inner) => - shelf.Pipeline()/*.addMiddleware()*/.addHandler(inner); - - Future startServer() async { - final router = await _setupRouter(); - - final app = - const shelf.Pipeline().addMiddleware(shelf.logRequests()).addMiddleware(corsHeaders()).addHandler(router.call); - - await shelf_io.serve(app, "0.0.0.0", 8088); - } -} diff --git a/lib/src/web_app/api_server.dart b/lib/src/web_app/api_server.dart new file mode 100644 index 0000000..41674a0 --- /dev/null +++ b/lib/src/web_app/api_server.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; + +import 'package:injector/injector.dart'; + +import 'package:running_on_dart/running_on_dart.dart'; +import 'package:running_on_dart/src/web_app/utils.dart'; +import 'package:running_on_dart/src/services/bot_info.dart'; +import 'package:shelf_cors_headers/shelf_cors_headers.dart'; + +import 'package:shelf_router/shelf_router.dart' as shelf_router; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; + +import 'package:http/http.dart' as http; +import 'package:shelf_session/cookies_middleware.dart'; +import 'package:shelf_session/session_middleware.dart'; + +final clientId = getEnv('DISCORD_CLIENT_ID'); +final clientSecret = getEnv('DISCORD_CLIENT_SECRET'); +final clientRedirectUri = getEnv('DISCORD_REDIRECT_URI'); + +class WebServer { + Future _handleIndex(shelf.Request request) async { + final data = await Injector.appInstance.get().getCurrentBotInfo(); + + return createTwigResponse("index.html", parameters: { + ...data.toJson(), + 'clientId': clientId, + 'redirectUri': clientRedirectUri, + 'user_data': Session.getSession(request)?.data['user_data'] ?? false, + }); + } + + Future _handleRedirect(shelf.Request request) async { + final authCode = request.url.queryParameters['code']; + + final tokenResponse = await http.post(Uri.https('discord.com', '/api/oauth2/token'), body: { + 'client_id': clientId, + 'client_secret': clientSecret, + 'redirect_uri': clientRedirectUri, + 'grant_type': 'authorization_code', + 'code': authCode, + }, headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }); + + final tokenBodyJson = jsonDecode(tokenResponse.body); + final token = tokenBodyJson['access_token']; + + print(tokenResponse.body); + print(tokenResponse.statusCode); + + final userData = await http.get(Uri.https('discord.com', '/api/oauth2/@me'), headers: { + "Accept": "application/json", + "Authorization": "Bearer $token", + }); + + final userDataJson = jsonDecode(userData.body); + print(userData.body); + print(userData.statusCode); + + var session = Session.getSession(request); + session ??= Session.createSession(request); + session.data['user_data'] = { + 'id': userDataJson['user']['id'], + 'name': userDataJson['user']['global_name'] ?? userDataJson['user']['username'], + 'avatar': userDataJson['user']['avatar'], + 'expires_at': userDataJson['expires'], + 'token': token, + }; + session.expires = DateTime.now().add(Duration(seconds: tokenBodyJson['expires_in'])); + + return shelf.Response.seeOther("/"); + } + + Future _handleLogOut(shelf.Request request) async { + Session.deleteSession(request); + + return shelf.Response.seeOther("/"); + } + + Future _setupRouter() async { + return shelf_router.Router() + ..get("/", _sessionAware(_handleIndex)) + ..get("/redirect", _sessionAware(_handleRedirect)) + ..get("/logout", _sessionAware(_handleLogOut)); + } + + shelf.Handler _sessionAware(shelf.Handler inner) => + shelf.Pipeline().addMiddleware(cookiesMiddleware()).addMiddleware(sessionMiddleware()).addHandler(inner); + + Future startServer() async { + final router = await _setupRouter(); + + final app = + const shelf.Pipeline().addMiddleware(shelf.logRequests()).addMiddleware(corsHeaders()).addHandler(router.call); + + await shelf_io.serve(app, "0.0.0.0", 8088); + } +} diff --git a/lib/src/api/utils.dart b/lib/src/web_app/utils.dart similarity index 59% rename from lib/src/api/utils.dart rename to lib/src/web_app/utils.dart index 9ca35e8..bccb208 100644 --- a/lib/src/api/utils.dart +++ b/lib/src/web_app/utils.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:mustachex/mustachex.dart'; import 'package:shelf/shelf.dart' as shelf; shelf.Response createJsonErrorResponse(int errorCode, String errorMessage) { @@ -11,6 +13,13 @@ shelf.Response createOkResponse(Object? body) { return shelf.Response.ok(jsonEncode(body), headers: {"Content-Type": 'application/json'}); } +Future createTwigResponse(String name, {Map? parameters}) async { + final processor = MustachexProcessor(initialVariables: parameters); + final templateData = await File("templates/$name").readAsString(); + + return shelf.Response.ok(await processor.process(templateData), headers: {"Content-Type": 'text/html'}); +} + shelf.Response createUnauthorizedResponse(String errorMessage) => createJsonErrorResponse(400, errorMessage); shelf.Response createForbiddenResponse() => shelf.Response.forbidden(null); diff --git a/pubspec.lock b/pubspec.lock index 4fa7999..7ee5071 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -514,6 +514,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.4" + shelf_session: + dependency: "direct main" + description: + name: shelf_session + sha256: "5c658d16ddac1ad364d047e9a22e1023fcb383f2f32d5b621feb57c6c3ca8748" + url: "https://pub.dev" + source: hosted + version: "0.1.2" shelf_static: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 747a1f8..40e857d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: shelf: ^1.4.2 shelf_router: ^1.1.4 shelf_cors_headers: ^0.1.0 + shelf_session: ^0.1.2 mustachex: ^1.0.0 dev_dependencies: diff --git a/templates/index.html b/templates/index.html index 8fc8d63..b5f57df 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3,8 +3,32 @@ Test + -

Memory usage: {{memory_usage_string}}

+ + +

Is logged in: {{user_data}}

From cb6b0760a0339a9a977e033519cd28b6a9f4ccc6 Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Mon, 18 Nov 2024 23:49:31 +0100 Subject: [PATCH 15/37] Refactor session handling; Implement main page statistics --- lib/src/web_app/api_server.dart | 23 ++------- lib/src/web_app/utils.dart | 21 ++++++++ templates/index.html | 86 +++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 22 deletions(-) diff --git a/lib/src/web_app/api_server.dart b/lib/src/web_app/api_server.dart index 41674a0..82a6c2f 100644 --- a/lib/src/web_app/api_server.dart +++ b/lib/src/web_app/api_server.dart @@ -27,7 +27,7 @@ class WebServer { ...data.toJson(), 'clientId': clientId, 'redirectUri': clientRedirectUri, - 'user_data': Session.getSession(request)?.data['user_data'] ?? false, + 'user_data': getUserDataFromSession(request) ?? false, }); } @@ -47,34 +47,19 @@ class WebServer { final tokenBodyJson = jsonDecode(tokenResponse.body); final token = tokenBodyJson['access_token']; - print(tokenResponse.body); - print(tokenResponse.statusCode); - final userData = await http.get(Uri.https('discord.com', '/api/oauth2/@me'), headers: { "Accept": "application/json", "Authorization": "Bearer $token", }); - final userDataJson = jsonDecode(userData.body); - print(userData.body); - print(userData.statusCode); - - var session = Session.getSession(request); - session ??= Session.createSession(request); - session.data['user_data'] = { - 'id': userDataJson['user']['id'], - 'name': userDataJson['user']['global_name'] ?? userDataJson['user']['username'], - 'avatar': userDataJson['user']['avatar'], - 'expires_at': userDataJson['expires'], - 'token': token, - }; - session.expires = DateTime.now().add(Duration(seconds: tokenBodyJson['expires_in'])); + + initSession(request, userDataJson, tokenBodyJson); return shelf.Response.seeOther("/"); } Future _handleLogOut(shelf.Request request) async { - Session.deleteSession(request); + deleteSession(request); return shelf.Response.seeOther("/"); } diff --git a/lib/src/web_app/utils.dart b/lib/src/web_app/utils.dart index bccb208..6fb0bb1 100644 --- a/lib/src/web_app/utils.dart +++ b/lib/src/web_app/utils.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:mustachex/mustachex.dart'; import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf_session/session_middleware.dart'; shelf.Response createJsonErrorResponse(int errorCode, String errorMessage) { return shelf.Response(errorCode, @@ -23,3 +24,23 @@ Future createTwigResponse(String name, {Map? pa shelf.Response createUnauthorizedResponse(String errorMessage) => createJsonErrorResponse(400, errorMessage); shelf.Response createForbiddenResponse() => shelf.Response.forbidden(null); + +void initSession(shelf.Request request, Map userDataJson, Map tokenDataJson) { + var session = Session.getSession(request); + session ??= Session.createSession(request); + session.data['user_data'] = { + 'id': userDataJson['user']['id'], + 'name': userDataJson['user']['global_name'] ?? userDataJson['user']['username'], + 'avatar': userDataJson['user']['avatar'], + 'expires_at': userDataJson['expires'], + 'token': tokenDataJson['access_token'], + }; + session.expires = DateTime.now().add(Duration(seconds: tokenDataJson['expires_in'])); +} + +void deleteSession(shelf.Request request) { + Session.deleteSession(request); +} + +Map? getUserDataFromSession(shelf.Request request) => + Session.getSession(request)?.data['user_data'] as Map?; diff --git a/templates/index.html b/templates/index.html index b5f57df..ad29661 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,20 +6,23 @@ - From 3c60c66651081c627deb7d031156b5706d765acc Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Wed, 20 Nov 2024 23:45:03 +0100 Subject: [PATCH 17/37] Rework frontend to bootstrap --- templates/index.html | 237 +++++++++++++++++++++++++------------------ 1 file changed, 141 insertions(+), 96 deletions(-) diff --git a/templates/index.html b/templates/index.html index 5cfb8a1..7649bab 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,124 +2,169 @@ - Test - + Running on Dart + + - -
-
- Experimental version -
-
+
+
+
{{cached_users}}
+
Cached Users
+
+
-
-

Cache Info

-
-
-

{{cached_channels}}

-

Cached channels

-
-
-

{{cached_messages}}

-

Cached messages

-
-
-

{{cached_guilds}}

-

Cached guilds

-
-
-

{{cached_users}}

-

Cached Users

-
-
-

{{cached_voice_states}}

-

Cached voice states

+
+
+
{{cached_voice_states}}
+
Cached voice states
+
+
-
- -
-

Modules Info

-
-
-

{{total_tags_count}}

-

Total tags

+
+
+

Modules Info

-
-

{{total_reminder_count}}

-

Total reminders

-
-
-

n/a

- -

Last docs update

-
-
-
+
+
+
+
{{total_tags_count}}
+
Total tags
+
+
-
-

Program Info

-
-
-

{{nyxx_version}}

-

Nyxx Version

-
-
-

{{version}}

-

Bot Version

+
+
+
{{total_reminder_count}}
+
Total reminders
+
+
+ +
+
+ +
n/a
+
Last docs update
+
+
-
-

{{platform}}

-

Dart Version

+
+
+
+

Program Info

-
-

{{memory_usage_string}}

-

Memory usage

+
+
+
+
{{nyxx_version}}
+
Nyxx Version
+
+
+ +
+
+
{{version}}
+
Bot Version
+
+
+ +
+
+
{{platform}}
+
Dart Version
+
+
+ +
+
+
{{memory_usage_string}}
+
Memory usage
+
+
-
-

n/a

- -

Uptime

+
+
+
+ +
n/a
+
Uptime
+
+
- From d2856f74f5bb31c2cec8e76996306abde915c64b Mon Sep 17 00:00:00 2001 From: Szymon Uglis Date: Thu, 21 Nov 2024 17:09:49 +0100 Subject: [PATCH 18/37] Implement htmx; Implement guilds; Save sessions to file --- .gitignore | 1 + bin/running_on_dart.dart | 1 + docker-compose.yml | 2 + lib/running_on_dart.dart | 1 + lib/src/web_app/api_server.dart | 43 +++++++++++++++++++-- lib/src/web_app/session_manager_plugin.dart | 37 ++++++++++++++++++ lib/src/web_app/utils.dart | 6 +++ pubspec.lock | 9 +++-- pubspec.yaml | 3 +- templates/component/navigation.html | 40 +++++++++++++++++++ templates/guilds.html | 33 ++++++++++++++++ templates/index.html | 42 +------------------- 12 files changed, 170 insertions(+), 48 deletions(-) create mode 100644 lib/src/web_app/session_manager_plugin.dart create mode 100644 templates/component/navigation.html create mode 100644 templates/guilds.html diff --git a/.gitignore b/.gitignore index a36b221..9cd7f5f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ doc/api/ *.db lib_old/ +sessions/ diff --git a/bin/running_on_dart.dart b/bin/running_on_dart.dart index 575e311..421d97e 100644 --- a/bin/running_on_dart.dart +++ b/bin/running_on_dart.dart @@ -43,6 +43,7 @@ void main() async { IgnoreExceptions(), commands, pagination, + SessionManagerPlugin(), ], )); diff --git a/docker-compose.yml b/docker-compose.yml index 8db9f0a..7a96926 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: - "8088:8088" depends_on: - db + volumes: + - "./sessions:/sessions" db: image: postgres:17 diff --git a/lib/running_on_dart.dart b/lib/running_on_dart.dart index 25699cc..dc54e15 100644 --- a/lib/running_on_dart.dart +++ b/lib/running_on_dart.dart @@ -17,3 +17,4 @@ export 'src/error_handler.dart'; export 'src/init.dart'; export 'src/web_app/api_server.dart' show WebServer; +export 'src/web_app/session_manager_plugin.dart' show SessionManagerPlugin; diff --git a/lib/src/web_app/api_server.dart b/lib/src/web_app/api_server.dart index 37ae36e..4d62384 100644 --- a/lib/src/web_app/api_server.dart +++ b/lib/src/web_app/api_server.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:injector/injector.dart'; +import 'package:nyxx/nyxx.dart'; import 'package:running_on_dart/running_on_dart.dart'; import 'package:running_on_dart/src/web_app/utils.dart'; @@ -20,14 +21,48 @@ final clientSecret = getEnv('DISCORD_CLIENT_SECRET'); final clientRedirectUri = getEnv('DISCORD_REDIRECT_URI'); class WebServer { + Future _handleGuilds(shelf.Request request) async { + if (!isAdminFromSession(request)) { + return shelf.Response.forbidden(null); + } + + final client = Injector.appInstance.get(); + + final guildData = client.guilds.cache.values + .map((entry) => { + 'id': entry.id.toString(), + 'name': entry.name, + 'banner': entry.bannerHash, + 'icon': entry.iconHash, + 'cached_members': entry.members.cache.length, + 'cached_channels': + client.channels.cache.values.whereType().where((c) => c.guildId == entry.id).length, + }) + .toList(); + + return createTwigResponse("guilds.html", parameters: { + 'guilds': guildData, + }); + } + + Future _handleHx(shelf.Request request) async { + final templateName = request.url.queryParameters['c']; + if (templateName == null) { + return shelf.Response.badRequest(); + } + + return createTwigResponse('component/$templateName.html', parameters: { + ...getCustomDataFromSession(request), + 'clientId': clientId, + 'redirectUri': clientRedirectUri, + }); + } + Future _handleIndex(shelf.Request request) async { final data = await Injector.appInstance.get().getCurrentBotInfo(); return createTwigResponse("index.html", parameters: { ...data.toJson(), - 'clientId': clientId, - 'redirectUri': clientRedirectUri, - ...getCustomDataFromSession(request), }); } @@ -67,6 +102,8 @@ class WebServer { Future _setupRouter() async { return shelf_router.Router() ..get("/", _sessionAware(_handleIndex)) + ..get("/guilds", _sessionAware(_handleGuilds)) + ..get("/hx", _sessionAware(_handleHx)) ..get("/redirect", _sessionAware(_handleRedirect)) ..get("/logout", _sessionAware(_handleLogOut)); } diff --git a/lib/src/web_app/session_manager_plugin.dart b/lib/src/web_app/session_manager_plugin.dart new file mode 100644 index 0000000..16dc5f4 --- /dev/null +++ b/lib/src/web_app/session_manager_plugin.dart @@ -0,0 +1,37 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:nyxx/nyxx.dart'; +import 'package:shelf_session/session_middleware.dart'; + +class SessionManagerPlugin extends NyxxPlugin { + static const _sessionsFile = '/sessions/sessions.json'; + + void _restoreSessions() async { + restoreSessions(() async { + final file = File(_sessionsFile); + if (await file.exists()) { + logger.info("Loading session file."); + return File(_sessionsFile).readAsString(); + } + + logger.info("Session file missing. Returning default"); + return '{}'; + }); + } + + @override + FutureOr afterConnect(NyxxGateway client) { + _restoreSessions(); + + Timer.periodic(Duration(minutes: 15), (timer) => _restoreSessions()); + } + + @override + FutureOr afterClose() async { + await saveSessions((sessionData) async { + logger.info("Saving session file"); + await File(_sessionsFile).writeAsString(sessionData); + }); + } +} diff --git a/lib/src/web_app/utils.dart b/lib/src/web_app/utils.dart index b9fe9f0..f805c05 100644 --- a/lib/src/web_app/utils.dart +++ b/lib/src/web_app/utils.dart @@ -49,6 +49,12 @@ void deleteSession(shelf.Request request) { Session.deleteSession(request); } +bool isAdminFromSession(shelf.Request request) { + final session = Session.getSession(request); + + return session?.data['is_admin'] as bool? ?? false; +} + Map getCustomDataFromSession(shelf.Request request) { final session = Session.getSession(request); diff --git a/pubspec.lock b/pubspec.lock index c82c855..baa9cb8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -517,10 +517,11 @@ packages: shelf_session: dependency: "direct main" description: - name: shelf_session - sha256: "5c658d16ddac1ad364d047e9a22e1023fcb383f2f32d5b621feb57c6c3ca8748" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: "1cc10d0723ed7de0a75e44e896a7cd0380b4bd0f" + url: "https://github.com/l7ssha/shelf_session.git" + source: git version: "0.1.2" shelf_static: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index f943abf..34bb9c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,8 @@ dependencies: shelf: ^1.4.2 shelf_router: ^1.1.4 shelf_cors_headers: ^0.1.0 - shelf_session: ^0.1.2 + shelf_session: + git: https://github.com/l7ssha/shelf_session.git mustachex: ^1.0.0 dev_dependencies: diff --git a/templates/component/navigation.html b/templates/component/navigation.html new file mode 100644 index 0000000..2ab9a4f --- /dev/null +++ b/templates/component/navigation.html @@ -0,0 +1,40 @@ + diff --git a/templates/guilds.html b/templates/guilds.html new file mode 100644 index 0000000..749a508 --- /dev/null +++ b/templates/guilds.html @@ -0,0 +1,33 @@ + + + + + Running on Dart + + + + + + + +
+ + +
+
+ {{#guilds}} +
+ +
+
Profile Picture{{name}}
+

{{cached_members}} Cached members {{cached_channels}} Cached channels

+
+
+ {{/guilds}} +
+
+
+ + diff --git a/templates/index.html b/templates/index.html index 7649bab..5f2e8e0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,48 +5,10 @@ Running on Dart + - +