remdb 0.3.180__py3-none-any.whl → 0.3.258__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.
- rem/agentic/README.md +36 -2
- rem/agentic/__init__.py +10 -1
- rem/agentic/context.py +185 -1
- rem/agentic/context_builder.py +56 -35
- rem/agentic/mcp/tool_wrapper.py +2 -2
- rem/agentic/providers/pydantic_ai.py +303 -111
- rem/agentic/schema.py +2 -2
- rem/api/main.py +1 -1
- rem/api/mcp_router/resources.py +223 -0
- rem/api/mcp_router/server.py +4 -0
- rem/api/mcp_router/tools.py +608 -166
- rem/api/routers/admin.py +30 -4
- rem/api/routers/auth.py +219 -20
- rem/api/routers/chat/child_streaming.py +393 -0
- rem/api/routers/chat/completions.py +77 -40
- rem/api/routers/chat/sse_events.py +7 -3
- rem/api/routers/chat/streaming.py +381 -291
- rem/api/routers/chat/streaming_utils.py +325 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +7 -1
- rem/api/routers/feedback.py +11 -3
- rem/api/routers/messages.py +176 -38
- rem/api/routers/models.py +9 -1
- rem/api/routers/query.py +17 -15
- rem/api/routers/shared_sessions.py +16 -0
- rem/auth/jwt.py +19 -4
- rem/auth/middleware.py +42 -28
- rem/cli/README.md +62 -0
- rem/cli/commands/ask.py +205 -114
- rem/cli/commands/db.py +55 -31
- rem/cli/commands/experiments.py +1 -1
- rem/cli/commands/process.py +179 -43
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/session.py +117 -0
- rem/cli/main.py +2 -0
- rem/models/core/experiment.py +1 -1
- rem/models/entities/ontology.py +18 -20
- rem/models/entities/session.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +1 -1
- rem/schemas/agents/rem.yaml +1 -1
- rem/schemas/agents/test_orchestrator.yaml +42 -0
- rem/schemas/agents/test_structured_output.yaml +52 -0
- rem/services/content/providers.py +151 -49
- rem/services/content/service.py +18 -5
- rem/services/embeddings/worker.py +26 -12
- rem/services/postgres/__init__.py +28 -3
- rem/services/postgres/diff_service.py +57 -5
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
- rem/services/postgres/register_type.py +11 -10
- rem/services/postgres/repository.py +39 -28
- rem/services/postgres/schema_generator.py +5 -5
- rem/services/postgres/sql_builder.py +6 -5
- rem/services/rem/README.md +4 -3
- rem/services/rem/parser.py +7 -10
- rem/services/rem/service.py +47 -0
- rem/services/session/__init__.py +8 -1
- rem/services/session/compression.py +47 -5
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +2 -1
- rem/settings.py +92 -7
- rem/sql/migrations/001_install.sql +125 -7
- rem/sql/migrations/002_install_models.sql +159 -149
- rem/sql/migrations/004_cache_system.sql +10 -276
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/schema_loader.py +180 -120
- {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/METADATA +7 -6
- {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/RECORD +70 -61
- {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/WHEEL +0 -0
- {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/entry_points.txt +0 -0
rem/api/routers/admin.py
CHANGED
|
@@ -31,6 +31,8 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Background
|
|
|
31
31
|
from loguru import logger
|
|
32
32
|
from pydantic import BaseModel
|
|
33
33
|
|
|
34
|
+
from .common import ErrorResponse
|
|
35
|
+
|
|
34
36
|
from ..deps import require_admin
|
|
35
37
|
from ...models.entities import Message, Session, SessionMode
|
|
36
38
|
from ...services.postgres import Repository
|
|
@@ -103,7 +105,13 @@ class SystemStats(BaseModel):
|
|
|
103
105
|
# =============================================================================
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
@router.get(
|
|
108
|
+
@router.get(
|
|
109
|
+
"/users",
|
|
110
|
+
response_model=UserListResponse,
|
|
111
|
+
responses={
|
|
112
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
113
|
+
},
|
|
114
|
+
)
|
|
107
115
|
async def list_all_users(
|
|
108
116
|
user: dict = Depends(require_admin),
|
|
109
117
|
limit: int = Query(default=50, ge=1, le=100),
|
|
@@ -155,7 +163,13 @@ async def list_all_users(
|
|
|
155
163
|
return UserListResponse(data=summaries, total=total, has_more=has_more)
|
|
156
164
|
|
|
157
165
|
|
|
158
|
-
@router.get(
|
|
166
|
+
@router.get(
|
|
167
|
+
"/sessions",
|
|
168
|
+
response_model=SessionListResponse,
|
|
169
|
+
responses={
|
|
170
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
171
|
+
},
|
|
172
|
+
)
|
|
159
173
|
async def list_all_sessions(
|
|
160
174
|
user: dict = Depends(require_admin),
|
|
161
175
|
user_id: str | None = Query(default=None, description="Filter by user ID"),
|
|
@@ -202,7 +216,13 @@ async def list_all_sessions(
|
|
|
202
216
|
return SessionListResponse(data=sessions, total=total, has_more=has_more)
|
|
203
217
|
|
|
204
218
|
|
|
205
|
-
@router.get(
|
|
219
|
+
@router.get(
|
|
220
|
+
"/messages",
|
|
221
|
+
response_model=MessageListResponse,
|
|
222
|
+
responses={
|
|
223
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
224
|
+
},
|
|
225
|
+
)
|
|
206
226
|
async def list_all_messages(
|
|
207
227
|
user: dict = Depends(require_admin),
|
|
208
228
|
user_id: str | None = Query(default=None, description="Filter by user ID"),
|
|
@@ -252,7 +272,13 @@ async def list_all_messages(
|
|
|
252
272
|
return MessageListResponse(data=messages, total=total, has_more=has_more)
|
|
253
273
|
|
|
254
274
|
|
|
255
|
-
@router.get(
|
|
275
|
+
@router.get(
|
|
276
|
+
"/stats",
|
|
277
|
+
response_model=SystemStats,
|
|
278
|
+
responses={
|
|
279
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
280
|
+
},
|
|
281
|
+
)
|
|
256
282
|
async def get_system_stats(
|
|
257
283
|
user: dict = Depends(require_admin),
|
|
258
284
|
) -> SystemStats:
|
rem/api/routers/auth.py
CHANGED
|
@@ -3,11 +3,12 @@ Authentication Router.
|
|
|
3
3
|
|
|
4
4
|
Supports multiple authentication methods:
|
|
5
5
|
1. Email (passwordless): POST /api/auth/email/send-code, POST /api/auth/email/verify
|
|
6
|
-
2.
|
|
6
|
+
2. Pre-approved codes: POST /api/auth/email/verify (with pre-approved code, no send-code needed)
|
|
7
|
+
3. OAuth (Google, Microsoft): GET /api/auth/{provider}/login, GET /api/auth/{provider}/callback
|
|
7
8
|
|
|
8
9
|
Endpoints:
|
|
9
10
|
- POST /api/auth/email/send-code - Send login code to email
|
|
10
|
-
- POST /api/auth/email/verify - Verify code and create session
|
|
11
|
+
- POST /api/auth/email/verify - Verify code and create session (supports pre-approved codes)
|
|
11
12
|
- GET /api/auth/{provider}/login - Initiate OAuth flow
|
|
12
13
|
- GET /api/auth/{provider}/callback - OAuth callback
|
|
13
14
|
- POST /api/auth/logout - Clear session
|
|
@@ -15,9 +16,39 @@ Endpoints:
|
|
|
15
16
|
|
|
16
17
|
Supported providers:
|
|
17
18
|
- email: Passwordless email login
|
|
19
|
+
- preapproved: Pre-approved codes (bypass email, set via AUTH__PREAPPROVED_CODES)
|
|
18
20
|
- google: Google OAuth 2.0 / OIDC
|
|
19
21
|
- microsoft: Microsoft Entra ID OIDC
|
|
20
22
|
|
|
23
|
+
=============================================================================
|
|
24
|
+
Pre-Approved Code Authentication
|
|
25
|
+
=============================================================================
|
|
26
|
+
|
|
27
|
+
Pre-approved codes allow login without email verification. Useful for:
|
|
28
|
+
- Demo accounts
|
|
29
|
+
- Testing
|
|
30
|
+
- Beta access codes
|
|
31
|
+
- Admin provisioning
|
|
32
|
+
|
|
33
|
+
Configuration:
|
|
34
|
+
AUTH__PREAPPROVED_CODES=A12345,A67890,B11111,B22222
|
|
35
|
+
|
|
36
|
+
Code prefixes:
|
|
37
|
+
A = Admin role (e.g., A12345, AADMIN1)
|
|
38
|
+
B = Normal user role (e.g., B11111, BUSER1)
|
|
39
|
+
|
|
40
|
+
Flow:
|
|
41
|
+
1. User enters email + pre-approved code (no send-code step needed)
|
|
42
|
+
2. POST /api/auth/email/verify with email and code
|
|
43
|
+
3. System validates code against AUTH__PREAPPROVED_CODES
|
|
44
|
+
4. Creates user if not exists, sets role based on prefix
|
|
45
|
+
5. Returns JWT tokens (same as email auth)
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
curl -X POST http://localhost:8000/api/auth/email/verify \
|
|
49
|
+
-H "Content-Type: application/json" \
|
|
50
|
+
-d '{"email": "admin@example.com", "code": "A12345"}'
|
|
51
|
+
|
|
21
52
|
=============================================================================
|
|
22
53
|
Email Authentication Access Control
|
|
23
54
|
=============================================================================
|
|
@@ -52,7 +83,7 @@ User Tiers (models.entities.UserTier):
|
|
|
52
83
|
|
|
53
84
|
Configuration:
|
|
54
85
|
# Allow only specific domains for new signups
|
|
55
|
-
EMAIL__TRUSTED_EMAIL_DOMAINS=
|
|
86
|
+
EMAIL__TRUSTED_EMAIL_DOMAINS=mycompany.com,example.com
|
|
56
87
|
|
|
57
88
|
# Allow all domains (no restrictions)
|
|
58
89
|
EMAIL__TRUSTED_EMAIL_DOMAINS=
|
|
@@ -101,6 +132,8 @@ from authlib.integrations.starlette_client import OAuth
|
|
|
101
132
|
from pydantic import BaseModel, EmailStr
|
|
102
133
|
from loguru import logger
|
|
103
134
|
|
|
135
|
+
from .common import ErrorResponse
|
|
136
|
+
|
|
104
137
|
from ...settings import settings
|
|
105
138
|
from ...services.postgres.service import PostgresService
|
|
106
139
|
from ...services.user_service import UserService
|
|
@@ -159,7 +192,14 @@ class EmailVerifyRequest(BaseModel):
|
|
|
159
192
|
code: str
|
|
160
193
|
|
|
161
194
|
|
|
162
|
-
@router.post(
|
|
195
|
+
@router.post(
|
|
196
|
+
"/email/send-code",
|
|
197
|
+
responses={
|
|
198
|
+
400: {"model": ErrorResponse, "description": "Invalid request or email rejected"},
|
|
199
|
+
500: {"model": ErrorResponse, "description": "Failed to send login code"},
|
|
200
|
+
501: {"model": ErrorResponse, "description": "Email auth or database not configured"},
|
|
201
|
+
},
|
|
202
|
+
)
|
|
163
203
|
async def send_email_code(request: Request, body: EmailSendCodeRequest):
|
|
164
204
|
"""
|
|
165
205
|
Send a login code to an email address.
|
|
@@ -221,11 +261,24 @@ async def send_email_code(request: Request, body: EmailSendCodeRequest):
|
|
|
221
261
|
await db.disconnect()
|
|
222
262
|
|
|
223
263
|
|
|
224
|
-
@router.post(
|
|
264
|
+
@router.post(
|
|
265
|
+
"/email/verify",
|
|
266
|
+
responses={
|
|
267
|
+
400: {"model": ErrorResponse, "description": "Invalid or expired code"},
|
|
268
|
+
500: {"model": ErrorResponse, "description": "Failed to verify login code"},
|
|
269
|
+
501: {"model": ErrorResponse, "description": "Email auth or database not configured"},
|
|
270
|
+
},
|
|
271
|
+
)
|
|
225
272
|
async def verify_email_code(request: Request, body: EmailVerifyRequest):
|
|
226
273
|
"""
|
|
227
274
|
Verify login code and create session with JWT tokens.
|
|
228
275
|
|
|
276
|
+
Supports two authentication methods:
|
|
277
|
+
1. Pre-approved codes: Codes from AUTH__PREAPPROVED_CODES bypass email verification.
|
|
278
|
+
- A prefix = admin role, B prefix = normal user role
|
|
279
|
+
- Creates user if not exists, logs in directly
|
|
280
|
+
2. Email verification: Standard 6-digit code sent via email
|
|
281
|
+
|
|
229
282
|
Args:
|
|
230
283
|
request: FastAPI request
|
|
231
284
|
body: EmailVerifyRequest with email and code
|
|
@@ -233,12 +286,6 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
|
|
|
233
286
|
Returns:
|
|
234
287
|
Success status with user info and JWT tokens
|
|
235
288
|
"""
|
|
236
|
-
if not settings.email.is_configured:
|
|
237
|
-
raise HTTPException(
|
|
238
|
-
status_code=501,
|
|
239
|
-
detail="Email authentication is not configured"
|
|
240
|
-
)
|
|
241
|
-
|
|
242
289
|
if not settings.postgres.enabled:
|
|
243
290
|
raise HTTPException(
|
|
244
291
|
status_code=501,
|
|
@@ -248,6 +295,79 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
|
|
|
248
295
|
db = PostgresService()
|
|
249
296
|
try:
|
|
250
297
|
await db.connect()
|
|
298
|
+
user_service = UserService(db)
|
|
299
|
+
|
|
300
|
+
# Check for pre-approved code first
|
|
301
|
+
preapproved = settings.auth.check_preapproved_code(body.code)
|
|
302
|
+
if preapproved:
|
|
303
|
+
logger.info(f"Pre-approved code login attempt for {body.email} (role: {preapproved['role']})")
|
|
304
|
+
|
|
305
|
+
# Get or create user with pre-approved role
|
|
306
|
+
user_id = email_to_user_id(body.email)
|
|
307
|
+
user_entity = await user_service.get_user_by_id(user_id)
|
|
308
|
+
|
|
309
|
+
if not user_entity:
|
|
310
|
+
# Create new user with role from pre-approved code
|
|
311
|
+
user_entity = await user_service.get_or_create_user(
|
|
312
|
+
email=body.email,
|
|
313
|
+
name=body.email.split("@")[0],
|
|
314
|
+
tenant_id="default",
|
|
315
|
+
)
|
|
316
|
+
# Update role based on pre-approved code prefix
|
|
317
|
+
user_entity.role = preapproved["role"]
|
|
318
|
+
from ...services.postgres.repository import Repository
|
|
319
|
+
from ...models.entities.user import User
|
|
320
|
+
user_repo = Repository(User, "users", db=db)
|
|
321
|
+
await user_repo.upsert(user_entity)
|
|
322
|
+
logger.info(f"Created user {body.email} with role={preapproved['role']} via pre-approved code")
|
|
323
|
+
else:
|
|
324
|
+
# Update existing user's role if admin code used
|
|
325
|
+
if preapproved["role"] == "admin" and user_entity.role != "admin":
|
|
326
|
+
user_entity.role = "admin"
|
|
327
|
+
from ...services.postgres.repository import Repository
|
|
328
|
+
from ...models.entities.user import User
|
|
329
|
+
user_repo = Repository(User, "users", db=db)
|
|
330
|
+
await user_repo.upsert(user_entity)
|
|
331
|
+
logger.info(f"Upgraded user {body.email} to admin via pre-approved code")
|
|
332
|
+
|
|
333
|
+
# Build user dict for session/JWT
|
|
334
|
+
user_dict = {
|
|
335
|
+
"id": str(user_entity.id),
|
|
336
|
+
"email": body.email,
|
|
337
|
+
"email_verified": True,
|
|
338
|
+
"name": user_entity.name or body.email.split("@")[0],
|
|
339
|
+
"provider": "preapproved",
|
|
340
|
+
"tenant_id": user_entity.tenant_id or "default",
|
|
341
|
+
"tier": user_entity.tier.value if user_entity.tier else "free",
|
|
342
|
+
"role": user_entity.role or preapproved["role"],
|
|
343
|
+
"roles": [user_entity.role or preapproved["role"]],
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Generate JWT tokens
|
|
347
|
+
jwt_service = get_jwt_service()
|
|
348
|
+
tokens = jwt_service.create_tokens(user_dict)
|
|
349
|
+
|
|
350
|
+
# Store user in session
|
|
351
|
+
request.session["user"] = user_dict
|
|
352
|
+
|
|
353
|
+
logger.info(f"User authenticated via pre-approved code: {body.email} (role: {user_dict['role']})")
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
"success": True,
|
|
357
|
+
"message": "Successfully authenticated with pre-approved code!",
|
|
358
|
+
"user": user_dict,
|
|
359
|
+
"access_token": tokens["access_token"],
|
|
360
|
+
"refresh_token": tokens["refresh_token"],
|
|
361
|
+
"token_type": tokens["token_type"],
|
|
362
|
+
"expires_in": tokens["expires_in"],
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# Standard email verification flow
|
|
366
|
+
if not settings.email.is_configured:
|
|
367
|
+
raise HTTPException(
|
|
368
|
+
status_code=501,
|
|
369
|
+
detail="Email authentication is not configured"
|
|
370
|
+
)
|
|
251
371
|
|
|
252
372
|
# Initialize email auth provider
|
|
253
373
|
email_auth = EmailAuthProvider()
|
|
@@ -272,7 +392,6 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
|
|
|
272
392
|
)
|
|
273
393
|
|
|
274
394
|
# Fetch actual user data from database to get role/tier
|
|
275
|
-
user_service = UserService(db)
|
|
276
395
|
try:
|
|
277
396
|
user_entity = await user_service.get_user_by_id(result.user_id)
|
|
278
397
|
if user_entity:
|
|
@@ -319,7 +438,13 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
|
|
|
319
438
|
# =============================================================================
|
|
320
439
|
|
|
321
440
|
|
|
322
|
-
@router.get(
|
|
441
|
+
@router.get(
|
|
442
|
+
"/{provider}/login",
|
|
443
|
+
responses={
|
|
444
|
+
400: {"model": ErrorResponse, "description": "Unknown OAuth provider"},
|
|
445
|
+
501: {"model": ErrorResponse, "description": "Authentication is disabled"},
|
|
446
|
+
},
|
|
447
|
+
)
|
|
323
448
|
async def login(provider: str, request: Request):
|
|
324
449
|
"""
|
|
325
450
|
Initiate OAuth flow with provider.
|
|
@@ -361,7 +486,13 @@ async def login(provider: str, request: Request):
|
|
|
361
486
|
return await client.authorize_redirect(request, redirect_uri)
|
|
362
487
|
|
|
363
488
|
|
|
364
|
-
@router.get(
|
|
489
|
+
@router.get(
|
|
490
|
+
"/{provider}/callback",
|
|
491
|
+
responses={
|
|
492
|
+
400: {"model": ErrorResponse, "description": "Authentication failed or unknown provider"},
|
|
493
|
+
501: {"model": ErrorResponse, "description": "Authentication is disabled"},
|
|
494
|
+
},
|
|
495
|
+
)
|
|
365
496
|
async def callback(provider: str, request: Request):
|
|
366
497
|
"""
|
|
367
498
|
OAuth callback endpoint.
|
|
@@ -498,7 +629,12 @@ async def logout(request: Request):
|
|
|
498
629
|
return {"message": "Logged out successfully"}
|
|
499
630
|
|
|
500
631
|
|
|
501
|
-
@router.get(
|
|
632
|
+
@router.get(
|
|
633
|
+
"/me",
|
|
634
|
+
responses={
|
|
635
|
+
401: {"model": ErrorResponse, "description": "Not authenticated"},
|
|
636
|
+
},
|
|
637
|
+
)
|
|
502
638
|
async def me(request: Request):
|
|
503
639
|
"""
|
|
504
640
|
Get current user information from session or JWT.
|
|
@@ -536,11 +672,19 @@ class TokenRefreshRequest(BaseModel):
|
|
|
536
672
|
refresh_token: str
|
|
537
673
|
|
|
538
674
|
|
|
539
|
-
@router.post(
|
|
675
|
+
@router.post(
|
|
676
|
+
"/token/refresh",
|
|
677
|
+
responses={
|
|
678
|
+
401: {"model": ErrorResponse, "description": "Invalid or expired refresh token"},
|
|
679
|
+
},
|
|
680
|
+
)
|
|
540
681
|
async def refresh_token(body: TokenRefreshRequest):
|
|
541
682
|
"""
|
|
542
683
|
Refresh access token using refresh token.
|
|
543
684
|
|
|
685
|
+
Fetches the user's current role/tier from the database to ensure
|
|
686
|
+
the new access token reflects their actual permissions.
|
|
687
|
+
|
|
544
688
|
Args:
|
|
545
689
|
body: TokenRefreshRequest with refresh_token
|
|
546
690
|
|
|
@@ -548,7 +692,46 @@ async def refresh_token(body: TokenRefreshRequest):
|
|
|
548
692
|
New access token or 401 if refresh token is invalid
|
|
549
693
|
"""
|
|
550
694
|
jwt_service = get_jwt_service()
|
|
551
|
-
|
|
695
|
+
|
|
696
|
+
# First decode the refresh token to get user_id (without full verification yet)
|
|
697
|
+
payload = jwt_service.decode_without_verification(body.refresh_token)
|
|
698
|
+
if not payload:
|
|
699
|
+
raise HTTPException(
|
|
700
|
+
status_code=401,
|
|
701
|
+
detail="Invalid refresh token format"
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
user_id = payload.get("sub")
|
|
705
|
+
if not user_id:
|
|
706
|
+
raise HTTPException(
|
|
707
|
+
status_code=401,
|
|
708
|
+
detail="Invalid refresh token: missing user ID"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Fetch user from database to get current role/tier
|
|
712
|
+
user_override = None
|
|
713
|
+
if settings.postgres.enabled:
|
|
714
|
+
db = PostgresService()
|
|
715
|
+
try:
|
|
716
|
+
await db.connect()
|
|
717
|
+
user_service = UserService(db)
|
|
718
|
+
user_entity = await user_service.get_user_by_id(user_id)
|
|
719
|
+
if user_entity:
|
|
720
|
+
user_override = {
|
|
721
|
+
"role": user_entity.role or "user",
|
|
722
|
+
"roles": [user_entity.role] if user_entity.role else ["user"],
|
|
723
|
+
"tier": user_entity.tier.value if user_entity.tier else "free",
|
|
724
|
+
"name": user_entity.name,
|
|
725
|
+
}
|
|
726
|
+
logger.debug(f"Refresh token: fetched user {user_id} with role={user_override['role']}, tier={user_override['tier']}")
|
|
727
|
+
except Exception as e:
|
|
728
|
+
logger.warning(f"Could not fetch user for token refresh: {e}")
|
|
729
|
+
# Continue without override - will use defaults
|
|
730
|
+
finally:
|
|
731
|
+
await db.disconnect()
|
|
732
|
+
|
|
733
|
+
# Now do the actual refresh with proper verification
|
|
734
|
+
result = jwt_service.refresh_access_token(body.refresh_token, user_override=user_override)
|
|
552
735
|
|
|
553
736
|
if not result:
|
|
554
737
|
raise HTTPException(
|
|
@@ -559,7 +742,12 @@ async def refresh_token(body: TokenRefreshRequest):
|
|
|
559
742
|
return result
|
|
560
743
|
|
|
561
744
|
|
|
562
|
-
@router.post(
|
|
745
|
+
@router.post(
|
|
746
|
+
"/token/verify",
|
|
747
|
+
responses={
|
|
748
|
+
401: {"model": ErrorResponse, "description": "Missing, invalid, or expired token"},
|
|
749
|
+
},
|
|
750
|
+
)
|
|
563
751
|
async def verify_token(request: Request):
|
|
564
752
|
"""
|
|
565
753
|
Verify an access token is valid.
|
|
@@ -623,7 +811,12 @@ def verify_dev_token(token: str) -> bool:
|
|
|
623
811
|
return token == expected
|
|
624
812
|
|
|
625
813
|
|
|
626
|
-
@router.get(
|
|
814
|
+
@router.get(
|
|
815
|
+
"/dev/token",
|
|
816
|
+
responses={
|
|
817
|
+
401: {"model": ErrorResponse, "description": "Dev tokens not available in production"},
|
|
818
|
+
},
|
|
819
|
+
)
|
|
627
820
|
async def get_dev_token(request: Request):
|
|
628
821
|
"""
|
|
629
822
|
Get a development token for testing (non-production only).
|
|
@@ -659,7 +852,13 @@ async def get_dev_token(request: Request):
|
|
|
659
852
|
}
|
|
660
853
|
|
|
661
854
|
|
|
662
|
-
@router.get(
|
|
855
|
+
@router.get(
|
|
856
|
+
"/dev/mock-code/{email}",
|
|
857
|
+
responses={
|
|
858
|
+
401: {"model": ErrorResponse, "description": "Mock codes not available in production"},
|
|
859
|
+
404: {"model": ErrorResponse, "description": "No code found for email"},
|
|
860
|
+
},
|
|
861
|
+
)
|
|
663
862
|
async def get_mock_code(email: str, request: Request):
|
|
664
863
|
"""
|
|
665
864
|
Get the mock login code for testing (non-production only).
|