From a018850257d35ec246d5a57a649bb8819c79cd6b Mon Sep 17 00:00:00 2001 From: Huarch Date: Mon, 3 Aug 2026 10:39:55 +0800 Subject: [PATCH] refactor(auth): remove project-local user API --- BACKEND_NAMING_AUDIT.md | 2 +- app/api/v1/endpoints/project.py | 2 +- app/api/v1/endpoints/users.py | 36 ---- app/api/v1/router.py | 6 - app/native/wndb/__init__.py | 2 - app/native/wndb/s39_user.py | 37 ---- app/services/scheme_management.py | 51 +---- app/services/tjnetwork.py | 13 -- contracts/manifest.json | 2 +- contracts/server-v1.openapi.json | 335 +---------------------------- tests/api/test_openapi_contract.py | 14 ++ 11 files changed, 20 insertions(+), 480 deletions(-) delete mode 100644 app/api/v1/endpoints/users.py delete mode 100644 app/native/wndb/s39_user.py diff --git a/BACKEND_NAMING_AUDIT.md b/BACKEND_NAMING_AUDIT.md index d5097b4..476212b 100644 --- a/BACKEND_NAMING_AUDIT.md +++ b/BACKEND_NAMING_AUDIT.md @@ -73,7 +73,7 @@ Headers keep standard HTTP casing: ## Internal vs Customer Difference -The customer backend retains local auth/user-management routes under `/api/v1/auth` and `/api/v1/users`; the internal backend has migrated to Keycloak/metadata admin routes. Treat those Customer-only auth routes as delivery compatibility surface, not a source for new internal API naming. +Both backend editions use Keycloak-backed metadata users and expose user administration through `/api/v1/admin/users`. Project-local user routes such as `/api/v1/users` are no longer part of the public contract. ## Recommendation diff --git a/app/api/v1/endpoints/project.py b/app/api/v1/endpoints/project.py index e56af00..d583866 100644 --- a/app/api/v1/endpoints/project.py +++ b/app/api/v1/endpoints/project.py @@ -261,7 +261,7 @@ async def is_project_locked_endpoint( """ return network in lockedPrjs.keys() -@router.get("/projects/current/lock/ownership", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前客户端 (IP) 锁定。") +@router.get("/projects/current/lock/ownership", summary="检查项目是否被当前用户锁定", description="检查指定项目是否被当前访问地址 (IP) 锁定。") async def is_project_locked_by_me_endpoint( network: str = Query(..., description="管网名称(或数据库名称)"), req: Request = None diff --git a/app/api/v1/endpoints/users.py b/app/api/v1/endpoints/users.py deleted file mode 100644 index 867a766..0000000 --- a/app/api/v1/endpoints/users.py +++ /dev/null @@ -1,36 +0,0 @@ -from fastapi import APIRouter, Request, Query -from typing import Any, List, Dict, Union -from app.services.tjnetwork import Any, get_all_users, get_user, get_user_schema - -router = APIRouter() - -########################################################### -# user 39 -########################################################### - -@router.get("/network-schemas/user", summary="获取用户模式", description="获取指定网络的用户模式定义") -async def fastapi_get_user_schema(network: str = Query(..., description="管网名称(或数据库名称)")) -> dict[str, dict[Any, Any]]: - """ - 获取用户模式定义 - - 返回指定网络的用户模式结构定义 - """ - return get_user_schema(network) - -@router.get("/users/detail", summary="获取单个用户", description="获取指定网络中的单个用户信息") -async def fastapi_get_user(network: str = Query(..., description="管网名称(或数据库名称)"), user_name: str = Query(..., description="用户名")) -> dict[Any, Any]: - """ - 获取用户信息 - - 返回指定网络中指定用户名的详细信息 - """ - return get_user(network, user_name) - -@router.get("/users", summary="获取所有用户", description="获取指定网络的所有用户列表") -async def fastapi_get_all_users(network: str = Query(..., description="管网名称(或数据库名称)")) -> list[dict[Any, Any]]: - """ - 获取所有用户列表 - - 返回指定网络中所有用户的信息 - """ - return get_all_users(network) \ No newline at end of file diff --git a/app/api/v1/router.py b/app/api/v1/router.py index b6f801c..b77b425 100644 --- a/app/api/v1/router.py +++ b/app/api/v1/router.py @@ -22,7 +22,6 @@ from app.api.v1.endpoints import ( sensor_placement, simulation, snapshots, - users, web_search, ) from app.api.v1.endpoints.components import ( @@ -152,11 +151,6 @@ api_router.include_router( tags=["Snapshots"], dependencies=[simulation_access], ) -api_router.include_router( - users.router, - tags=["Users"], - dependencies=[webgis_view_access], -) api_router.include_router( schemes.router, tags=["Schemes"], diff --git a/app/native/wndb/__init__.py b/app/native/wndb/__init__.py index 4765495..35b9c20 100644 --- a/app/native/wndb/__init__.py +++ b/app/native/wndb/__init__.py @@ -460,8 +460,6 @@ from .s36_wda_cal import ( # ----------------------------------------------------------------------------- from .s38_scada_info import get_scada_info_schema, get_scada_info, get_all_scada_info -from .s39_user import get_user_schema, get_user, get_all_users - from .s40_schema import get_scheme_schema, get_scheme, get_all_schemes from .s41_pipe_risk_probability import ( diff --git a/app/native/wndb/s39_user.py b/app/native/wndb/s39_user.py deleted file mode 100644 index 3b4c37d..0000000 --- a/app/native/wndb/s39_user.py +++ /dev/null @@ -1,37 +0,0 @@ -from .database import * -from .s0_base import * - -class User(object): - def __init__(self, input: dict[str, Any]) -> None: - self.type = 'user' - self.id = str(input['user_id']) - self.name = str(input['username']) - self.password = str(input['password']) - - def as_dict(self) -> dict[str, Any]: - return { 'type': self.type, 'id': self.id, 'name': self.name, 'password': self.password } - - def as_id_dict(self) -> dict[str, Any]: - return { 'type': self.type, 'id': self.id } - - -def get_user_schema(name: str) -> dict[str, dict[Any, Any]]: - return { 'id' : {'type': 'str' , 'optional': False , 'readonly': True }, - 'name' : {'type': 'str' , 'optional': False , 'readonly': False}, - 'password' : {'type': 'str' , 'optional': False , 'readonly': False} } - -def get_user(name: str, user_name: str) -> dict[Any, Any]: - t = try_read(name, f"select * from users where username = '{user_name}'") - if t == None: - return {} - - d = {} - d['id'] = str(t['user_id']) - d['name'] = str(t['username']) - # d['password'] = str(t['password']) - - return d - -def get_all_users(name: str) -> list[dict[Any, Any]]: - return read_all(name, "select * from users") - diff --git a/app/services/scheme_management.py b/app/services/scheme_management.py index aaa5c04..1cab298 100644 --- a/app/services/scheme_management.py +++ b/app/services/scheme_management.py @@ -11,53 +11,6 @@ from app.core.config import get_pgconn_string from app.services.time_api import parse_utc_time -# 2025/03/23 -def create_user(name: str, username: str, password: str): - """ - 创建用户 - :param name: 数据库名称 - :param username: 用户名 - :param password: 密码 - :return: - """ - try: - # 动态替换数据库名称 - conn_string = get_pgconn_string(db_name=name) - # 连接到 PostgreSQL 数据库(这里是数据库 "bb") - with psycopg.connect(conn_string) as conn: - with conn.cursor() as cur: - cur.execute( - "INSERT INTO users (username, password) VALUES (%s, %s)", - (username, password), - ) - # 提交事务 - conn.commit() - print("新用户创建成功!") - except Exception as e: - print(f"创建用户出错:{e}") - - -# 2025/03/23 -def delete_user(name: str, username: str): - """ - 删除用户 - :param name: 数据库名称 - :param username: 用户名 - :return: - """ - try: - # 动态替换数据库名称 - conn_string = get_pgconn_string(db_name=name) - # 连接到 PostgreSQL 数据库(这里是数据库 "bb") - with psycopg.connect(conn_string) as conn: - with conn.cursor() as cur: - cur.execute("DELETE FROM users WHERE username = %s", (username,)) - conn.commit() - print(f"用户 {username} 删除成功!") - except Exception as e: - print(f"删除用户出错:{e}") - - # 2025/03/23 def scheme_name_exists(name: str, scheme_name: str) -> bool: """ @@ -98,8 +51,8 @@ def store_scheme_info( :param name: 数据库名称 :param scheme_name: 方案名称 :param scheme_type: 方案类型 - :param username: 用户名(需在 users 表中已存在) - :param scheme_start_time: 方案起始时间(字符串) + :param username: MetaDB 中的用户名快照 + :param scheme_start_time: 带时区的方案起始时间;写入前统一转换为 UTC :param scheme_detail: 方案详情(字典,会转换为 JSON) :return: """ diff --git a/app/services/tjnetwork.py b/app/services/tjnetwork.py index fa36d76..2400e38 100644 --- a/app/services/tjnetwork.py +++ b/app/services/tjnetwork.py @@ -1290,19 +1290,6 @@ def get_scada_info(name: str, id: str) -> dict[str, Any]: def get_all_scada_info(name: str) -> list[dict[str, Any]]: return api.get_all_scada_info(name) -# DingZQ 2025-03-27 -############################################################ -# 39 users -############################################################ -def get_user_schema(name: str) -> dict[str, dict[str, Any]]: - return api.get_user_schema(name) - -def get_user(name: str, user_name: str) -> dict[str, Any]: - return api.get_user(name, user_name=user_name) - -def get_all_users(name: str) -> list[dict[str, Any]]: - return api.get_all_users(name) - ############################################################ # scheme 40 ############################################################ diff --git a/contracts/manifest.json b/contracts/manifest.json index 719a09e..c11cbce 100644 --- a/contracts/manifest.json +++ b/contracts/manifest.json @@ -3,7 +3,7 @@ "contracts": { "server": { "file": "server-v1.openapi.json", - "sha256": "d80a968d281fdb2953364a5979c2d61fda5151a1e1759c01cc96780b11a6d56c" + "sha256": "9cd5b962e9556ec227c52d0dc7d4ef4af562dcea86e16877c923c37de0f4f704" } } } diff --git a/contracts/server-v1.openapi.json b/contracts/server-v1.openapi.json index 15f2abe..f6ddeac 100644 --- a/contracts/server-v1.openapi.json +++ b/contracts/server-v1.openapi.json @@ -19425,108 +19425,6 @@ ] } }, - "/api/v1/network-schemas/user": { - "get": { - "description": "获取指定网络的用户模式定义", - "operationId": "get_network_schemas_user", - "parameters": [ - { - "in": "header", - "name": "X-Project-Id", - "required": true, - "schema": { - "title": "X-Project-Id", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": { - "type": "object" - }, - "title": "Response Get Network Schemas User", - "type": "object" - } - } - }, - "description": "Successful Response" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Authentication required" - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Insufficient permission" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Resource not found" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Resource conflict" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Validation error" - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Dependency unavailable" - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "summary": "获取用户模式", - "tags": [ - "Users" - ] - } - }, "/api/v1/network-schemas/valve": { "get": { "description": "获取指定水网中所有阀门的架构和字段定义", @@ -27430,7 +27328,7 @@ }, "/api/v1/projects/current/lock/ownership": { "get": { - "description": "检查指定项目是否被当前客户端 (IP) 锁定。", + "description": "检查指定项目是否被当前访问地址 (IP) 锁定。", "operationId": "get_projects_current_lock_ownership", "parameters": [ { @@ -47552,237 +47450,6 @@ ] } }, - "/api/v1/users": { - "get": { - "description": "获取指定网络的所有用户列表", - "operationId": "get_users", - "parameters": [ - { - "in": "query", - "name": "limit", - "required": false, - "schema": { - "default": 100, - "maximum": 1000, - "minimum": 1, - "title": "Limit", - "type": "integer" - } - }, - { - "in": "query", - "name": "offset", - "required": false, - "schema": { - "default": 0, - "minimum": 0, - "title": "Offset", - "type": "integer" - } - }, - { - "in": "header", - "name": "X-Project-Id", - "required": true, - "schema": { - "title": "X-Project-Id", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Page_dict_Any__Any__" - } - } - }, - "description": "Successful Response" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Authentication required" - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Insufficient permission" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Resource not found" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Resource conflict" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Validation error" - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Dependency unavailable" - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "summary": "获取所有用户", - "tags": [ - "Users" - ] - } - }, - "/api/v1/users/detail": { - "get": { - "description": "获取指定网络中的单个用户信息", - "operationId": "get_users_detail", - "parameters": [ - { - "description": "用户名", - "in": "query", - "name": "user_name", - "required": true, - "schema": { - "description": "用户名", - "title": "User Name", - "type": "string" - } - }, - { - "in": "header", - "name": "X-Project-Id", - "required": true, - "schema": { - "title": "X-Project-Id", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "title": "Response Get Users Detail", - "type": "object" - } - } - }, - "description": "Successful Response" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Authentication required" - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Insufficient permission" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Resource not found" - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Resource conflict" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Validation error" - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "Dependency unavailable" - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "summary": "获取单个用户", - "tags": [ - "Users" - ] - } - }, "/api/v1/valve-closure-analyses": { "post": { "description": "高级版本的阀门关闭分析,支持同时关闭多个阀门,并在指定持续时间内进行模拟。返回纯文本格式的分析结果。", diff --git a/tests/api/test_openapi_contract.py b/tests/api/test_openapi_contract.py index fd3eb50..07b45b6 100644 --- a/tests/api/test_openapi_contract.py +++ b/tests/api/test_openapi_contract.py @@ -77,6 +77,20 @@ def test_handler_router_defines_only_the_public_rest_operations() -> None: assert ("GET", "/getpipeproperties/") not in source_operations +def test_legacy_project_user_operations_are_not_exposed() -> None: + source_paths = { + route.path + for route in source_api_router.routes + if isinstance(route, APIRoute) + } + + assert { + "/network-schemas/user", + "/users", + "/users/detail", + }.isdisjoint(source_paths) + + def test_rest_openapi_satisfies_contract_invariants() -> None: from fastapi import FastAPI