remdb 0.3.146__py3-none-any.whl → 0.3.163__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of remdb might be problematic. Click here for more details.
- rem/agentic/agents/__init__.py +16 -0
- rem/agentic/agents/agent_manager.py +310 -0
- rem/agentic/context.py +81 -3
- rem/agentic/context_builder.py +18 -3
- rem/api/deps.py +3 -5
- rem/api/main.py +22 -3
- rem/api/mcp_router/server.py +2 -0
- rem/api/mcp_router/tools.py +90 -0
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/auth.py +346 -5
- rem/api/routers/chat/completions.py +4 -2
- rem/api/routers/chat/streaming.py +77 -22
- rem/api/routers/messages.py +24 -15
- rem/auth/__init__.py +13 -3
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +42 -5
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/models/entities/__init__.py +4 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +134 -0
- rem/services/__init__.py +3 -1
- rem/services/content/service.py +4 -3
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +511 -0
- rem/services/email/templates.py +360 -0
- rem/services/postgres/README.md +38 -0
- rem/services/postgres/diff_service.py +19 -3
- rem/services/postgres/pydantic_to_sqlalchemy.py +37 -2
- rem/services/postgres/repository.py +5 -4
- rem/services/session/compression.py +113 -50
- rem/services/session/reload.py +14 -7
- rem/services/user_service.py +29 -0
- rem/settings.py +175 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/METADATA +1 -1
- {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/RECORD +40 -31
- {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/WHEEL +0 -0
- {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/entry_points.txt +0 -0
rem/api/mcp_router/tools.py
CHANGED
|
@@ -1040,3 +1040,93 @@ async def get_schema(
|
|
|
1040
1040
|
logger.info(f"Retrieved schema for table '{table_name}' with {len(column_defs)} columns")
|
|
1041
1041
|
|
|
1042
1042
|
return result
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
@mcp_tool_error_handler
|
|
1046
|
+
async def save_agent(
|
|
1047
|
+
name: str,
|
|
1048
|
+
description: str,
|
|
1049
|
+
properties: dict[str, Any] | None = None,
|
|
1050
|
+
required: list[str] | None = None,
|
|
1051
|
+
tools: list[str] | None = None,
|
|
1052
|
+
tags: list[str] | None = None,
|
|
1053
|
+
version: str = "1.0.0",
|
|
1054
|
+
user_id: str | None = None,
|
|
1055
|
+
) -> dict[str, Any]:
|
|
1056
|
+
"""
|
|
1057
|
+
Save an agent schema to REM, making it available for use.
|
|
1058
|
+
|
|
1059
|
+
This tool creates or updates an agent definition in the user's schema space.
|
|
1060
|
+
The agent becomes immediately available for conversations.
|
|
1061
|
+
|
|
1062
|
+
**Default Tools**: All agents automatically get `search_rem` and `register_metadata`
|
|
1063
|
+
tools unless explicitly overridden.
|
|
1064
|
+
|
|
1065
|
+
Args:
|
|
1066
|
+
name: Agent name in kebab-case (e.g., "code-reviewer", "sales-assistant").
|
|
1067
|
+
Must be unique within the user's schema space.
|
|
1068
|
+
description: The agent's system prompt. This is the full instruction set
|
|
1069
|
+
that defines the agent's behavior, personality, and capabilities.
|
|
1070
|
+
Use markdown formatting for structure.
|
|
1071
|
+
properties: Output schema properties as a dict. Each property should have:
|
|
1072
|
+
- type: "string", "number", "boolean", "array", "object"
|
|
1073
|
+
- description: What this field captures
|
|
1074
|
+
Example: {"answer": {"type": "string", "description": "Response to user"}}
|
|
1075
|
+
If not provided, defaults to a simple {"answer": {"type": "string"}} schema.
|
|
1076
|
+
required: List of required property names. Defaults to ["answer"] if not provided.
|
|
1077
|
+
tools: List of tool names the agent can use. Defaults to ["search_rem", "register_metadata"].
|
|
1078
|
+
tags: Optional tags for categorizing the agent.
|
|
1079
|
+
version: Semantic version string (default: "1.0.0").
|
|
1080
|
+
user_id: User identifier for scoping. Uses authenticated user if not provided.
|
|
1081
|
+
|
|
1082
|
+
Returns:
|
|
1083
|
+
Dict with:
|
|
1084
|
+
- status: "success" or "error"
|
|
1085
|
+
- agent_name: Name of the saved agent
|
|
1086
|
+
- version: Version saved
|
|
1087
|
+
- message: Human-readable status
|
|
1088
|
+
|
|
1089
|
+
Examples:
|
|
1090
|
+
# Create a simple agent
|
|
1091
|
+
save_agent(
|
|
1092
|
+
name="greeting-bot",
|
|
1093
|
+
description="You are a friendly greeter. Say hello warmly.",
|
|
1094
|
+
properties={"answer": {"type": "string", "description": "Greeting message"}},
|
|
1095
|
+
required=["answer"]
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
# Create agent with structured output
|
|
1099
|
+
save_agent(
|
|
1100
|
+
name="sentiment-analyzer",
|
|
1101
|
+
description="Analyze sentiment of text provided by the user.",
|
|
1102
|
+
properties={
|
|
1103
|
+
"answer": {"type": "string", "description": "Analysis explanation"},
|
|
1104
|
+
"sentiment": {"type": "string", "enum": ["positive", "negative", "neutral"]},
|
|
1105
|
+
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
|
|
1106
|
+
},
|
|
1107
|
+
required=["answer", "sentiment"],
|
|
1108
|
+
tags=["analysis", "nlp"]
|
|
1109
|
+
)
|
|
1110
|
+
"""
|
|
1111
|
+
from ...agentic.agents.agent_manager import save_agent as _save_agent
|
|
1112
|
+
|
|
1113
|
+
# Get user_id from context if not provided
|
|
1114
|
+
user_id = AgentContext.get_user_id_or_default(user_id, source="save_agent")
|
|
1115
|
+
|
|
1116
|
+
# Delegate to agent_manager
|
|
1117
|
+
result = await _save_agent(
|
|
1118
|
+
name=name,
|
|
1119
|
+
description=description,
|
|
1120
|
+
user_id=user_id,
|
|
1121
|
+
properties=properties,
|
|
1122
|
+
required=required,
|
|
1123
|
+
tools=tools,
|
|
1124
|
+
tags=tags,
|
|
1125
|
+
version=version,
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
# Add helpful message for Slack users
|
|
1129
|
+
if result.get("status") == "success":
|
|
1130
|
+
result["message"] = f"Agent '{name}' saved. Use `/custom-agent {name}` to chat with it."
|
|
1131
|
+
|
|
1132
|
+
return result
|
rem/api/middleware/tracking.py
CHANGED
|
@@ -102,14 +102,14 @@ class AnonymousTrackingMiddleware(BaseHTTPMiddleware):
|
|
|
102
102
|
# Tenant ID from header or default
|
|
103
103
|
tenant_id = request.headers.get("X-Tenant-Id", "default")
|
|
104
104
|
|
|
105
|
-
# 4. Rate Limiting
|
|
106
|
-
if settings.postgres.enabled:
|
|
105
|
+
# 4. Rate Limiting (skip if disabled via settings)
|
|
106
|
+
if settings.postgres.enabled and settings.api.rate_limit_enabled:
|
|
107
107
|
is_allowed, current, limit = await self.rate_limiter.check_rate_limit(
|
|
108
108
|
tenant_id=tenant_id,
|
|
109
109
|
identifier=identifier,
|
|
110
110
|
tier=tier
|
|
111
111
|
)
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
if not is_allowed:
|
|
114
114
|
return JSONResponse(
|
|
115
115
|
status_code=429,
|
|
@@ -141,8 +141,8 @@ class AnonymousTrackingMiddleware(BaseHTTPMiddleware):
|
|
|
141
141
|
secure=settings.environment == "production"
|
|
142
142
|
)
|
|
143
143
|
|
|
144
|
-
# Add Rate Limit headers
|
|
145
|
-
if settings.postgres.enabled and 'limit' in locals():
|
|
144
|
+
# Add Rate Limit headers (only if rate limiting is enabled)
|
|
145
|
+
if settings.postgres.enabled and settings.api.rate_limit_enabled and 'limit' in locals():
|
|
146
146
|
response.headers["X-RateLimit-Limit"] = str(limit)
|
|
147
147
|
response.headers["X-RateLimit-Remaining"] = str(max(0, limit - current))
|
|
148
148
|
|
rem/api/routers/auth.py
CHANGED
|
@@ -1,20 +1,68 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Authentication Router.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Supports multiple authentication methods:
|
|
5
|
+
1. Email (passwordless): POST /api/auth/email/send-code, POST /api/auth/email/verify
|
|
6
|
+
2. OAuth (Google, Microsoft): GET /api/auth/{provider}/login, GET /api/auth/{provider}/callback
|
|
6
7
|
|
|
7
8
|
Endpoints:
|
|
9
|
+
- POST /api/auth/email/send-code - Send login code to email
|
|
10
|
+
- POST /api/auth/email/verify - Verify code and create session
|
|
8
11
|
- GET /api/auth/{provider}/login - Initiate OAuth flow
|
|
9
12
|
- GET /api/auth/{provider}/callback - OAuth callback
|
|
10
13
|
- POST /api/auth/logout - Clear session
|
|
11
14
|
- GET /api/auth/me - Current user info
|
|
12
15
|
|
|
13
16
|
Supported providers:
|
|
17
|
+
- email: Passwordless email login
|
|
14
18
|
- google: Google OAuth 2.0 / OIDC
|
|
15
19
|
- microsoft: Microsoft Entra ID OIDC
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
=============================================================================
|
|
22
|
+
Email Authentication Access Control
|
|
23
|
+
=============================================================================
|
|
24
|
+
|
|
25
|
+
The email auth provider implements a tiered access control system:
|
|
26
|
+
|
|
27
|
+
Access Control Flow (send-code):
|
|
28
|
+
User requests login code
|
|
29
|
+
├── User exists in database?
|
|
30
|
+
│ ├── Yes → Check user.tier
|
|
31
|
+
│ │ ├── tier == BLOCKED → Reject "Account is blocked"
|
|
32
|
+
│ │ └── tier != BLOCKED → Allow (send code, existing users grandfathered)
|
|
33
|
+
│ └── No (new user) → Check EMAIL__TRUSTED_EMAIL_DOMAINS
|
|
34
|
+
│ ├── Setting configured → domain in trusted list?
|
|
35
|
+
│ │ ├── Yes → Create user & send code
|
|
36
|
+
│ │ └── No → Reject "Email domain not allowed for signup"
|
|
37
|
+
│ └── Not configured (empty) → Create user & send code (no restrictions)
|
|
38
|
+
|
|
39
|
+
Key Behaviors:
|
|
40
|
+
- Existing users: Always allowed to login (unless tier=BLOCKED)
|
|
41
|
+
- New users: Must have email from trusted domain (if EMAIL__TRUSTED_EMAIL_DOMAINS is set)
|
|
42
|
+
- No restrictions: Leave EMAIL__TRUSTED_EMAIL_DOMAINS empty to allow all domains
|
|
43
|
+
|
|
44
|
+
User Tiers (models.entities.UserTier):
|
|
45
|
+
- BLOCKED: Cannot login (rejected at send-code)
|
|
46
|
+
- ANONYMOUS: Rate-limited anonymous access
|
|
47
|
+
- FREE: Standard free tier
|
|
48
|
+
- BASIC/PRO: Paid tiers with additional features
|
|
49
|
+
|
|
50
|
+
Configuration:
|
|
51
|
+
# Allow only specific domains for new signups
|
|
52
|
+
EMAIL__TRUSTED_EMAIL_DOMAINS=siggymd.ai,example.com
|
|
53
|
+
|
|
54
|
+
# Allow all domains (no restrictions)
|
|
55
|
+
EMAIL__TRUSTED_EMAIL_DOMAINS=
|
|
56
|
+
|
|
57
|
+
Example blocking a user:
|
|
58
|
+
user = await user_repo.get_by_id(user_id, tenant_id="default")
|
|
59
|
+
user.tier = UserTier.BLOCKED
|
|
60
|
+
await user_repo.upsert(user)
|
|
61
|
+
|
|
62
|
+
=============================================================================
|
|
63
|
+
OAuth Design Pattern (OAuth 2.1 + PKCE)
|
|
64
|
+
=============================================================================
|
|
65
|
+
|
|
18
66
|
1. User clicks "Login with Google"
|
|
19
67
|
2. /login generates state + PKCE code_verifier
|
|
20
68
|
3. Store code_verifier in session
|
|
@@ -37,6 +85,7 @@ Environment variables:
|
|
|
37
85
|
AUTH__MICROSOFT__CLIENT_ID=<microsoft-client-id>
|
|
38
86
|
AUTH__MICROSOFT__CLIENT_SECRET=<microsoft-client-secret>
|
|
39
87
|
AUTH__MICROSOFT__TENANT=common
|
|
88
|
+
EMAIL__TRUSTED_EMAIL_DOMAINS=example.com # Optional: restrict new signups
|
|
40
89
|
|
|
41
90
|
References:
|
|
42
91
|
- Authlib: https://docs.authlib.org/en/latest/
|
|
@@ -46,11 +95,14 @@ References:
|
|
|
46
95
|
from fastapi import APIRouter, HTTPException, Request
|
|
47
96
|
from fastapi.responses import RedirectResponse
|
|
48
97
|
from authlib.integrations.starlette_client import OAuth
|
|
98
|
+
from pydantic import BaseModel, EmailStr
|
|
49
99
|
from loguru import logger
|
|
50
100
|
|
|
51
101
|
from ...settings import settings
|
|
52
102
|
from ...services.postgres.service import PostgresService
|
|
53
103
|
from ...services.user_service import UserService
|
|
104
|
+
from ...auth.providers.email import EmailAuthProvider
|
|
105
|
+
from ...auth.jwt import JWTService, get_jwt_service
|
|
54
106
|
|
|
55
107
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
|
56
108
|
|
|
@@ -87,6 +139,182 @@ if settings.auth.microsoft.client_id:
|
|
|
87
139
|
logger.info(f"Microsoft OAuth provider registered (tenant: {tenant})")
|
|
88
140
|
|
|
89
141
|
|
|
142
|
+
# =============================================================================
|
|
143
|
+
# Email Authentication Endpoints
|
|
144
|
+
# =============================================================================
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class EmailSendCodeRequest(BaseModel):
|
|
148
|
+
"""Request to send login code."""
|
|
149
|
+
email: EmailStr
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class EmailVerifyRequest(BaseModel):
|
|
153
|
+
"""Request to verify login code."""
|
|
154
|
+
email: EmailStr
|
|
155
|
+
code: str
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.post("/email/send-code")
|
|
159
|
+
async def send_email_code(request: Request, body: EmailSendCodeRequest):
|
|
160
|
+
"""
|
|
161
|
+
Send a login code to an email address.
|
|
162
|
+
|
|
163
|
+
Creates user if not exists (using deterministic UUID from email).
|
|
164
|
+
Stores code in user metadata with expiry.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
request: FastAPI request
|
|
168
|
+
body: EmailSendCodeRequest with email
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Success status and message
|
|
172
|
+
"""
|
|
173
|
+
if not settings.email.is_configured:
|
|
174
|
+
raise HTTPException(
|
|
175
|
+
status_code=501,
|
|
176
|
+
detail="Email authentication is not configured"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Get database connection
|
|
180
|
+
if not settings.postgres.enabled:
|
|
181
|
+
raise HTTPException(
|
|
182
|
+
status_code=501,
|
|
183
|
+
detail="Database is required for email authentication"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
db = PostgresService()
|
|
187
|
+
try:
|
|
188
|
+
await db.connect()
|
|
189
|
+
|
|
190
|
+
# Initialize email auth provider
|
|
191
|
+
email_auth = EmailAuthProvider()
|
|
192
|
+
|
|
193
|
+
# Send code
|
|
194
|
+
result = await email_auth.send_code(
|
|
195
|
+
email=body.email,
|
|
196
|
+
db=db,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if result.success:
|
|
200
|
+
return {
|
|
201
|
+
"success": True,
|
|
202
|
+
"message": result.message,
|
|
203
|
+
"email": result.email,
|
|
204
|
+
}
|
|
205
|
+
else:
|
|
206
|
+
raise HTTPException(
|
|
207
|
+
status_code=400,
|
|
208
|
+
detail=result.message or result.error
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
except HTTPException:
|
|
212
|
+
raise
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error(f"Error sending login code: {e}")
|
|
215
|
+
raise HTTPException(status_code=500, detail="Failed to send login code")
|
|
216
|
+
finally:
|
|
217
|
+
await db.disconnect()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@router.post("/email/verify")
|
|
221
|
+
async def verify_email_code(request: Request, body: EmailVerifyRequest):
|
|
222
|
+
"""
|
|
223
|
+
Verify login code and create session with JWT tokens.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
request: FastAPI request
|
|
227
|
+
body: EmailVerifyRequest with email and code
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Success status with user info and JWT tokens
|
|
231
|
+
"""
|
|
232
|
+
if not settings.email.is_configured:
|
|
233
|
+
raise HTTPException(
|
|
234
|
+
status_code=501,
|
|
235
|
+
detail="Email authentication is not configured"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not settings.postgres.enabled:
|
|
239
|
+
raise HTTPException(
|
|
240
|
+
status_code=501,
|
|
241
|
+
detail="Database is required for email authentication"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
db = PostgresService()
|
|
245
|
+
try:
|
|
246
|
+
await db.connect()
|
|
247
|
+
|
|
248
|
+
# Initialize email auth provider
|
|
249
|
+
email_auth = EmailAuthProvider()
|
|
250
|
+
|
|
251
|
+
# Verify code
|
|
252
|
+
result = await email_auth.verify_code(
|
|
253
|
+
email=body.email,
|
|
254
|
+
code=body.code,
|
|
255
|
+
db=db,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if not result.success:
|
|
259
|
+
raise HTTPException(
|
|
260
|
+
status_code=400,
|
|
261
|
+
detail=result.message or result.error
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Create session - compatible with OAuth session format
|
|
265
|
+
user_dict = email_auth.get_user_dict(
|
|
266
|
+
email=result.email,
|
|
267
|
+
user_id=result.user_id,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Fetch actual user data from database to get role/tier
|
|
271
|
+
user_service = UserService(db)
|
|
272
|
+
try:
|
|
273
|
+
user_entity = await user_service.get_user_by_id(result.user_id)
|
|
274
|
+
if user_entity:
|
|
275
|
+
# Override defaults with actual database values
|
|
276
|
+
user_dict["role"] = user_entity.role or "user"
|
|
277
|
+
user_dict["roles"] = [user_entity.role] if user_entity.role else ["user"]
|
|
278
|
+
user_dict["tier"] = user_entity.tier.value if user_entity.tier else "free"
|
|
279
|
+
user_dict["name"] = user_entity.name or user_dict["name"]
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.warning(f"Could not fetch user details: {e}")
|
|
282
|
+
# Continue with defaults from get_user_dict
|
|
283
|
+
|
|
284
|
+
# Generate JWT tokens
|
|
285
|
+
jwt_service = get_jwt_service()
|
|
286
|
+
tokens = jwt_service.create_tokens(user_dict)
|
|
287
|
+
|
|
288
|
+
# Store user in session (for backward compatibility)
|
|
289
|
+
request.session["user"] = user_dict
|
|
290
|
+
|
|
291
|
+
logger.info(f"User authenticated via email: {result.email}")
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"success": True,
|
|
295
|
+
"message": result.message,
|
|
296
|
+
"user": user_dict,
|
|
297
|
+
# JWT tokens for stateless auth
|
|
298
|
+
"access_token": tokens["access_token"],
|
|
299
|
+
"refresh_token": tokens["refresh_token"],
|
|
300
|
+
"token_type": tokens["token_type"],
|
|
301
|
+
"expires_in": tokens["expires_in"],
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
except HTTPException:
|
|
305
|
+
raise
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Error verifying login code: {e}")
|
|
308
|
+
raise HTTPException(status_code=500, detail="Failed to verify login code")
|
|
309
|
+
finally:
|
|
310
|
+
await db.disconnect()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# =============================================================================
|
|
314
|
+
# OAuth Authentication Endpoints
|
|
315
|
+
# =============================================================================
|
|
316
|
+
|
|
317
|
+
|
|
90
318
|
@router.get("/{provider}/login")
|
|
91
319
|
async def login(provider: str, request: Request):
|
|
92
320
|
"""
|
|
@@ -268,7 +496,7 @@ async def logout(request: Request):
|
|
|
268
496
|
@router.get("/me")
|
|
269
497
|
async def me(request: Request):
|
|
270
498
|
"""
|
|
271
|
-
Get current user information from session.
|
|
499
|
+
Get current user information from session or JWT.
|
|
272
500
|
|
|
273
501
|
Args:
|
|
274
502
|
request: FastAPI request
|
|
@@ -276,6 +504,16 @@ async def me(request: Request):
|
|
|
276
504
|
Returns:
|
|
277
505
|
User information or 401 if not authenticated
|
|
278
506
|
"""
|
|
507
|
+
# First check for JWT in Authorization header
|
|
508
|
+
auth_header = request.headers.get("Authorization")
|
|
509
|
+
if auth_header and auth_header.startswith("Bearer "):
|
|
510
|
+
token = auth_header[7:]
|
|
511
|
+
jwt_service = get_jwt_service()
|
|
512
|
+
user = jwt_service.verify_token(token)
|
|
513
|
+
if user:
|
|
514
|
+
return user
|
|
515
|
+
|
|
516
|
+
# Fall back to session
|
|
279
517
|
user = request.session.get("user")
|
|
280
518
|
if not user:
|
|
281
519
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
|
@@ -283,6 +521,69 @@ async def me(request: Request):
|
|
|
283
521
|
return user
|
|
284
522
|
|
|
285
523
|
|
|
524
|
+
# =============================================================================
|
|
525
|
+
# JWT Token Endpoints
|
|
526
|
+
# =============================================================================
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class TokenRefreshRequest(BaseModel):
|
|
530
|
+
"""Request to refresh access token."""
|
|
531
|
+
refresh_token: str
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@router.post("/token/refresh")
|
|
535
|
+
async def refresh_token(body: TokenRefreshRequest):
|
|
536
|
+
"""
|
|
537
|
+
Refresh access token using refresh token.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
body: TokenRefreshRequest with refresh_token
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
New access token or 401 if refresh token is invalid
|
|
544
|
+
"""
|
|
545
|
+
jwt_service = get_jwt_service()
|
|
546
|
+
result = jwt_service.refresh_access_token(body.refresh_token)
|
|
547
|
+
|
|
548
|
+
if not result:
|
|
549
|
+
raise HTTPException(
|
|
550
|
+
status_code=401,
|
|
551
|
+
detail="Invalid or expired refresh token"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
return result
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@router.post("/token/verify")
|
|
558
|
+
async def verify_token(request: Request):
|
|
559
|
+
"""
|
|
560
|
+
Verify an access token is valid.
|
|
561
|
+
|
|
562
|
+
Pass the token in the Authorization header: Bearer <token>
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
User info if valid, 401 if invalid
|
|
566
|
+
"""
|
|
567
|
+
auth_header = request.headers.get("Authorization")
|
|
568
|
+
if not auth_header or not auth_header.startswith("Bearer "):
|
|
569
|
+
raise HTTPException(
|
|
570
|
+
status_code=401,
|
|
571
|
+
detail="Missing Authorization header"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
token = auth_header[7:]
|
|
575
|
+
jwt_service = get_jwt_service()
|
|
576
|
+
user = jwt_service.verify_token(token)
|
|
577
|
+
|
|
578
|
+
if not user:
|
|
579
|
+
raise HTTPException(
|
|
580
|
+
status_code=401,
|
|
581
|
+
detail="Invalid or expired token"
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
return {"valid": True, "user": user}
|
|
585
|
+
|
|
586
|
+
|
|
286
587
|
# =============================================================================
|
|
287
588
|
# Development Token Endpoints (non-production only)
|
|
288
589
|
# =============================================================================
|
|
@@ -351,3 +652,43 @@ async def get_dev_token(request: Request):
|
|
|
351
652
|
"usage": f'curl -H "Authorization: Bearer {token}" http://localhost:8000/api/v1/...',
|
|
352
653
|
"warning": "This token is for development/testing only and will not work in production.",
|
|
353
654
|
}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
@router.get("/dev/mock-code/{email}")
|
|
658
|
+
async def get_mock_code(email: str, request: Request):
|
|
659
|
+
"""
|
|
660
|
+
Get the mock login code for testing (non-production only).
|
|
661
|
+
|
|
662
|
+
This endpoint retrieves the code that was "sent" via email in mock mode.
|
|
663
|
+
Use this for automated testing without real email delivery.
|
|
664
|
+
|
|
665
|
+
Usage:
|
|
666
|
+
1. POST /api/auth/email/send-code with email
|
|
667
|
+
2. GET /api/auth/dev/mock-code/{email} to retrieve the code
|
|
668
|
+
3. POST /api/auth/email/verify with email and code
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
401 if in production environment
|
|
672
|
+
404 if no code found for the email
|
|
673
|
+
The code and email otherwise
|
|
674
|
+
"""
|
|
675
|
+
if settings.environment == "production":
|
|
676
|
+
raise HTTPException(
|
|
677
|
+
status_code=401,
|
|
678
|
+
detail="Mock codes are not available in production"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
from ...services.email import EmailService
|
|
682
|
+
|
|
683
|
+
code = EmailService.get_mock_code(email)
|
|
684
|
+
if not code:
|
|
685
|
+
raise HTTPException(
|
|
686
|
+
status_code=404,
|
|
687
|
+
detail=f"No mock code found for {email}. Send a code first."
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
"email": email,
|
|
692
|
+
"code": code,
|
|
693
|
+
"warning": "This endpoint is for testing only and will not work in production.",
|
|
694
|
+
}
|
|
@@ -330,8 +330,8 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
|
|
|
330
330
|
- Useful for A/B testing, model comparison, and feedback collection
|
|
331
331
|
"""
|
|
332
332
|
# Load agent schema: use header value from context or default
|
|
333
|
-
# Extract AgentContext
|
|
334
|
-
temp_context = AgentContext.
|
|
333
|
+
# Extract AgentContext from request (gets user_id from JWT token)
|
|
334
|
+
temp_context = AgentContext.from_request(request)
|
|
335
335
|
schema_name = temp_context.agent_schema_uri or DEFAULT_AGENT_SCHEMA
|
|
336
336
|
|
|
337
337
|
# Resolve model: use body.model if provided, otherwise settings default
|
|
@@ -350,6 +350,7 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
|
|
|
350
350
|
context, messages = await ContextBuilder.build_from_headers(
|
|
351
351
|
headers=dict(request.headers),
|
|
352
352
|
new_messages=new_messages,
|
|
353
|
+
user_id=temp_context.user_id, # From JWT token (source of truth)
|
|
353
354
|
)
|
|
354
355
|
|
|
355
356
|
# Ensure session exists with metadata and eval mode if applicable
|
|
@@ -509,6 +510,7 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
|
|
|
509
510
|
context, messages = await ContextBuilder.build_from_headers(
|
|
510
511
|
headers=dict(request.headers),
|
|
511
512
|
new_messages=new_messages,
|
|
513
|
+
user_id=temp_context.user_id, # From JWT token (source of truth)
|
|
512
514
|
)
|
|
513
515
|
|
|
514
516
|
logger.info(f"Built context with {len(messages)} total messages (includes history + user context)")
|