From 4b75e4811bc41d8401073fca3ba4e2eeb778c460 Mon Sep 17 00:00:00 2001 From: seniorswe Date: Fri, 19 Sep 2025 22:31:58 -0400 Subject: [PATCH] pre release updates --- README.md | 23 ++ backend-services/cookies.txt | 6 + backend-services/doorman.py | 2 + backend-services/proto/alerts_qlrsje_v3.proto | 16 + backend-services/proto/alerts_utz_v1.proto | 16 + backend-services/proto/alerts_xupo_v1.proto | 16 + backend-services/proto/crypto_xhyhse_v1.proto | 16 + .../proto/customers_jdqwhv_v2.proto | 16 + .../proto/inventory_pscsl_v1.proto | 16 + backend-services/proto/inventory_qhq_v3.proto | 16 + .../proto/inventory_srxncz_v3.proto | 16 + .../proto/inventory_zwrd_v2.proto | 16 + backend-services/proto/metrics_cfhe_v1.proto | 16 + backend-services/proto/metrics_jtfb_v2.proto | 16 + backend-services/proto/news_pvu_v1.proto | 16 + backend-services/proto/orders_owypz_v2.proto | 16 + .../proto/payments_fgabj_v3.proto | 16 + .../proto/payments_vxzhs_v2.proto | 16 + .../proto/recommendations_adury_v2.proto | 16 + backend-services/proto/weather_itjwo_v3.proto | 16 + backend-services/proto/weather_lgf_v3.proto | 16 + backend-services/routes/demo_routes.py | 53 +++ backend-services/routes/token_routes.py | 65 ++++ backend-services/services/token_service.py | 62 ++++ backend-services/utils/demo_seed_util.py | 320 ++++++++++++++++++ .../src/app/token-defs/[group]/page.tsx | 127 +++++++ web-client/src/app/token-defs/add/page.tsx | 78 +++++ web-client/src/app/token-defs/page.tsx | 179 ++++++++++ web-client/src/app/tokens/page.tsx | 44 +-- web-client/src/components/Layout.tsx | 1 + 30 files changed, 1212 insertions(+), 36 deletions(-) create mode 100644 backend-services/cookies.txt create mode 100644 backend-services/proto/alerts_qlrsje_v3.proto create mode 100644 backend-services/proto/alerts_utz_v1.proto create mode 100644 backend-services/proto/alerts_xupo_v1.proto create mode 100644 backend-services/proto/crypto_xhyhse_v1.proto create mode 100644 backend-services/proto/customers_jdqwhv_v2.proto create mode 100644 backend-services/proto/inventory_pscsl_v1.proto create mode 100644 backend-services/proto/inventory_qhq_v3.proto create mode 100644 backend-services/proto/inventory_srxncz_v3.proto create mode 100644 backend-services/proto/inventory_zwrd_v2.proto create mode 100644 backend-services/proto/metrics_cfhe_v1.proto create mode 100644 backend-services/proto/metrics_jtfb_v2.proto create mode 100644 backend-services/proto/news_pvu_v1.proto create mode 100644 backend-services/proto/orders_owypz_v2.proto create mode 100644 backend-services/proto/payments_fgabj_v3.proto create mode 100644 backend-services/proto/payments_vxzhs_v2.proto create mode 100644 backend-services/proto/recommendations_adury_v2.proto create mode 100644 backend-services/proto/weather_itjwo_v3.proto create mode 100644 backend-services/proto/weather_lgf_v3.proto create mode 100644 backend-services/routes/demo_routes.py create mode 100644 backend-services/utils/demo_seed_util.py create mode 100644 web-client/src/app/token-defs/[group]/page.tsx create mode 100644 web-client/src/app/token-defs/add/page.tsx create mode 100644 web-client/src/app/token-defs/page.tsx diff --git a/README.md b/README.md index b8c5670..28083bb 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,29 @@ Quick go-live checklist Optional: run `bash scripts/smoke.sh` (uses `BASE_URL`, `STARTUP_ADMIN_EMAIL`, `STARTUP_ADMIN_PASSWORD`). +## Demo Data Seeder +- Populate the platform with realistic random data (users, APIs, endpoints, roles, groups, tokens, subscriptions, logs, protos) for UI exploration. + +Run from repo root: + +``` +python backend-services/scripts/seed_demo_data.py --users 40 --apis 15 --endpoints 6 --protos 6 --logs 1500 +``` + +Flags: +- `--users` count (default 30) +- `--apis` count (default 12) +- `--endpoints` per-API (default 5) +- `--groups` extra groups (default 6) +- `--protos` proto files (default 5) +- `--logs` log lines to append (default 1000) +- `--seed` RNG seed for reproducibility + +Notes: +- Works with memory-only or MongoDB modes. Metrics (for the dashboard) are in-memory and will reset on server restart; re-run the seeder to repopulate. +- Admin user is preserved; additional roles/groups are added if missing. +- Proto files are written under `backend-services/proto/` (no external tooling required to view in UI). + ## License Information The contents of this repository are property of doorman.so. diff --git a/backend-services/cookies.txt b/backend-services/cookies.txt new file mode 100644 index 0000000..45f62a4 --- /dev/null +++ b/backend-services/cookies.txt @@ -0,0 +1,6 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_.localhost TRUE / FALSE 1758337273 access_token_cookie eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTc1ODMzNzI3MywianRpIjoiNGY1NGU0MDUtN2Q4Mi00NDE5LTg1NmQtNDUzYWUyMmNhMjNhIiwiYWNjZXNzZXMiOnsidWlfYWNjZXNzIjp0cnVlLCJtYW5hZ2VfdXNlcnMiOnRydWUsIm1hbmFnZV9hcGlzIjp0cnVlLCJtYW5hZ2VfZW5kcG9pbnRzIjp0cnVlLCJtYW5hZ2VfZ3JvdXBzIjp0cnVlLCJtYW5hZ2Vfcm9sZXMiOnRydWUsIm1hbmFnZV9yb3V0aW5ncyI6dHJ1ZSwibWFuYWdlX2dhdGV3YXkiOnRydWUsIm1hbmFnZV9zdWJzY3JpcHRpb25zIjp0cnVlLCJtYW5hZ2Vfc2VjdXJpdHkiOnRydWUsImV4cG9ydF9sb2dzIjp0cnVlLCJ2aWV3X2xvZ3MiOnRydWV9fQ.oQSyvnu4raM2X0C18H6Evr-_UgZ8rGbAHrtQNGxJTcI +.localhost TRUE / FALSE 1758337273 csrf_token 17996394-05ab-4cb2-a86b-ecc3b72f071d diff --git a/backend-services/doorman.py b/backend-services/doorman.py index 3dda7c5..37a69e9 100755 --- a/backend-services/doorman.py +++ b/backend-services/doorman.py @@ -38,6 +38,7 @@ from routes.dashboard_routes import dashboard_router from routes.memory_routes import memory_router from routes.security_routes import security_router from routes.token_routes import token_router +from routes.demo_routes import demo_router from routes.monitor_routes import monitor_router from utils.security_settings_util import load_settings, start_auto_save_task, stop_auto_save_task, get_cached_settings from utils.memory_dump_util import dump_memory_to_file, restore_memory_from_file, find_latest_dump_path @@ -423,6 +424,7 @@ doorman.include_router(memory_router, prefix="/platform", tags=["Memory"]) doorman.include_router(security_router, prefix="/platform", tags=["Security"]) doorman.include_router(monitor_router, prefix="/platform", tags=["Monitor"]) doorman.include_router(token_router, prefix="/platform/token", tags=["Token"]) +doorman.include_router(demo_router, prefix="/platform/demo", tags=["Demo"]) def start(): if os.path.exists(PID_FILE): diff --git a/backend-services/proto/alerts_qlrsje_v3.proto b/backend-services/proto/alerts_qlrsje_v3.proto new file mode 100644 index 0000000..a6f1f8e --- /dev/null +++ b/backend-services/proto/alerts_qlrsje_v3.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package alerts_qlrsje_v3; + +service AlertsQlrsjeService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/alerts_utz_v1.proto b/backend-services/proto/alerts_utz_v1.proto new file mode 100644 index 0000000..10920fc --- /dev/null +++ b/backend-services/proto/alerts_utz_v1.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package alerts_utz_v1; + +service AlertsUtzService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/alerts_xupo_v1.proto b/backend-services/proto/alerts_xupo_v1.proto new file mode 100644 index 0000000..30ecd00 --- /dev/null +++ b/backend-services/proto/alerts_xupo_v1.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package alerts_xupo_v1; + +service AlertsXupoService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/crypto_xhyhse_v1.proto b/backend-services/proto/crypto_xhyhse_v1.proto new file mode 100644 index 0000000..d5fc7d1 --- /dev/null +++ b/backend-services/proto/crypto_xhyhse_v1.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package crypto_xhyhse_v1; + +service CryptoXhyhseService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/customers_jdqwhv_v2.proto b/backend-services/proto/customers_jdqwhv_v2.proto new file mode 100644 index 0000000..e663ae8 --- /dev/null +++ b/backend-services/proto/customers_jdqwhv_v2.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package customers_jdqwhv_v2; + +service CustomersJdqwhvService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/inventory_pscsl_v1.proto b/backend-services/proto/inventory_pscsl_v1.proto new file mode 100644 index 0000000..641f0b2 --- /dev/null +++ b/backend-services/proto/inventory_pscsl_v1.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package inventory_pscsl_v1; + +service InventoryPscslService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/inventory_qhq_v3.proto b/backend-services/proto/inventory_qhq_v3.proto new file mode 100644 index 0000000..e6c3b65 --- /dev/null +++ b/backend-services/proto/inventory_qhq_v3.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package inventory_qhq_v3; + +service InventoryQhqService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/inventory_srxncz_v3.proto b/backend-services/proto/inventory_srxncz_v3.proto new file mode 100644 index 0000000..a1c59c3 --- /dev/null +++ b/backend-services/proto/inventory_srxncz_v3.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package inventory_srxncz_v3; + +service InventorySrxnczService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/inventory_zwrd_v2.proto b/backend-services/proto/inventory_zwrd_v2.proto new file mode 100644 index 0000000..fe2f332 --- /dev/null +++ b/backend-services/proto/inventory_zwrd_v2.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package inventory_zwrd_v2; + +service InventoryZwrdService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/metrics_cfhe_v1.proto b/backend-services/proto/metrics_cfhe_v1.proto new file mode 100644 index 0000000..c080bed --- /dev/null +++ b/backend-services/proto/metrics_cfhe_v1.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package metrics_cfhe_v1; + +service MetricsCfheService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/metrics_jtfb_v2.proto b/backend-services/proto/metrics_jtfb_v2.proto new file mode 100644 index 0000000..6c7750e --- /dev/null +++ b/backend-services/proto/metrics_jtfb_v2.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package metrics_jtfb_v2; + +service MetricsJtfbService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/news_pvu_v1.proto b/backend-services/proto/news_pvu_v1.proto new file mode 100644 index 0000000..1fa773b --- /dev/null +++ b/backend-services/proto/news_pvu_v1.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package news_pvu_v1; + +service NewsPvuService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/orders_owypz_v2.proto b/backend-services/proto/orders_owypz_v2.proto new file mode 100644 index 0000000..6aa829b --- /dev/null +++ b/backend-services/proto/orders_owypz_v2.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package orders_owypz_v2; + +service OrdersOwypzService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/payments_fgabj_v3.proto b/backend-services/proto/payments_fgabj_v3.proto new file mode 100644 index 0000000..6648fc8 --- /dev/null +++ b/backend-services/proto/payments_fgabj_v3.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package payments_fgabj_v3; + +service PaymentsFgabjService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/payments_vxzhs_v2.proto b/backend-services/proto/payments_vxzhs_v2.proto new file mode 100644 index 0000000..38dceca --- /dev/null +++ b/backend-services/proto/payments_vxzhs_v2.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package payments_vxzhs_v2; + +service PaymentsVxzhsService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/recommendations_adury_v2.proto b/backend-services/proto/recommendations_adury_v2.proto new file mode 100644 index 0000000..0a93b67 --- /dev/null +++ b/backend-services/proto/recommendations_adury_v2.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package recommendations_adury_v2; + +service RecommendationsAduryService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/weather_itjwo_v3.proto b/backend-services/proto/weather_itjwo_v3.proto new file mode 100644 index 0000000..0ebae91 --- /dev/null +++ b/backend-services/proto/weather_itjwo_v3.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package weather_itjwo_v3; + +service WeatherItjwoService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/proto/weather_lgf_v3.proto b/backend-services/proto/weather_lgf_v3.proto new file mode 100644 index 0000000..c487cb6 --- /dev/null +++ b/backend-services/proto/weather_lgf_v3.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package weather_lgf_v3; + +service WeatherLgfService { + rpc GetStatus (StatusRequest) returns (StatusReply) {} +} + +message StatusRequest { + string id = 1; +} + +message StatusReply { + string status = 1; + string message = 2; +} diff --git a/backend-services/routes/demo_routes.py b/backend-services/routes/demo_routes.py new file mode 100644 index 0000000..d52b039 --- /dev/null +++ b/backend-services/routes/demo_routes.py @@ -0,0 +1,53 @@ +""" +Protected demo seeding routes for populating the running server with dummy data. +Only available to users with 'manage_gateway' OR 'manage_tokens'. +""" + +from fastapi import APIRouter, Request +from models.response_model import ResponseModel +from utils.response_util import respond_rest +from utils.role_util import platform_role_required_bool +from utils.auth_util import auth_required +from utils.demo_seed_util import run_seed + +import uuid +import time +import logging + +demo_router = APIRouter() +logger = logging.getLogger("doorman.gateway") + +@demo_router.post("/seed", + description="Seed the running server with demo data", + response_model=ResponseModel +) +async def demo_seed(request: Request, + users: int = 40, + apis: int = 15, + endpoints: int = 6, + groups: int = 8, + protos: int = 6, + logs: int = 1500, + seed: int | None = None): + request_id = str(uuid.uuid4()) + start_time = time.time() * 1000 + try: + payload = await auth_required(request) + username = payload.get("sub") + logger.info(f"{request_id} | Username: {username} | From: {request.client.host}:{request.client.port}") + logger.info(f"{request_id} | Endpoint: {request.method} {str(request.url.path)}") + if not (await platform_role_required_bool(username, 'manage_gateway') or await platform_role_required_bool(username, 'manage_tokens')): + return respond_rest(ResponseModel( + status_code=403, + error_code='DEMO001', + error_message='Permission denied to run seeder' + )) + res = run_seed(users=users, apis=apis, endpoints=endpoints, groups=groups, protos=protos, logs=logs, seed=seed) + return respond_rest(ResponseModel(status_code=200, response=res, message='Seed completed')) + except Exception as e: + logger.error(f"{request_id} | Demo seed error: {str(e)}", exc_info=True) + return respond_rest(ResponseModel(status_code=500, error_code='DEMO999', error_message='Failed to seed demo data')) + finally: + end_time = time.time() * 1000 + logger.info(f"{request_id} | Total time: {str(end_time - start_time)}ms") + diff --git a/backend-services/routes/token_routes.py b/backend-services/routes/token_routes.py index 4edb461..aee51bf 100644 --- a/backend-services/routes/token_routes.py +++ b/backend-services/routes/token_routes.py @@ -24,6 +24,71 @@ token_router = APIRouter() logger = logging.getLogger("doorman.gateway") +@token_router.get("/defs", + description="List token definitions", + response_model=ResponseModel, + responses={ + 200: {"description": "Successful Response"} + } +) +async def list_token_definitions(request: Request, page: int = 1, page_size: int = 50): + request_id = str(uuid.uuid4()) + start_time = time.time() * 1000 + try: + payload = await auth_required(request) + username = payload.get("sub") + logger.info(f"{request_id} | Username: {username} | From: {request.client.host}:{request.client.port}") + logger.info(f"{request_id} | Endpoint: {request.method} {str(request.url.path)}") + if not await platform_role_required_bool(username, 'manage_tokens'): + return respond_rest(ResponseModel( + status_code=403, + error_code='TKN002', + error_message='Unable to retrieve tokens' + )) + return respond_rest(await TokenService.list_token_defs(page, page_size, request_id)) + except Exception as e: + logger.critical(f"{request_id} | Unexpected error: {str(e)}", exc_info=True) + return process_response(ResponseModel( + status_code=500, + response_headers={"request_id": request_id}, + error_code="GTW999", + error_message="An unexpected error occurred" + ).dict(), "rest") + finally: + end_time = time.time() * 1000 + logger.info(f"{request_id} | Total time: {str(end_time - start_time)}ms") + +@token_router.get("/defs/{api_token_group}", + description="Get a token definition", + response_model=ResponseModel, +) +async def get_token_definition(api_token_group: str, request: Request): + request_id = str(uuid.uuid4()) + start_time = time.time() * 1000 + try: + payload = await auth_required(request) + username = payload.get("sub") + logger.info(f"{request_id} | Username: {username} | From: {request.client.host}:{request.client.port}") + logger.info(f"{request_id} | Endpoint: {request.method} {str(request.url.path)}") + if not await platform_role_required_bool(username, 'manage_tokens'): + return respond_rest(ResponseModel( + status_code=403, + error_code='TKN002', + error_message='Unable to retrieve tokens' + )) + return respond_rest(await TokenService.get_token_def(api_token_group, request_id)) + except Exception as e: + logger.critical(f"{request_id} | Unexpected error: {str(e)}", exc_info=True) + return process_response(ResponseModel( + status_code=500, + response_headers={"request_id": request_id}, + error_code="GTW999", + error_message="An unexpected error occurred" + ).dict(), "rest") + finally: + end_time = time.time() * 1000 + logger.info(f"{request_id} | Total time: {str(end_time - start_time)}ms") + @token_router.post("", description="Create a token", response_model=ResponseModel, diff --git a/backend-services/services/token_service.py b/backend-services/services/token_service.py index d4f013f..855f605 100644 --- a/backend-services/services/token_service.py +++ b/backend-services/services/token_service.py @@ -192,6 +192,68 @@ class TokenService: error_message='Database error occurred while deleting token' ).dict() + @staticmethod + async def list_token_defs(page: int, page_size: int, request_id): + """List token definitions (masked), paginated.""" + logger.info(request_id + " | Listing token definitions") + try: + cursor = token_def_collection.find({}).sort('api_token_group', 1) + # In-memory collections support chaining skip/limit; pymongo cursor also does + if page and page_size: + cursor = cursor.skip(max((page - 1), 0) * page_size).limit(page_size) + items = [] + for doc in cursor: + if doc.get('_id'): + del doc['_id'] + items.append({ + 'api_token_group': doc.get('api_token_group'), + 'api_key_header': doc.get('api_key_header'), + # do not return key; just whether present + 'api_key_present': bool(doc.get('api_key')), + 'token_tiers': doc.get('token_tiers', []), + }) + return ResponseModel( + status_code=200, + response={'items': items} + ).dict() + except PyMongoError as e: + logger.error(request_id + f" | Token list failed with database error: {str(e)}") + return ResponseModel( + status_code=500, + error_code='TKN020', + error_message='Database error occurred while listing tokens' + ).dict() + + @staticmethod + async def get_token_def(api_token_group: str, request_id): + """Get a single token definition (masked).""" + logger.info(request_id + " | Getting token definition") + try: + doc = token_def_collection.find_one({'api_token_group': api_token_group}) + if not doc: + return ResponseModel( + status_code=404, + error_code='TKN021', + error_message='Token definition not found' + ).dict() + if doc.get('_id'): + del doc['_id'] + # mask key presence only + masked = { + 'api_token_group': doc.get('api_token_group'), + 'api_key_header': doc.get('api_key_header'), + 'api_key_present': bool(doc.get('api_key')), + 'token_tiers': doc.get('token_tiers', []), + } + return ResponseModel(status_code=200, response=masked).dict() + except PyMongoError as e: + logger.error(request_id + f" | Token fetch failed with database error: {str(e)}") + return ResponseModel( + status_code=500, + error_code='TKN022', + error_message='Database error occurred while retrieving token' + ).dict() + @staticmethod async def add_tokens(username: str, data: UserTokenModel, request_id): """Add or update a user's token balances for one or more groups.""" diff --git a/backend-services/utils/demo_seed_util.py b/backend-services/utils/demo_seed_util.py new file mode 100644 index 0000000..6ff0f2c --- /dev/null +++ b/backend-services/utils/demo_seed_util.py @@ -0,0 +1,320 @@ +from __future__ import annotations + +import os +import uuid +import random +import string +from datetime import datetime, timedelta + +from utils import password_util +from utils.database import ( + api_collection, + endpoint_collection, + group_collection, + role_collection, + subscriptions_collection, + token_def_collection, + user_token_collection, + user_collection, +) +from utils.metrics_util import metrics_store, MinuteBucket +from utils.token_util import encrypt_value + + +def _rand_choice(seq): + return random.choice(seq) + + +def _rand_word(min_len=4, max_len=10) -> str: + length = random.randint(min_len, max_len) + return ''.join(random.choices(string.ascii_lowercase, k=length)) + + +def _rand_name() -> str: + firsts = ["alex","casey","morgan","sam","taylor","riley","jamie","jordan","drew","quinn","kyle","parker","blake","devon"] + lasts = ["lee","kim","patel","garcia","nguyen","williams","brown","davis","miller","wilson","moore","taylor","thomas"] + return f"{_rand_choice(firsts)}.{_rand_choice(lasts)}" + + +def _rand_domain() -> str: + return _rand_choice(["example.com","acme.io","contoso.net","demo.dev"]) + + +def _rand_password() -> str: + upp = _rand_choice(string.ascii_uppercase) + low = ''.join(random.choices(string.ascii_lowercase, k=8)) + dig = ''.join(random.choices(string.digits, k=4)) + spc = _rand_choice('!@#$%^&*()-_=+[]{};:,.<>?/') + tail = ''.join(random.choices(string.ascii_letters + string.digits, k=6)) + raw = upp + low + dig + spc + tail + return ''.join(random.sample(raw, len(raw))) + + +def ensure_roles() -> list[str]: + roles = [("developer", dict(manage_apis=True, manage_endpoints=True, manage_subscriptions=True, manage_tokens=True, view_logs=True)), + ("analyst", dict(view_logs=True, export_logs=True)), + ("viewer", dict(view_logs=True)), + ("ops", dict(manage_gateway=True, view_logs=True, export_logs=True, manage_security=True))] + created = [] + for role_name, extra in roles: + if not role_collection.find_one({"role_name": role_name}): + doc = {"role_name": role_name, "role_description": f"{role_name} role"} + doc.update({k: bool(v) for k, v in extra.items()}) + role_collection.insert_one(doc) + created.append(role_name) + return ["admin", *created] + + +def seed_groups(n: int, api_keys: list[str]) -> list[str]: + names = [] + for i in range(n): + gname = f"team-{_rand_word(3,6)}-{i}" + if group_collection.find_one({"group_name": gname}): + names.append(gname) + continue + access = sorted(set(random.sample(api_keys, k=min(len(api_keys), random.randint(1, max(1, len(api_keys)//3))))) ) if api_keys else [] + group_collection.insert_one({"group_name": gname, "group_description": f"Auto group {gname}", "api_access": access}) + names.append(gname) + for base in ("ALL", "admin"): + if not group_collection.find_one({"group_name": base}): + group_collection.insert_one({"group_name": base, "group_description": f"{base} group", "api_access": []}) + if base not in names: + names.append(base) + return names + + +def seed_users(n: int, roles: list[str], groups: list[str]) -> list[str]: + usernames = [] + for i in range(n): + uname = f"{_rand_name()}_{i}" + email = f"{uname.replace('.', '_')}@{_rand_domain()}" + if user_collection.find_one({"username": uname}): + usernames.append(uname) + continue + hashed = password_util.hash_password(_rand_password()) + ugrps = sorted(set(random.sample(groups, k=min(len(groups), random.randint(1, min(3, max(1, len(groups)))))))) + role = _rand_choice(roles) + user_collection.insert_one({ + "username": uname, + "email": email, + "password": hashed, + "role": role, + "groups": ugrps, + "rate_limit_duration": random.randint(100, 10000), + "rate_limit_duration_type": _rand_choice(["minute","hour","day"]), + "throttle_duration": random.randint(1000, 100000), + "throttle_duration_type": _rand_choice(["second","minute"]), + "throttle_wait_duration": random.randint(100, 10000), + "throttle_wait_duration_type": _rand_choice(["seconds","minutes"]), + "custom_attributes": {"dept": _rand_choice(["sales","eng","support","ops"])}, + "active": True, + "ui_access": _rand_choice([True, False]) + }) + usernames.append(uname) + return usernames + + +def seed_apis(n: int, roles: list[str], groups: list[str]) -> list[tuple[str,str]]: + pairs = [] + for i in range(n): + name = _rand_choice(["customers","orders","billing","weather","news","crypto","search","inventory","shipping","payments","alerts","metrics","recommendations"]) + f"-{_rand_word(3,6)}" + ver = _rand_choice(["v1","v2","v3"]) + if api_collection.find_one({"api_name": name, "api_version": ver}): + pairs.append((name, ver)) + continue + api_id = str(uuid.uuid4()) + doc = { + "api_name": name, + "api_version": ver, + "api_description": f"Auto API {name}/{ver}", + "api_allowed_roles": sorted(set(random.sample(roles, k=min(len(roles), random.randint(1, min(3, len(roles))))))), + "api_allowed_groups": sorted(set(random.sample(groups, k=min(len(groups), random.randint(1, min(5, len(groups))))))), + "api_servers": [f"http://localhost:{8000+random.randint(0,999)}"], + "api_type": "REST", + "api_allowed_retry_count": random.randint(0, 3), + "api_id": api_id, + "api_path": f"/{name}/{ver}", + } + api_collection.insert_one(doc) + pairs.append((name, ver)) + return pairs + + +def seed_endpoints(apis: list[tuple[str,str]], per_api: int) -> None: + methods = ["GET","POST","PUT","DELETE","PATCH"] + bases = ["/status","/health","/items","/items/{id}","/search","/reports","/export","/metrics","/list","/detail/{id}"] + for (name, ver) in apis: + created = set() + for _ in range(per_api): + m = _rand_choice(methods) + u = _rand_choice(bases) + key = (m, u) + if key in created: + continue + created.add(key) + if endpoint_collection.find_one({"api_name": name, "api_version": ver, "endpoint_method": m, "endpoint_uri": u}): + continue + endpoint_collection.insert_one({ + "api_name": name, + "api_version": ver, + "endpoint_method": m, + "endpoint_uri": u, + "endpoint_description": f"{m} {u} for {name}", + "api_id": api_collection.find_one({"api_name": name, "api_version": ver}).get("api_id"), + "endpoint_id": str(uuid.uuid4()), + }) + + +def seed_tokens() -> list[str]: + groups = ["ai-basic","ai-pro","maps-basic","maps-pro","news-tier","weather-tier"] + tiers_catalog = [ + {"tier_name": "basic", "tokens": 100, "input_limit": 100, "output_limit": 100, "reset_frequency": "monthly"}, + {"tier_name": "pro", "tokens": 1000, "input_limit": 500, "output_limit": 500, "reset_frequency": "monthly"}, + {"tier_name": "enterprise", "tokens": 10000, "input_limit": 2000, "output_limit": 2000, "reset_frequency": "monthly"}, + ] + created = [] + for g in groups: + if token_def_collection.find_one({"api_token_group": g}): + created.append(g) + continue + tiers = random.sample(tiers_catalog, k=random.randint(1, 3)) + token_def_collection.insert_one({ + "api_token_group": g, + "api_key": encrypt_value(uuid.uuid4().hex), + "api_key_header": _rand_choice(["x-api-key","authorization","x-token"]), + "token_tiers": tiers, + }) + created.append(g) + return created + + +def seed_user_tokens(usernames: list[str], token_groups: list[str]) -> None: + pick_users = random.sample(usernames, k=min(len(usernames), max(1, len(usernames)//2))) if usernames else [] + for u in pick_users: + users_tokens = {} + for g in random.sample(token_groups, k=random.randint(1, min(3, len(token_groups)))): + users_tokens[g] = { + "tier_name": _rand_choice(["basic","pro","enterprise"]), + "available_tokens": random.randint(10, 10000), + "reset_date": (datetime.utcnow() + timedelta(days=random.randint(1, 30))).strftime("%Y-%m-%d"), + "user_api_key": encrypt_value(uuid.uuid4().hex), + } + existing = user_token_collection.find_one({"username": u}) + if existing: + user_token_collection.update_one({"username": u}, {"$set": {"users_tokens": users_tokens}}) + else: + user_token_collection.insert_one({"username": u, "users_tokens": users_tokens}) + + +def seed_subscriptions(usernames: list[str], apis: list[tuple[str,str]]) -> None: + api_keys = [f"{a}/{v}" for a, v in apis] + for u in usernames: + subs = sorted(set(random.sample(api_keys, k=random.randint(1, min(5, len(api_keys))))) ) if api_keys else [] + existing = subscriptions_collection.find_one({"username": u}) + if existing: + subscriptions_collection.update_one({"username": u}, {"$set": {"apis": subs}}) + else: + subscriptions_collection.insert_one({"username": u, "apis": subs}) + + +def seed_logs(n: int, usernames: list[str], apis: list[tuple[str,str]]) -> None: + base_dir = os.path.dirname(os.path.abspath(__file__)) + base_dir = os.path.abspath(os.path.join(base_dir, '..')) + logs_dir = os.path.join(base_dir, "logs") + os.makedirs(logs_dir, exist_ok=True) + log_path = os.path.join(logs_dir, "doorman.log") + methods = ["GET","POST","PUT","DELETE","PATCH"] + uris = ["/status","/list","/items","/items/123","/search?q=test","/export","/metrics"] + now = datetime.now() + with open(log_path, "a", encoding="utf-8") as lf: + for _ in range(n): + api = _rand_choice(apis) if apis else ("demo","v1") + method = _rand_choice(methods) + uri = _rand_choice(uris) + ts = (now - timedelta(seconds=random.randint(0, 3600))).strftime('%Y-%m-%d %H:%M:%S,%f')[:-3] + rid = str(uuid.uuid4()) + user = _rand_choice(usernames) if usernames else "admin" + port = random.randint(10000, 65000) + msg = f"{rid} | Username: {user} | From: 127.0.0.1:{port} | Endpoint: {method} /{api[0]}/{api[1]}{uri} | Total time: {random.randint(5,500)}ms" + lf.write(f"{ts} - doorman.gateway - INFO - {msg}\n") + + +def seed_protos(n: int, apis: list[tuple[str,str]]) -> None: + base_dir = os.path.dirname(os.path.abspath(__file__)) + base_dir = os.path.abspath(os.path.join(base_dir, '..')) + proto_dir = os.path.join(base_dir, "proto") + gen_dir = os.path.join(base_dir, "generated") + os.makedirs(proto_dir, exist_ok=True) + os.makedirs(gen_dir, exist_ok=True) + picked = random.sample(apis, k=min(n, len(apis))) if apis else [] + for name, ver in picked: + key = f"{name}_{ver}".replace('-', '_') + svc = ''.join([p.capitalize() for p in name.split('-')]) + content = f'''syntax = "proto3"; + +package {key}; + +service {svc}Service {{ + rpc GetStatus (StatusRequest) returns (StatusReply) {{}} +}} + +message StatusRequest {{ + string id = 1; +}} + +message StatusReply {{ + string status = 1; + string message = 2; +}} +''' + path = os.path.join(proto_dir, f"{key}.proto") + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + +def seed_metrics(usernames: list[str], apis: list[tuple[str,str]], minutes: int = 400) -> None: + now = datetime.utcnow() + for i in range(minutes, 0, -1): + minute_start = int(((now - timedelta(minutes=i)).timestamp()) // 60) * 60 + b = MinuteBucket(start_ts=minute_start) + count = random.randint(0, 50) + for _ in range(count): + dur = random.uniform(10, 400) + status = _rand_choice([200,200,200,201,204,400,401,403,404,500]) + b.add(dur, status) + metrics_store.total_requests += 1 + metrics_store.total_ms += dur + metrics_store.status_counts[status] += 1 + u = _rand_choice(usernames) if usernames else None + if u: + metrics_store.username_counts[u] += 1 + if apis: + metrics_store.api_counts[f"rest:{_rand_choice(apis)[0]}"] += 1 + metrics_store._buckets.append(b) + + +def run_seed(users=30, apis=12, endpoints=5, groups=6, protos=5, logs=1000, seed=None): + if seed is not None: + random.seed(seed) + roles = ensure_roles() + api_pairs = seed_apis(apis, roles, ["ALL","admin"]) + group_names = seed_groups(groups, [f"{a}/{v}" for a, v in api_pairs]) + usernames = seed_users(users, roles, group_names) + seed_endpoints(api_pairs, endpoints) + token_groups = seed_tokens() + seed_user_tokens(usernames, token_groups) + seed_subscriptions(usernames, api_pairs) + seed_logs(logs, usernames, api_pairs) + seed_protos(protos, api_pairs) + seed_metrics(usernames, api_pairs) + return { + 'users': user_collection.count_documents({}), + 'apis': api_collection.count_documents({}), + 'endpoints': endpoint_collection.count_documents({}), + 'groups': group_collection.count_documents({}), + 'roles': role_collection.count_documents({}), + 'subscriptions': subscriptions_collection.count_documents({}), + 'token_defs': token_def_collection.count_documents({}), + 'user_tokens': user_token_collection.count_documents({}), + } + diff --git a/web-client/src/app/token-defs/[group]/page.tsx b/web-client/src/app/token-defs/[group]/page.tsx new file mode 100644 index 0000000..a06e2ce --- /dev/null +++ b/web-client/src/app/token-defs/[group]/page.tsx @@ -0,0 +1,127 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import Link from 'next/link' +import { useParams, useRouter } from 'next/navigation' +import Layout from '@/components/Layout' +import { SERVER_URL } from '@/utils/config' +import { getJson, putJson, delJson } from '@/utils/api' +import { ProtectedRoute } from '@/components/ProtectedRoute' +import ConfirmModal from '@/components/ConfirmModal' + +export default function EditTokenDefPage() { + const params = useParams<{ group: string }>() + const router = useRouter() + const group = decodeURIComponent(params.group) + const [api_token_group] = useState(group) + const [api_key_header, setHeader] = useState('x-api-key') + const [api_key, setKey] = useState('') + const [tokenTiersText, setTiersText] = useState('[]') + const [working, setWorking] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + const [showDelete, setShowDelete] = useState(false) + const [confirmText, setConfirmText] = useState('') + + useEffect(() => { load() }, []) + const load = async () => { + try { + setError(null) + const res = await getJson(`${SERVER_URL}/platform/token/defs/${encodeURIComponent(group)}`) + const data = res?.response || res + setHeader(data.api_key_header || 'x-api-key') + setTiersText(JSON.stringify(data.token_tiers || [], null, 2)) + } catch (e:any) { + setError(e?.message || 'Failed to load token definition') + } + } + + const update = async () => { + setWorking(true); setError(null); setSuccess(null) + try { + const tiers = JSON.parse(tokenTiersText || '[]') + const payload: any = { api_token_group, api_key_header, token_tiers: tiers } + if (api_key) payload.api_key = api_key // set only if provided + await putJson(`${SERVER_URL}/platform/token/${encodeURIComponent(api_token_group)}`, payload) + setSuccess('Token definition updated') + setKey('') + } catch (e:any) { + setError(e?.message || 'Failed to update token definition') + } finally { + setWorking(false) + } + } + + const onDelete = async () => { + if (confirmText !== api_token_group) return + setWorking(true) + try { + await delJson(`${SERVER_URL}/platform/token/${encodeURIComponent(api_token_group)}`) + router.push('/token-defs') + } catch (e:any) { + setError(e?.message || 'Failed to delete token definition') + } finally { + setWorking(false) + } + } + + return ( + + +
+
+
+

Edit Token Definition

+

{api_token_group}

+
+ Back to Token Definitions +
+ +
+
+ {error &&
{error}
} + {success &&
{success}
} +
+
+ + +
+
+ + setHeader(e.target.value)} /> +
+
+ + setKey(e.target.value)} placeholder="sk_live_..." /> +
+
+ +