diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..e61a6c6 --- /dev/null +++ b/.env.dist @@ -0,0 +1,22 @@ +ROD_TOKEN= +ROD_INTENT_FEATURES_ENABLE= +ROD_ADMIN_IDS= +ROD_ADMIN_GUILD= +ROD_DEV= +ROD_DEV_GUILD_ID= +ROD_PREFIX= + +ROD_ENABLE_API_SERVER= +API_SERVER_HOST= +API_SERVER_PORT= + +DB_HOST= +DB_PORT= +POSTGRES_PASSWORD= +POSTGRES_USER= +POSTGRES_DB= + +JWT_SECRET= +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_REDIRECT_URI= diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d12b4cc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,80 @@ +name: Test + +on: + push: + branches: + - rewrite + pull_request: + +jobs: + analyze: + name: dart analyze + runs-on: ubuntu-latest + steps: + - name: Setup Dart Action + uses: dart-lang/setup-dart@v1 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + + - name: Install dependencies + run: dart pub get + + - name: Analyze project source + run: dart analyze + + fix: + name: dart fix + runs-on: ubuntu-latest + steps: + - name: Setup Dart Action + uses: dart-lang/setup-dart@v1 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + + - name: Install dependencies + run: dart pub get + + - name: Analyze project source + run: dart fix --dry-run + + format: + name: dart format + runs-on: ubuntu-latest + steps: + - name: Setup Dart Action + uses: dart-lang/setup-dart@v1 + + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: ${{ runner.os }}-pubspec-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-pubspec- + + - name: Install dependencies + run: dart pub get + + - name: Format + run: dart format --set-exit-if-changed -l120 . diff --git a/.gitignore b/.gitignore index a36b221..b9e43f8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ doc/api/ # dotenv environment variables file .env* +!.env.dist # IDE configuration folders .vscode/ diff --git a/Makefile b/Makefile index 63493f4..fde07a1 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,10 @@ format: ## Run dart format fix: ## Run dart fix dart fix --apply -fix-project: fix format ## Fix whole project +analyze: ## Run dart analyze + dart analyze + +fix-project: fix analyze format ## Fix whole project run: ## Run dev project - docker compose up --build \ No newline at end of file + docker compose up --build diff --git a/analysis_options.yaml b/analysis_options.yaml index 5075f41..d9ff023 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,3 +2,5 @@ include: package:lints/recommended.yaml analyzer: exclude: [build/**] + errors: + implementation_imports: ignore diff --git a/bin/console.dart b/bin/console.dart new file mode 100644 index 0000000..fe7f2cb --- /dev/null +++ b/bin/console.dart @@ -0,0 +1,32 @@ +import 'package:running_on_dart/src/api/jwt_middleware.dart'; + +enum Commands { + generateAdminJwt('generate-admin-jwt'); + + final String name; + const Commands(this.name); + + static Commands byName(String name) { + return values.firstWhere((e) => e.name == name); + } +} + +int main(List args) { + if (args.isEmpty) { + print("Available commands: \n${Commands.values.map((e) => e.name).join(", ")}"); + return -1; + } + + final command = Commands.byName(args[0]); + + switch (command) { + case Commands.generateAdminJwt: + print(generateJwtKey("0", "test-user")); + break; + default: + print("Command '$command' not recognized!"); + return -1; + } + + return 0; +} diff --git a/lib/src/api/api_server.dart b/lib/src/api/api_server.dart index e6a3cf3..41ec7ab 100644 --- a/lib/src/api/api_server.dart +++ b/lib/src/api/api_server.dart @@ -27,6 +27,8 @@ enum WebApiPermission { } class WebServer { + final Logger _logger = Logger('ROD.ApiServer'); + Future _handleBotInfo(shelf.Request request) async { final botInfo = await Injector.appInstance.get().getCurrentBotInfo(); @@ -103,11 +105,17 @@ class WebServer { shelf.Pipeline().addMiddleware(jwtMiddleware(requiredRoles.map((e) => e.name).toList())).addHandler(inner); Future startServer() async { + if (!enableApiServer) { + _logger.info("Api server disabled..."); + return; + } + 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); + _logger.info("Starting api server at `$apiServerHost:$apiServerPort`"); + await shelf_io.serve(app, apiServerHost, apiServerPort); } } diff --git a/lib/src/api/jwt_middleware.dart b/lib/src/api/jwt_middleware.dart index 9167f0f..3429a05 100644 --- a/lib/src/api/jwt_middleware.dart +++ b/lib/src/api/jwt_middleware.dart @@ -1,5 +1,7 @@ import 'package:jaguar_jwt/jaguar_jwt.dart'; +import 'package:nyxx/nyxx.dart'; import 'package:running_on_dart/running_on_dart.dart'; +import 'package:running_on_dart/src/api/api_server.dart'; import 'package:running_on_dart/src/api/utils.dart'; import 'package:shelf/shelf.dart' as shelf; @@ -7,9 +9,17 @@ 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}); +String generateJwtKey(String discordUserId, String userName) { + final perms = adminIds.contains(Snowflake.parse(discordUserId)) ? WebApiPermission.values.map((e) => e.name) : []; + + final jwtClaim = JwtClaim( + subject: discordUserId, + maxAge: Duration(days: 1), + payload: { + "name": userName, + 'perms': perms, + }, + ); return issueJwtHS256(jwtClaim, jwtKey); } diff --git a/lib/src/settings.dart b/lib/src/settings.dart index f1963df..b346dff 100644 --- a/lib/src/settings.dart +++ b/lib/src/settings.dart @@ -17,6 +17,13 @@ String getEnv(String key, [String? def]) => /// instead of throwing an exception. bool getEnvBool(String key, [bool? def]) => ['true', '1'].contains(getEnv(key, def?.toString()).toLowerCase()); +/// Get a [int] from an environment variable, throwing an exception if it is not set or if cannot parse env value to int. +/// +/// If [def] is provided and the environment variable [key] is not set, [def] will be returned +/// instead of throwing an exception. +int getEnvInt(String key, [int? def]) => + int.tryParse(getEnv(key, def?.toString())) ?? (throw Exception("Cannot parse `$key` env to int")); + /// The token to use for this instance. final String token = getEnv('ROD_TOKEN'); @@ -35,6 +42,15 @@ final List adminIds = getEnv('ROD_ADMIN_IDS').split(RegExp(r'\s+')).m /// The interval at which to update the docs cache. final Duration docsUpdateInterval = Duration(seconds: int.parse(getEnv('ROD_DOCS_UPDATE_INTERVAL', '86400'))); +/// Whether api server functionality should be enabled. +final bool enableApiServer = getEnvBool('ROD_ENABLE_API_SERVER', false); + +/// Api server host. Default 'localhost'. +final String apiServerHost = getEnv('API_SERVER_HOST', 'localhost'); + +/// Api server port. Default '8088'. +final int apiServerPort = getEnvInt('API_SERVER_PORT', 8088); + /// The packages to cache documentation for. final List docsPackages = getEnv('ROD_DOCS_PACKAGES', 'nyxx nyxx_commands nyxx_lavalink nyxx_extensions').split(RegExp(r'\s+'));