Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 97e9a04

Browse files
committedJun 20, 2025··
chore: tidy up
1 parent 96941b4 commit 97e9a04

File tree

8 files changed

+419
-345
lines changed

8 files changed

+419
-345
lines changed
 

‎aperag/db/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ class AuditLog(Base):
774774
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
775775
user_id = Column(String(36), nullable=True, comment="User ID")
776776
username = Column(String(255), nullable=True, comment="Username")
777-
resource_type = Column(Enum(AuditResource), nullable=True, comment="Resource type")
777+
resource_type = Column(EnumColumn(AuditResource), nullable=True, comment="Resource type")
778778
resource_id = Column(String(255), nullable=True, comment="Resource ID (extracted at query time)")
779779
api_name = Column(String(255), nullable=False, comment="API operation name")
780780
http_method = Column(String(10), nullable=False, comment="HTTP method (POST, PUT, DELETE)")

‎aperag/middleware/audit_middleware.py

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,15 @@ def _get_audit_info_from_route(self, request: Request) -> Tuple[Optional[str], O
7474
if hasattr(route, 'tags') and route.tags:
7575
resource_type = audit_service.get_resource_type_from_tags(route.tags)
7676

77+
# Debug logging
78+
logger.debug(f"Route info - Path: {request.url.path}, API name: {api_name}, Tags: {getattr(route, 'tags', None)}, Resource type: {resource_type}")
79+
7780
# Both API name and resource type are required
7881
if api_name and resource_type:
7982
return api_name, resource_type
83+
elif api_name or resource_type:
84+
# Log when we have partial info to help debugging
85+
logger.warning(f"Partial audit info - Path: {request.url.path}, API name: {api_name}, Resource type: {resource_type}, Tags: {getattr(route, 'tags', None)}")
8086

8187
except Exception as e:
8288
logger.warning(f"Failed to get audit info from route: {e}")
@@ -111,13 +117,6 @@ async def dispatch(self, request: Request, call_next):
111117
if not self._should_audit(request.url.path, request.method):
112118
return await call_next(request)
113119

114-
# Get audit info from route
115-
api_name, resource_type = self._get_audit_info_from_route(request)
116-
117-
if not api_name or not resource_type:
118-
# No matching operation found, skip audit
119-
return await call_next(request)
120-
121120
# Record start time in milliseconds
122121
start_time_ms = int(time.time() * 1000)
123122
request_data = None
@@ -127,7 +126,7 @@ async def dispatch(self, request: Request, call_next):
127126
end_time_ms = None
128127

129128
try:
130-
# Extract request data
129+
# Extract request data before calling next
131130
request_data = await self._extract_request_data(request)
132131

133132
# Call the actual endpoint
@@ -137,52 +136,92 @@ async def dispatch(self, request: Request, call_next):
137136
# Record end time
138137
end_time_ms = int(time.time() * 1000)
139138

140-
# Try to extract response data for non-streaming responses
141-
if hasattr(response, 'body'):
139+
# Get audit info from route (now available after call_next)
140+
api_name, resource_type = self._get_audit_info_from_route(request)
141+
142+
# Only proceed with audit if we have both api_name and resource_type
143+
if api_name and resource_type:
144+
# Try to extract response data for non-streaming responses
145+
if hasattr(response, 'body'):
146+
try:
147+
response_body = response.body.decode() if response.body else None
148+
if response_body:
149+
response_data = json.loads(response_body)
150+
except:
151+
pass
152+
153+
# Log audit asynchronously
142154
try:
143-
response_body = response.body.decode() if response.body else None
144-
if response_body:
145-
response_data = json.loads(response_body)
146-
except:
147-
pass
155+
# Get user info from request state (set by auth middleware)
156+
user_id = getattr(request.state, 'user_id', None)
157+
username = getattr(request.state, 'username', None)
158+
159+
# Extract client info
160+
ip_address, user_agent = audit_service._extract_client_info(request)
161+
162+
# Log audit in background (don't await to avoid blocking)
163+
import asyncio
164+
asyncio.create_task(
165+
audit_service.log_audit(
166+
user_id=user_id,
167+
username=username,
168+
resource_type=resource_type,
169+
api_name=api_name,
170+
http_method=request.method,
171+
path=request.url.path,
172+
status_code=status_code,
173+
start_time=start_time_ms,
174+
end_time=end_time_ms,
175+
request_data=request_data,
176+
response_data=response_data,
177+
error_message=error_message,
178+
ip_address=ip_address,
179+
user_agent=user_agent
180+
)
181+
)
182+
except Exception as audit_error:
183+
logger.error(f"Failed to log audit: {audit_error}")
148184

149185
except Exception as e:
150186
error_message = str(e)
151187
status_code = 500
152188
end_time_ms = int(time.time() * 1000)
153-
# Re-raise for normal error handling
154-
raise
155-
finally:
156-
# Log audit asynchronously
189+
190+
# Still try to log the error audit if route info is available
157191
try:
158-
# Get user info from request state (set by auth middleware)
159-
user_id = getattr(request.state, 'user_id', None)
160-
username = getattr(request.state, 'username', None)
161-
162-
# Extract client info
163-
ip_address, user_agent = audit_service._extract_client_info(request)
164-
165-
# Log audit in background (don't await to avoid blocking)
166-
import asyncio
167-
asyncio.create_task(
168-
audit_service.log_audit(
169-
user_id=user_id,
170-
username=username,
171-
resource_type=resource_type,
172-
api_name=api_name,
173-
http_method=request.method,
174-
path=request.url.path,
175-
status_code=status_code,
176-
start_time=start_time_ms,
177-
end_time=end_time_ms,
178-
request_data=request_data,
179-
response_data=response_data,
180-
error_message=error_message,
181-
ip_address=ip_address,
182-
user_agent=user_agent
192+
api_name, resource_type = self._get_audit_info_from_route(request)
193+
if api_name and resource_type:
194+
# Get user info from request state (set by auth middleware)
195+
user_id = getattr(request.state, 'user_id', None)
196+
username = getattr(request.state, 'username', None)
197+
198+
# Extract client info
199+
ip_address, user_agent = audit_service._extract_client_info(request)
200+
201+
# Log audit in background (don't await to avoid blocking)
202+
import asyncio
203+
asyncio.create_task(
204+
audit_service.log_audit(
205+
user_id=user_id,
206+
username=username,
207+
resource_type=resource_type,
208+
api_name=api_name,
209+
http_method=request.method,
210+
path=request.url.path,
211+
status_code=status_code,
212+
start_time=start_time_ms,
213+
end_time=end_time_ms,
214+
request_data=request_data,
215+
response_data=response_data,
216+
error_message=error_message,
217+
ip_address=ip_address,
218+
user_agent=user_agent
219+
)
183220
)
184-
)
185221
except Exception as audit_error:
186-
logger.error(f"Failed to log audit: {audit_error}")
222+
logger.error(f"Failed to log error audit: {audit_error}")
223+
224+
# Re-raise for normal error handling
225+
raise
187226

188227
return response

‎aperag/migration/versions/20250617113449-audit_log_table.py

Lines changed: 0 additions & 81 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""empty message
2+
3+
Revision ID: 3369b50dc810
4+
Revises: 12ea6d2bf365
5+
Create Date: 2025-06-20 17:22:42.655570
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '3369b50dc810'
16+
down_revision: Union[str, None] = '12ea6d2bf365'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Upgrade schema."""
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_table('audit_log',
25+
sa.Column('id', sa.String(length=36), nullable=False),
26+
sa.Column('user_id', sa.String(length=36), nullable=True, comment='User ID'),
27+
sa.Column('username', sa.String(length=255), nullable=True, comment='Username'),
28+
sa.Column('resource_type', sa.Enum('collection', 'document', 'bot', 'chat', 'message', 'api_key', 'llm_provider', 'llm_provider_model', 'model_service_provider', 'user', 'config', name='auditresource'), nullable=True, comment='Resource type'),
29+
sa.Column('resource_id', sa.String(length=255), nullable=True, comment='Resource ID (extracted at query time)'),
30+
sa.Column('api_name', sa.String(length=255), nullable=False, comment='API operation name'),
31+
sa.Column('http_method', sa.String(length=10), nullable=False, comment='HTTP method (POST, PUT, DELETE)'),
32+
sa.Column('path', sa.String(length=512), nullable=False, comment='API path'),
33+
sa.Column('status_code', sa.Integer(), nullable=True, comment='HTTP status code'),
34+
sa.Column('request_data', sa.Text(), nullable=True, comment='Request data (JSON)'),
35+
sa.Column('response_data', sa.Text(), nullable=True, comment='Response data (JSON)'),
36+
sa.Column('error_message', sa.Text(), nullable=True, comment='Error message if failed'),
37+
sa.Column('ip_address', sa.String(length=45), nullable=True, comment='Client IP address'),
38+
sa.Column('user_agent', sa.String(length=500), nullable=True, comment='User agent string'),
39+
sa.Column('request_id', sa.String(length=255), nullable=False, comment='Request ID for tracking'),
40+
sa.Column('start_time', sa.BigInteger(), nullable=False, comment='Request start time (milliseconds since epoch)'),
41+
sa.Column('end_time', sa.BigInteger(), nullable=True, comment='Request end time (milliseconds since epoch)'),
42+
sa.Column('gmt_created', sa.DateTime(timezone=True), nullable=False, comment='Created time'),
43+
sa.PrimaryKeyConstraint('id')
44+
)
45+
op.create_index('idx_audit_api_name', 'audit_log', ['api_name'], unique=False)
46+
op.create_index('idx_audit_gmt_created', 'audit_log', ['gmt_created'], unique=False)
47+
op.create_index('idx_audit_http_method', 'audit_log', ['http_method'], unique=False)
48+
op.create_index('idx_audit_request_id', 'audit_log', ['request_id'], unique=False)
49+
op.create_index('idx_audit_resource_id', 'audit_log', ['resource_id'], unique=False)
50+
op.create_index('idx_audit_resource_type', 'audit_log', ['resource_type'], unique=False)
51+
op.create_index('idx_audit_start_time', 'audit_log', ['start_time'], unique=False)
52+
op.create_index('idx_audit_status_code', 'audit_log', ['status_code'], unique=False)
53+
op.create_index('idx_audit_user_id', 'audit_log', ['user_id'], unique=False)
54+
# ### end Alembic commands ###
55+
56+
57+
def downgrade() -> None:
58+
"""Downgrade schema."""
59+
# ### commands auto generated by Alembic - please adjust! ###
60+
op.drop_index('idx_audit_user_id', table_name='audit_log')
61+
op.drop_index('idx_audit_status_code', table_name='audit_log')
62+
op.drop_index('idx_audit_start_time', table_name='audit_log')
63+
op.drop_index('idx_audit_resource_type', table_name='audit_log')
64+
op.drop_index('idx_audit_resource_id', table_name='audit_log')
65+
op.drop_index('idx_audit_request_id', table_name='audit_log')
66+
op.drop_index('idx_audit_http_method', table_name='audit_log')
67+
op.drop_index('idx_audit_gmt_created', table_name='audit_log')
68+
op.drop_index('idx_audit_api_name', table_name='audit_log')
69+
op.drop_table('audit_log')
70+
# ### end Alembic commands ###

‎aperag/service/audit_service.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import re
1818
import time
1919
import uuid
20+
from datetime import datetime
2021
from typing import Any, Dict, List, Optional
2122

2223
from sqlalchemy import and_, desc, select
@@ -40,14 +41,24 @@ def __init__(self):
4041

4142
# Map FastAPI tags to audit resources
4243
self.tag_resource_map = {
44+
# Support both singular and plural forms
45+
"collection": AuditResource.COLLECTION,
4346
"collections": AuditResource.COLLECTION,
47+
"document": AuditResource.DOCUMENT,
4448
"documents": AuditResource.DOCUMENT,
49+
"bot": AuditResource.BOT,
4550
"bots": AuditResource.BOT,
51+
"chat": AuditResource.CHAT,
4652
"chats": AuditResource.CHAT,
53+
"message": AuditResource.MESSAGE,
4754
"messages": AuditResource.MESSAGE,
55+
"apikey": AuditResource.API_KEY,
4856
"apikeys": AuditResource.API_KEY,
57+
"llm_provider": AuditResource.LLM_PROVIDER,
4958
"llm_providers": AuditResource.LLM_PROVIDER,
59+
"llm_provider_model": AuditResource.LLM_PROVIDER_MODEL,
5060
"llm_provider_models": AuditResource.LLM_PROVIDER_MODEL,
61+
"user": AuditResource.USER,
5162
"users": AuditResource.USER,
5263
"config": AuditResource.CONFIG,
5364
}
@@ -206,7 +217,7 @@ async def log_audit(
206217
)
207218

208219
# Save to database asynchronously
209-
async with get_async_session() as session:
220+
async for session in get_async_session():
210221
session.add(audit_log)
211222
await session.commit()
212223

@@ -220,12 +231,12 @@ async def list_audit_logs(
220231
api_name: Optional[str] = None,
221232
http_method: Optional[str] = None,
222233
status_code: Optional[int] = None,
223-
start_date: Optional[str] = None,
224-
end_date: Optional[str] = None,
234+
start_date: Optional[datetime] = None,
235+
end_date: Optional[datetime] = None,
225236
limit: int = 1000
226237
) -> List[AuditLog]:
227238
"""List audit logs with filtering"""
228-
async with get_async_session() as session:
239+
async for session in get_async_session():
229240
# Build query
230241
stmt = select(AuditLog)
231242

‎aperag/views/audit.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,11 @@
2222
from aperag.config import get_async_session
2323
from aperag.schema import view_models
2424
from aperag.service.audit_service import audit_service
25-
from aperag.views.auth import get_current_active_user, get_current_admin
25+
from aperag.views.auth import current_user, get_current_admin
2626

2727
router = APIRouter()
2828

2929

30-
async def verify_admin_user(current_user: User = Depends(get_current_active_user)) -> User:
31-
"""Verify that the current user is an admin"""
32-
if not current_user.is_superuser:
33-
raise HTTPException(status_code=403, detail="Admin access required")
34-
return current_user
35-
36-
3730
@router.get("/audit-logs", tags=["audit"], name="ListAuditLogs", response_model=view_models.AuditLogList)
3831
async def list_audit_logs(
3932
user_id: Optional[str] = Query(None, description="Filter by user ID"),
@@ -46,7 +39,7 @@ async def list_audit_logs(
4639
start_date: Optional[datetime] = Query(None, description="Filter by start date"),
4740
end_date: Optional[datetime] = Query(None, description="Filter by end date"),
4841
limit: int = Query(1000, le=5000, description="Maximum number of records"),
49-
current_user: User = Depends(verify_admin_user)
42+
user: User = Depends(current_user)
5043
):
5144
"""List audit logs with filtering"""
5245

@@ -66,8 +59,8 @@ async def list_audit_logs(
6659
api_name=api_name,
6760
http_method=http_method,
6861
status_code=status_code,
69-
start_date=start_date.isoformat() if start_date else None,
70-
end_date=end_date.isoformat() if end_date else None,
62+
start_date=start_date,
63+
end_date=end_date,
7164
limit=limit
7265
)
7366

@@ -78,7 +71,7 @@ async def list_audit_logs(
7871
id=str(log.id),
7972
user_id=log.user_id,
8073
username=log.username,
81-
resource_type=log.resource_type.value if log.resource_type else None,
74+
resource_type=log.resource_type.value if hasattr(log.resource_type, 'value') else log.resource_type,
8275
resource_id=getattr(log, 'resource_id', None), # This is set during query
8376
api_name=log.api_name,
8477
http_method=log.http_method,
@@ -102,7 +95,7 @@ async def list_audit_logs(
10295
@router.get("/audit-logs/{audit_id}", tags=["audit"], name="GetAuditLog", response_model=view_models.AuditLog)
10396
async def get_audit_log(
10497
audit_id: str,
105-
current_user: User = Depends(verify_admin_user)
98+
user: User = Depends(current_user)
10699
):
107100
"""Get a specific audit log by ID"""
108101

@@ -153,7 +146,7 @@ async def list_audit_logs_view(
153146
limit: int = Query(20, ge=1, le=100, description="Items per page"),
154147
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
155148
api_name: Optional[str] = Query(None, description="Filter by API name"),
156-
user: User = Depends(get_current_admin),
149+
user: User = Depends(current_user),
157150
) -> view_models.AuditLogList:
158151
"""List audit logs with filtering and pagination"""
159152
return await audit_service.list_audit_logs(

‎frontend/src/locales/zh-CN.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ export default {
368368
'settings.title': '设置',
369369
'settings.profile': '个人资料',
370370
'settings.apiKeys': 'API 密钥',
371-
'settings.auditLogs': '审计日志',
371+
'settings.auditLogs': '操作日志',
372372
'settings.models': '模型管理',
373373
'settings.invitations': '邀请管理',
374374

‎frontend/src/pages/settings/auditLogs.tsx

Lines changed: 240 additions & 198 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.