From 5dd93b79137e3815d48324366a0e3cb01f482d2e Mon Sep 17 00:00:00 2001 From: seniorswe Date: Mon, 19 Jan 2026 00:59:44 -0500 Subject: [PATCH 1/5] Enhancements to docs, search, and bug fixes. --- backend-services/routes/config_routes.py | 27 +-- backend-services/routes/gateway_routes.py | 72 ++++++ backend-services/routes/security_routes.py | 25 ++- .../routes/subscription_routes.py | 26 ++- backend-services/routes/user_routes.py | 2 +- backend-services/services/user_service.py | 8 +- backend-services/utils/subscription_util.py | 12 +- web-client/next.config.ts | 9 + web-client/public/android-chrome-192x192.png | Bin 0 -> 5666 bytes web-client/public/android-chrome-512x512.png | Bin 0 -> 9770 bytes web-client/public/apple-touch-icon.png | Bin 0 -> 5107 bytes web-client/public/favicon-16x16.png | Bin 0 -> 386 bytes web-client/public/favicon-32x32.png | Bin 0 -> 688 bytes web-client/src/app/apis/[apiId]/page.tsx | 6 +- web-client/src/app/apis/add/page.tsx | 6 +- .../app/authorizations/[username]/page.tsx | 3 +- .../src/app/credits/[username]/page.tsx | 28 ++- web-client/src/app/credits/page.tsx | 36 ++- web-client/src/app/documentation/page.tsx | 212 +----------------- web-client/src/app/groups/add/page.tsx | 51 +++-- web-client/src/app/icon.png | Bin 0 -> 9770 bytes web-client/src/app/login/page.tsx | 4 +- web-client/src/app/tiers/[id]/users/page.tsx | 31 ++- web-client/src/app/users/[username]/page.tsx | 63 +++--- web-client/src/app/users/add/page.tsx | 6 +- web-client/src/components/Layout.tsx | 4 +- web-client/src/components/OpenApiViewer.tsx | 3 +- .../src/components/SearchableSelect.tsx | 55 ++++- web-client/src/middleware.ts | 31 ++- 29 files changed, 379 insertions(+), 341 deletions(-) create mode 100644 web-client/public/android-chrome-192x192.png create mode 100644 web-client/public/android-chrome-512x512.png create mode 100644 web-client/public/apple-touch-icon.png create mode 100644 web-client/public/favicon-16x16.png create mode 100644 web-client/public/favicon-32x32.png create mode 100644 web-client/src/app/icon.png diff --git a/backend-services/routes/config_routes.py b/backend-services/routes/config_routes.py index afaed66..6beab68 100644 --- a/backend-services/routes/config_routes.py +++ b/backend-services/routes/config_routes.py @@ -35,11 +35,12 @@ def _strip_id(doc: dict[str, Any]) -> dict[str, Any]: def _export_all() -> dict[str, Any]: - apis = [_strip_id(a) for a in api_collection.find().to_list(length=None)] - endpoints = [_strip_id(e) for e in endpoint_collection.find().to_list(length=None)] - roles = [_strip_id(r) for r in role_collection.find().to_list(length=None)] - groups = [_strip_id(g) for g in group_collection.find().to_list(length=None)] - routings = [_strip_id(r) for r in routing_collection.find().to_list(length=None)] + # PyMongo cursors are synchronous; convert to lists directly + apis = [_strip_id(a) for a in list(api_collection.find())] + endpoints = [_strip_id(e) for e in list(endpoint_collection.find())] + roles = [_strip_id(r) for r in list(role_collection.find())] + groups = [_strip_id(g) for g in list(group_collection.find())] + routings = [_strip_id(r) for r in list(routing_collection.find())] return { 'apis': apis, 'endpoints': endpoints, @@ -147,9 +148,9 @@ async def export_apis( 'rest', ) api.get('api_id') - eps = endpoint_collection.find( - {'api_name': api_name, 'api_version': api_version} - ).to_list(length=None) + eps = list( + endpoint_collection.find({'api_name': api_name, 'api_version': api_version}) + ) audit( request, actor=username, @@ -166,7 +167,7 @@ async def export_apis( ).dict(), 'rest', ) - apis = [_strip_id(a) for a in api_collection.find().to_list(length=None)] + apis = [_strip_id(a) for a in list(api_collection.find())] audit( request, actor=username, @@ -237,7 +238,7 @@ async def export_roles(request: Request, role_name: str | None = None): return process_response( ResponseModel(status_code=200, response={'role': _strip_id(role)}).dict(), 'rest' ) - roles = [_strip_id(r) for r in role_collection.find().to_list(length=None)] + roles = [_strip_id(r) for r in list(role_collection.find())] audit( request, actor=username, @@ -308,7 +309,7 @@ async def export_groups(request: Request, group_name: str | None = None): return process_response( ResponseModel(status_code=200, response={'group': _strip_id(group)}).dict(), 'rest' ) - groups = [_strip_id(g) for g in group_collection.find().to_list(length=None)] + groups = [_strip_id(g) for g in list(group_collection.find())] audit( request, actor=username, @@ -380,7 +381,7 @@ async def export_routings(request: Request, client_key: str | None = None): ResponseModel(status_code=200, response={'routing': _strip_id(routing)}).dict(), 'rest', ) - routings = [_strip_id(r) for r in routing_collection.find().to_list(length=None)] + routings = [_strip_id(r) for r in list(routing_collection.find())] audit( request, actor=username, @@ -440,7 +441,7 @@ async def export_endpoints( query['api_name'] = api_name if api_version: query['api_version'] = api_version - eps = [_strip_id(e) for e in endpoint_collection.find(query).to_list(length=None)] + eps = [_strip_id(e) for e in list(endpoint_collection.find(query))] return process_response( ResponseModel(status_code=200, response={'endpoints': eps}).dict(), 'rest' ) diff --git a/backend-services/routes/gateway_routes.py b/backend-services/routes/gateway_routes.py index 3706d15..fe3ee2f 100644 --- a/backend-services/routes/gateway_routes.py +++ b/backend-services/routes/gateway_routes.py @@ -365,6 +365,24 @@ async def gateway(request: Request, path: str): await limit_and_throttle(request) payload = await auth_required(request) username = payload.get('sub') + # Enforce API allowed roles when configured + try: + allowed_roles = resolved_api.get('api_allowed_roles') or [] + if allowed_roles: + from services.user_service import UserService as _US + u = await _US.get_user_by_username_helper(username) + if (u.get('role') or '') not in set(allowed_roles): + return process_response( + ResponseModel( + status_code=403, + response_headers={'request_id': request_id}, + error_code='GTW014', + error_message='Forbidden: role not allowed for this API', + ).dict(), + 'rest', + ) + except Exception: + pass await enforce_pre_request_limit(request, username) else: pass @@ -653,6 +671,24 @@ async def soap_gateway(request: Request, path: str): await limit_and_throttle(request) payload = await auth_required(request) username = payload.get('sub') + # Enforce API allowed roles when configured + try: + allowed_roles = api.get('api_allowed_roles') or [] + if allowed_roles: + from services.user_service import UserService as _US + u = await _US.get_user_by_username_helper(username) + if (u.get('role') or '') not in set(allowed_roles): + return process_response( + ResponseModel( + status_code=403, + response_headers={'request_id': request_id}, + error_code='GTW014', + error_message='Forbidden: role not allowed for this API', + ).dict(), + 'graphql', + ) + except Exception: + pass await enforce_pre_request_limit(request, username) else: pass @@ -833,6 +869,24 @@ async def graphql_gateway(request: Request, path: str): await limit_and_throttle(request) payload = await auth_required(request) username = payload.get('sub') + # Enforce API allowed roles when configured + try: + allowed_roles = api.get('api_allowed_roles') or [] + if allowed_roles: + from services.user_service import UserService as _US + u = await _US.get_user_by_username_helper(username) + if (u.get('role') or '') not in set(allowed_roles): + return process_response( + ResponseModel( + status_code=403, + response_headers={'request_id': request_id}, + error_code='GTW014', + error_message='Forbidden: role not allowed for this API', + ).dict(), + 'grpc', + ) + except Exception: + pass await enforce_pre_request_limit(request, username) else: pass @@ -1107,6 +1161,24 @@ async def grpc_gateway(request: Request, path: str): await limit_and_throttle(request) payload = await auth_required(request) username = payload.get('sub') + # Enforce API allowed roles when configured + try: + allowed_roles = api.get('api_allowed_roles') or [] + if allowed_roles: + from services.user_service import UserService as _US + u = await _US.get_user_by_username_helper(username) + if (u.get('role') or '') not in set(allowed_roles): + return process_response( + ResponseModel( + status_code=403, + response_headers={'request_id': request_id}, + error_code='GTW014', + error_message='Forbidden: role not allowed for this API', + ).dict(), + 'soap', + ) + except Exception: + pass await enforce_pre_request_limit(request, username) else: pass diff --git a/backend-services/routes/security_routes.py b/backend-services/routes/security_routes.py index 44e62dc..9de581a 100644 --- a/backend-services/routes/security_routes.py +++ b/backend-services/routes/security_routes.py @@ -250,8 +250,29 @@ async def restart_gateway(request: Request): 'rest', ) - pid_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'doorman.pid') - pid_file = os.path.abspath(pid_file) + # Try multiple likely locations for doorman.pid to avoid CWD ambiguity + candidates = [] + try: + routes_dir = os.path.dirname(os.path.abspath(__file__)) + be_root = os.path.abspath(os.path.join(routes_dir, '..')) + candidates.append(os.path.join(be_root, 'doorman.pid')) + except Exception: + pass + try: + # CWD-relative (used by doorman.start in some setups) + candidates.append(os.path.abspath('doorman.pid')) + except Exception: + pass + try: + # Alongside doorman.py + from importlib import import_module as _imp + _dm = _imp('doorman') + _dm_path = os.path.abspath(getattr(_dm, '__file__', '')) + if _dm_path: + candidates.append(os.path.join(os.path.dirname(_dm_path), 'doorman.pid')) + except Exception: + pass + pid_file = next((p for p in candidates if p and os.path.exists(p)), candidates[0] if candidates else os.path.abspath('doorman.pid')) if not os.path.exists(pid_file): return process_response( ResponseModel( diff --git a/backend-services/routes/subscription_routes.py b/backend-services/routes/subscription_routes.py index 2d65699..09652d7 100644 --- a/backend-services/routes/subscription_routes.py +++ b/backend-services/routes/subscription_routes.py @@ -58,6 +58,18 @@ async def subscribe_api(api_data: SubscribeModel, request: Request): ) logger.info(f'{request_id} | Endpoint: {request.method} {str(request.url.path)}') + # If targeting a different user, require manage_subscriptions permission + if api_data.username and api_data.username != username: + if not await platform_role_required_bool(username, 'manage_subscriptions'): + return respond_rest( + ResponseModel( + status_code=403, + response_headers={'request_id': request_id}, + error_code='SUB009', + error_message='You do not have permission to subscribe another user', + ) + ) + if not await group_required( request, api_data.api_name + '/' + api_data.api_version, api_data.username ): @@ -145,6 +157,18 @@ async def unsubscribe_api(api_data: SubscribeModel, request: Request): ) logger.info(f'{request_id} | Endpoint: {request.method} {str(request.url.path)}') + # If targeting a different user, require manage_subscriptions permission + if api_data.username and api_data.username != username: + if not await platform_role_required_bool(username, 'manage_subscriptions'): + return respond_rest( + ResponseModel( + status_code=403, + response_headers={'request_id': request_id}, + error_code='SUB010', + error_message='You do not have permission to unsubscribe another user', + ) + ) + if not await group_required( request, api_data.api_name + '/' + api_data.api_version, api_data.username ): @@ -324,7 +348,7 @@ async def available_apis(username: str, request: Request): can_manage = await platform_role_required_bool(actor, 'manage_subscriptions') cursor = api_collection.find().sort('api_name', 1) - apis = cursor.to_list(length=None) + apis = list(cursor) for a in apis: if a.get('_id'): del a['_id'] diff --git a/backend-services/routes/user_routes.py b/backend-services/routes/user_routes.py index 8b7c985..51bbe55 100644 --- a/backend-services/routes/user_routes.py +++ b/backend-services/routes/user_routes.py @@ -559,7 +559,7 @@ Response: @user_router.get( - '/email/{email}', description='Get user by email', response_model=list[UserModelResponse] + '/email/{email}', description='Get user by email', response_model=ResponseModel ) async def get_user_by_email(email: str, request: Request): request_id = str(uuid.uuid4()) diff --git a/backend-services/services/user_service.py b/backend-services/services/user_service.py index 44b647b..583aae3 100644 --- a/backend-services/services/user_service.py +++ b/backend-services/services/user_service.py @@ -124,10 +124,6 @@ class UserService: """ logger.info(f'{request_id} | Getting user by email: {email}') user = await db_find_one(user_collection, {'email': email}) - if '_id' in user: - del user['_id'] - if 'password' in user: - del user['password'] if not user: logger.error(f'{request_id} | User retrieval failed with code USR002') return ResponseModel( @@ -136,6 +132,10 @@ class UserService: error_code='USR002', error_message='User not found', ).dict() + if '_id' in user: + del user['_id'] + if 'password' in user: + del user['password'] logger.info(f'{request_id} | User retrieval successful') if not active_username == user.get('username') and not await platform_role_required_bool( active_username, 'manage_users' diff --git a/backend-services/utils/subscription_util.py b/backend-services/utils/subscription_util.py index 5fa0628..75e5c16 100644 --- a/backend-services/utils/subscription_util.py +++ b/backend-services/utils/subscription_util.py @@ -9,6 +9,7 @@ import os from fastapi import HTTPException, Request from jose import JWTError +from utils.role_util import is_admin_user from utils.async_db import db_find_one from utils.auth_util import auth_required @@ -26,11 +27,12 @@ async def subscription_required(request: Request): raise HTTPException(status_code=401, detail='Invalid token') # Admin user bypasses subscription requirements - admin_email = os.getenv('DOORMAN_ADMIN_EMAIL', '') - logger.debug(f'Subscription check - username: "{username}", admin_email: "{admin_email}", match: {username == admin_email}') - if username == admin_email and admin_email: - logger.info(f'Admin user {username} bypassing subscription check') - return payload + try: + if await is_admin_user(username): + logger.info(f'Admin user {username} bypassing subscription check') + return payload + except Exception: + pass full_path = request.url.path if full_path.startswith('/api/rest/'): prefix = '/api/rest/' diff --git a/web-client/next.config.ts b/web-client/next.config.ts index 4a7319c..f11e25d 100644 --- a/web-client/next.config.ts +++ b/web-client/next.config.ts @@ -42,6 +42,15 @@ const nextConfig: NextConfig = { // Allow opt-out to disable optimizer entirely when desired unoptimized: (process.env.NEXT_IMAGE_UNOPTIMIZED || '').toLowerCase() === 'true', }, + async redirects() { + return [ + { + source: '/favicon.ico', + destination: '/favicon-32x32.png', + permanent: true, + }, + ] + }, async headers() { return [ { diff --git a/web-client/public/android-chrome-192x192.png b/web-client/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..10ad9cb46863ebe5ac087efd5799ea087245280f GIT binary patch literal 5666 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 IM6N<$f*7roMgRZ+ literal 0 HcmV?d00001 diff --git a/web-client/public/android-chrome-512x512.png b/web-client/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..c4430d66a72413dbd48610666bc6ab8a46fd5a49 GIT binary patch literal 9770 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 D*2{zB literal 0 HcmV?d00001 diff --git a/web-client/public/apple-touch-icon.png b/web-client/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f99dead182d4f93705791ddb8cf881c650680b78 GIT binary patch literal 5107 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 literal 0 HcmV?d00001 diff --git a/web-client/public/favicon-32x32.png b/web-client/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..32da2471db5c40b10607c89c9c4d0fc98f2afeb5 GIT binary patch literal 688 zcmV;h0#E&kP)LLqJsg69&OR;ye

gXR&~#A7P7lDCgZkP@G;HwP~TV8 zHQil`!VRt{Z{aG9*vj5~NpCioW`ARv6*Aq1OM!f#uaa&z*orgH={Og1_Q=sH8++H7 z-mI5RFHzS5D4RZ9nmh&h5hy`I|;Op6Zmy<9{y1R-}fWc-cpe5{tV#`Mqko~?-3UkK#>zU6&Yb_*@7o;TsR5NzO($8 z3nd@JvT$Jm^seieFtu#M{Z|fOo~}{h&Q||d0n(RXU=h9_QxQu24W(^F|ISt{<1os5 z9REm< { onChange={setNewRole} onAdd={addRole} onKeyPress={(e) => e.key === 'Enter' && addRole()} - placeholder="Select or type role name" + placeholder="Select role" fetchOptions={fetchRoles} disabled={saving} addButtonText="Add" + restrictToOptions /> )} @@ -1324,10 +1325,11 @@ const ApiDetailPage = () => { onChange={setNewGroup} onAdd={addGroup} onKeyPress={(e) => e.key === 'Enter' && addGroup()} - placeholder="Select or type group name" + placeholder="Select group" fetchOptions={fetchGroups} disabled={saving} addButtonText="Add" + restrictToOptions /> )} diff --git a/web-client/src/app/apis/add/page.tsx b/web-client/src/app/apis/add/page.tsx index d783a79..e5d5521 100644 --- a/web-client/src/app/apis/add/page.tsx +++ b/web-client/src/app/apis/add/page.tsx @@ -593,10 +593,11 @@ const AddApiPage = () => { onChange={setNewRole} onAdd={addRole} onKeyPress={(e) => e.key === 'Enter' && addRole()} - placeholder="Select or type role name" + placeholder="Select role" fetchOptions={fetchRoles} disabled={loading} addButtonText="Add" + restrictToOptions />

{formData.api_allowed_roles.map((r, i) => ( @@ -630,10 +631,11 @@ const AddApiPage = () => { onChange={setNewGroup} onAdd={addGroup} onKeyPress={(e) => e.key === 'Enter' && addGroup()} - placeholder="Select or type group name" + placeholder="Select group" fetchOptions={fetchGroups} disabled={loading} addButtonText="Add" + restrictToOptions />
{formData.api_allowed_groups.map((g, i) => ( diff --git a/web-client/src/app/authorizations/[username]/page.tsx b/web-client/src/app/authorizations/[username]/page.tsx index c6b7f90..78c75d5 100644 --- a/web-client/src/app/authorizations/[username]/page.tsx +++ b/web-client/src/app/authorizations/[username]/page.tsx @@ -233,9 +233,10 @@ const UserSubscriptionsPage = () => {
- ))} -
- - )} +
+
- -
-
- {tabs.map(t => { - const isActive = active === t.key - return ( - - ) - })} -
- -
- {tabs.map(t => ( -
- {t.type === 'md' ? ( -
- -
- ) : t.type === 'html' ? ( -
- -
- ) : ( -
-