remdb 0.3.14__py3-none-any.whl → 0.3.157__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 +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +32 -2
- rem/agentic/agents/agent_manager.py +310 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +51 -27
- rem/agentic/context_builder.py +5 -3
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/tool_wrapper.py +155 -18
- rem/agentic/otel/setup.py +93 -4
- rem/agentic/providers/phoenix.py +371 -108
- rem/agentic/providers/pydantic_ai.py +280 -57
- rem/agentic/schema.py +361 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/README.md +215 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +132 -40
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +28 -5
- rem/api/mcp_router/tools.py +555 -7
- rem/api/routers/admin.py +494 -0
- rem/api/routers/auth.py +278 -4
- rem/api/routers/chat/completions.py +402 -20
- rem/api/routers/chat/models.py +88 -10
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +542 -0
- rem/api/routers/chat/streaming.py +697 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +268 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/__init__.py +13 -3
- rem/auth/middleware.py +186 -22
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +4 -7
- rem/cli/commands/db.py +386 -143
- rem/cli/commands/experiments.py +468 -76
- rem/cli/commands/process.py +14 -8
- rem/cli/commands/schema.py +97 -50
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +29 -6
- rem/config.py +10 -3
- rem/models/core/core_model.py +7 -1
- rem/models/core/experiment.py +58 -14
- rem/models/core/rem_query.py +5 -2
- rem/models/entities/__init__.py +25 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/message.py +30 -1
- rem/models/entities/ontology.py +1 -1
- rem/models/entities/ontology_config.py +1 -1
- rem/models/entities/session.py +83 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/registry.py +10 -4
- rem/schemas/agents/core/agent-builder.yaml +134 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
- rem/schemas/agents/examples/contract-extractor.yaml +1 -1
- rem/schemas/agents/examples/cv-parser.yaml +1 -1
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/__init__.py +3 -1
- rem/services/content/service.py +92 -19
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +459 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/api.py +4 -4
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/client.py +154 -14
- rem/services/postgres/README.md +197 -15
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +547 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +470 -140
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +205 -4
- rem/services/postgres/service.py +6 -6
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +137 -51
- rem/services/session/reload.py +15 -8
- rem/settings.py +515 -27
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +387 -54
- rem/sql/migrations/002_install_models.sql +2304 -377
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/README.md +45 -0
- rem/utils/__init__.py +18 -0
- rem/utils/date_utils.py +2 -2
- rem/utils/files.py +157 -1
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +220 -22
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +1 -1
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/METADATA +340 -229
- {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/RECORD +109 -80
- {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1051
- rem/sql/migrations/003_seed_default_user.sql +0 -48
- {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session sharing endpoints.
|
|
3
|
+
|
|
4
|
+
Enables session sharing between users for collaborative access to conversation history.
|
|
5
|
+
|
|
6
|
+
Endpoints:
|
|
7
|
+
POST /api/v1/sessions/{session_id}/share - Share a session with another user
|
|
8
|
+
DELETE /api/v1/sessions/{session_id}/share/{user_id} - Revoke a share (soft delete)
|
|
9
|
+
GET /api/v1/sessions/shared-with-me - Get users sharing sessions with you
|
|
10
|
+
GET /api/v1/sessions/shared-with-me/{user_id}/messages - Get messages from a user's shared sessions
|
|
11
|
+
|
|
12
|
+
See src/rem/models/entities/shared_session.py for full documentation.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Literal
|
|
16
|
+
|
|
17
|
+
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
|
|
18
|
+
from loguru import logger
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from ..deps import get_current_user, require_auth
|
|
22
|
+
from ...models.entities import (
|
|
23
|
+
Message,
|
|
24
|
+
SharedSession,
|
|
25
|
+
SharedSessionCreate,
|
|
26
|
+
SharedWithMeResponse,
|
|
27
|
+
SharedWithMeSummary,
|
|
28
|
+
)
|
|
29
|
+
from ...services.postgres import get_postgres_service
|
|
30
|
+
from ...settings import settings
|
|
31
|
+
from ...utils.date_utils import utc_now
|
|
32
|
+
|
|
33
|
+
router = APIRouter(prefix="/api/v1")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def get_connected_postgres():
|
|
37
|
+
"""Get a connected PostgresService instance."""
|
|
38
|
+
pg = get_postgres_service()
|
|
39
|
+
if pg and not pg.pool:
|
|
40
|
+
await pg.connect()
|
|
41
|
+
return pg
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# Request/Response Models
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PaginationMetadata(BaseModel):
|
|
50
|
+
"""Pagination metadata for paginated responses."""
|
|
51
|
+
|
|
52
|
+
total: int = Field(description="Total number of records matching filters")
|
|
53
|
+
page: int = Field(description="Current page number (1-indexed)")
|
|
54
|
+
page_size: int = Field(description="Number of records per page")
|
|
55
|
+
total_pages: int = Field(description="Total number of pages")
|
|
56
|
+
has_next: bool = Field(description="Whether there are more pages after this one")
|
|
57
|
+
has_previous: bool = Field(description="Whether there are pages before this one")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SharedMessagesResponse(BaseModel):
|
|
61
|
+
"""Response for shared messages query."""
|
|
62
|
+
|
|
63
|
+
object: Literal["list"] = "list"
|
|
64
|
+
data: list[Message] = Field(description="List of messages from shared sessions")
|
|
65
|
+
metadata: PaginationMetadata = Field(description="Pagination metadata")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ShareSessionResponse(BaseModel):
|
|
69
|
+
"""Response after sharing a session."""
|
|
70
|
+
|
|
71
|
+
success: bool = True
|
|
72
|
+
message: str
|
|
73
|
+
share: SharedSession
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# =============================================================================
|
|
77
|
+
# Share Session Endpoints
|
|
78
|
+
# =============================================================================
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post(
|
|
82
|
+
"/sessions/{session_id}/share",
|
|
83
|
+
response_model=ShareSessionResponse,
|
|
84
|
+
status_code=201,
|
|
85
|
+
tags=["sessions"],
|
|
86
|
+
)
|
|
87
|
+
async def share_session(
|
|
88
|
+
request: Request,
|
|
89
|
+
session_id: str,
|
|
90
|
+
body: SharedSessionCreate,
|
|
91
|
+
user: dict = Depends(require_auth),
|
|
92
|
+
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
93
|
+
) -> ShareSessionResponse:
|
|
94
|
+
"""
|
|
95
|
+
Share a session with another user.
|
|
96
|
+
|
|
97
|
+
Creates a SharedSession record that grants the recipient access to view
|
|
98
|
+
messages in this session.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
session_id: The session to share
|
|
102
|
+
body: Contains shared_with_user_id - the recipient
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The created SharedSession record
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
400: Session already shared with this user
|
|
109
|
+
503: Database not enabled
|
|
110
|
+
"""
|
|
111
|
+
if not settings.postgres.enabled:
|
|
112
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
113
|
+
|
|
114
|
+
current_user_id = user.get("id", "default")
|
|
115
|
+
pg = await get_connected_postgres()
|
|
116
|
+
|
|
117
|
+
# Check if share already exists (active)
|
|
118
|
+
existing = await pg.fetchrow(
|
|
119
|
+
"""
|
|
120
|
+
SELECT id FROM shared_sessions
|
|
121
|
+
WHERE tenant_id = $1
|
|
122
|
+
AND session_id = $2
|
|
123
|
+
AND owner_user_id = $3
|
|
124
|
+
AND shared_with_user_id = $4
|
|
125
|
+
AND deleted_at IS NULL
|
|
126
|
+
""",
|
|
127
|
+
x_tenant_id,
|
|
128
|
+
session_id,
|
|
129
|
+
current_user_id,
|
|
130
|
+
body.shared_with_user_id,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if existing:
|
|
134
|
+
raise HTTPException(
|
|
135
|
+
status_code=400,
|
|
136
|
+
detail=f"Session '{session_id}' is already shared with user '{body.shared_with_user_id}'",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Create the share
|
|
140
|
+
result = await pg.fetchrow(
|
|
141
|
+
"""
|
|
142
|
+
INSERT INTO shared_sessions (session_id, owner_user_id, shared_with_user_id, tenant_id)
|
|
143
|
+
VALUES ($1, $2, $3, $4)
|
|
144
|
+
RETURNING id, session_id, owner_user_id, shared_with_user_id, tenant_id, created_at, updated_at, deleted_at
|
|
145
|
+
""",
|
|
146
|
+
session_id,
|
|
147
|
+
current_user_id,
|
|
148
|
+
body.shared_with_user_id,
|
|
149
|
+
x_tenant_id,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
share = SharedSession(
|
|
153
|
+
id=result["id"],
|
|
154
|
+
session_id=result["session_id"],
|
|
155
|
+
owner_user_id=result["owner_user_id"],
|
|
156
|
+
shared_with_user_id=result["shared_with_user_id"],
|
|
157
|
+
tenant_id=result["tenant_id"],
|
|
158
|
+
created_at=result["created_at"],
|
|
159
|
+
updated_at=result["updated_at"],
|
|
160
|
+
deleted_at=result["deleted_at"],
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
logger.debug(
|
|
164
|
+
f"User {current_user_id} shared session '{session_id}' with {body.shared_with_user_id}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return ShareSessionResponse(
|
|
168
|
+
success=True,
|
|
169
|
+
message=f"Session shared with {body.shared_with_user_id}",
|
|
170
|
+
share=share,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@router.delete(
|
|
175
|
+
"/sessions/{session_id}/share/{shared_with_user_id}",
|
|
176
|
+
status_code=200,
|
|
177
|
+
tags=["sessions"],
|
|
178
|
+
)
|
|
179
|
+
async def remove_session_share(
|
|
180
|
+
request: Request,
|
|
181
|
+
session_id: str,
|
|
182
|
+
shared_with_user_id: str,
|
|
183
|
+
user: dict = Depends(require_auth),
|
|
184
|
+
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
185
|
+
) -> dict:
|
|
186
|
+
"""
|
|
187
|
+
Remove a session share (soft delete).
|
|
188
|
+
|
|
189
|
+
Sets deleted_at on the SharedSession record. The share can be re-created
|
|
190
|
+
later if needed.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
session_id: The session to unshare
|
|
194
|
+
shared_with_user_id: The user to remove access from
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Success message
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
404: Share not found
|
|
201
|
+
503: Database not enabled
|
|
202
|
+
"""
|
|
203
|
+
if not settings.postgres.enabled:
|
|
204
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
205
|
+
|
|
206
|
+
current_user_id = user.get("id", "default")
|
|
207
|
+
pg = await get_connected_postgres()
|
|
208
|
+
|
|
209
|
+
# Soft delete the share
|
|
210
|
+
result = await pg.fetchrow(
|
|
211
|
+
"""
|
|
212
|
+
UPDATE shared_sessions
|
|
213
|
+
SET deleted_at = $1, updated_at = $1
|
|
214
|
+
WHERE tenant_id = $2
|
|
215
|
+
AND session_id = $3
|
|
216
|
+
AND owner_user_id = $4
|
|
217
|
+
AND shared_with_user_id = $5
|
|
218
|
+
AND deleted_at IS NULL
|
|
219
|
+
RETURNING id
|
|
220
|
+
""",
|
|
221
|
+
utc_now(),
|
|
222
|
+
x_tenant_id,
|
|
223
|
+
session_id,
|
|
224
|
+
current_user_id,
|
|
225
|
+
shared_with_user_id,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if not result:
|
|
229
|
+
raise HTTPException(
|
|
230
|
+
status_code=404,
|
|
231
|
+
detail=f"Share not found for session '{session_id}' with user '{shared_with_user_id}'",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
logger.debug(
|
|
235
|
+
f"User {current_user_id} removed share for session '{session_id}' with {shared_with_user_id}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"success": True,
|
|
240
|
+
"message": f"Share removed for user {shared_with_user_id}",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# =============================================================================
|
|
245
|
+
# Shared With Me Endpoints
|
|
246
|
+
# =============================================================================
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@router.get(
|
|
250
|
+
"/sessions/shared-with-me",
|
|
251
|
+
response_model=SharedWithMeResponse,
|
|
252
|
+
tags=["sessions"],
|
|
253
|
+
)
|
|
254
|
+
async def get_shared_with_me(
|
|
255
|
+
request: Request,
|
|
256
|
+
page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
|
|
257
|
+
page_size: int = Query(default=50, ge=1, le=100, description="Results per page"),
|
|
258
|
+
user: dict = Depends(require_auth),
|
|
259
|
+
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
260
|
+
) -> SharedWithMeResponse:
|
|
261
|
+
"""
|
|
262
|
+
Get aggregate summary of users sharing sessions with you.
|
|
263
|
+
|
|
264
|
+
Returns a paginated list of users who have shared sessions with the
|
|
265
|
+
current user, including message counts and date ranges.
|
|
266
|
+
|
|
267
|
+
Each entry shows:
|
|
268
|
+
- user_id, name, email of the person sharing
|
|
269
|
+
- message_count: total messages across all their shared sessions
|
|
270
|
+
- session_count: number of sessions they've shared
|
|
271
|
+
- first_message_at, last_message_at: date range
|
|
272
|
+
|
|
273
|
+
Results are ordered by most recent message first.
|
|
274
|
+
"""
|
|
275
|
+
if not settings.postgres.enabled:
|
|
276
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
277
|
+
|
|
278
|
+
current_user_id = user.get("id", "default")
|
|
279
|
+
pg = await get_connected_postgres()
|
|
280
|
+
offset = (page - 1) * page_size
|
|
281
|
+
|
|
282
|
+
# Get total count
|
|
283
|
+
count_result = await pg.fetchrow(
|
|
284
|
+
"SELECT fn_count_shared_with_me($1, $2) as total",
|
|
285
|
+
x_tenant_id,
|
|
286
|
+
current_user_id,
|
|
287
|
+
)
|
|
288
|
+
total = count_result["total"] if count_result else 0
|
|
289
|
+
|
|
290
|
+
# Get paginated results
|
|
291
|
+
rows = await pg.fetch(
|
|
292
|
+
"SELECT * FROM fn_get_shared_with_me($1, $2, $3, $4)",
|
|
293
|
+
x_tenant_id,
|
|
294
|
+
current_user_id,
|
|
295
|
+
page_size,
|
|
296
|
+
offset,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
data = [
|
|
300
|
+
SharedWithMeSummary(
|
|
301
|
+
user_id=row["user_id"],
|
|
302
|
+
name=row["name"],
|
|
303
|
+
email=row["email"],
|
|
304
|
+
message_count=row["message_count"],
|
|
305
|
+
session_count=row["session_count"],
|
|
306
|
+
first_message_at=row["first_message_at"],
|
|
307
|
+
last_message_at=row["last_message_at"],
|
|
308
|
+
)
|
|
309
|
+
for row in rows
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
313
|
+
|
|
314
|
+
return SharedWithMeResponse(
|
|
315
|
+
data=data,
|
|
316
|
+
metadata={
|
|
317
|
+
"total": total,
|
|
318
|
+
"page": page,
|
|
319
|
+
"page_size": page_size,
|
|
320
|
+
"total_pages": total_pages,
|
|
321
|
+
"has_next": page < total_pages,
|
|
322
|
+
"has_previous": page > 1,
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@router.get(
|
|
328
|
+
"/sessions/shared-with-me/{owner_user_id}/messages",
|
|
329
|
+
response_model=SharedMessagesResponse,
|
|
330
|
+
tags=["sessions"],
|
|
331
|
+
)
|
|
332
|
+
async def get_shared_messages(
|
|
333
|
+
request: Request,
|
|
334
|
+
owner_user_id: str,
|
|
335
|
+
page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
|
|
336
|
+
page_size: int = Query(default=50, ge=1, le=100, description="Results per page"),
|
|
337
|
+
user: dict = Depends(require_auth),
|
|
338
|
+
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
339
|
+
) -> SharedMessagesResponse:
|
|
340
|
+
"""
|
|
341
|
+
Get messages from sessions shared by a specific user.
|
|
342
|
+
|
|
343
|
+
Returns paginated messages from all sessions that owner_user_id has
|
|
344
|
+
shared with the current user. Messages are ordered by created_at DESC.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
owner_user_id: The user who shared the sessions
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Paginated list of Message objects
|
|
351
|
+
"""
|
|
352
|
+
if not settings.postgres.enabled:
|
|
353
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
354
|
+
|
|
355
|
+
current_user_id = user.get("id", "default")
|
|
356
|
+
pg = await get_connected_postgres()
|
|
357
|
+
offset = (page - 1) * page_size
|
|
358
|
+
|
|
359
|
+
# Get total count
|
|
360
|
+
count_result = await pg.fetchrow(
|
|
361
|
+
"SELECT fn_count_shared_messages($1, $2, $3) as total",
|
|
362
|
+
x_tenant_id,
|
|
363
|
+
current_user_id,
|
|
364
|
+
owner_user_id,
|
|
365
|
+
)
|
|
366
|
+
total = count_result["total"] if count_result else 0
|
|
367
|
+
|
|
368
|
+
# Get paginated messages
|
|
369
|
+
rows = await pg.fetch(
|
|
370
|
+
"SELECT * FROM fn_get_shared_messages($1, $2, $3, $4, $5)",
|
|
371
|
+
x_tenant_id,
|
|
372
|
+
current_user_id,
|
|
373
|
+
owner_user_id,
|
|
374
|
+
page_size,
|
|
375
|
+
offset,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Convert to Message objects
|
|
379
|
+
data = [
|
|
380
|
+
Message(
|
|
381
|
+
id=row["id"],
|
|
382
|
+
content=row["content"],
|
|
383
|
+
message_type=row["message_type"],
|
|
384
|
+
session_id=row["session_id"],
|
|
385
|
+
model=row["model"],
|
|
386
|
+
token_count=row["token_count"],
|
|
387
|
+
created_at=row["created_at"],
|
|
388
|
+
metadata=row["metadata"] or {},
|
|
389
|
+
tenant_id=x_tenant_id,
|
|
390
|
+
)
|
|
391
|
+
for row in rows
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
395
|
+
|
|
396
|
+
return SharedMessagesResponse(
|
|
397
|
+
data=data,
|
|
398
|
+
metadata=PaginationMetadata(
|
|
399
|
+
total=total,
|
|
400
|
+
page=page,
|
|
401
|
+
page_size=page_size,
|
|
402
|
+
total_pages=total_pages,
|
|
403
|
+
has_next=page < total_pages,
|
|
404
|
+
has_previous=page > 1,
|
|
405
|
+
),
|
|
406
|
+
)
|
rem/auth/__init__.py
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
"""
|
|
2
2
|
REM Authentication Module.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
Authentication with support for:
|
|
5
|
+
- Email passwordless login (verification codes)
|
|
5
6
|
- Google OAuth
|
|
6
7
|
- Microsoft Entra ID (Azure AD) OIDC
|
|
7
8
|
- Custom OIDC providers
|
|
8
9
|
|
|
9
10
|
Design Pattern:
|
|
10
11
|
- Provider-agnostic base classes
|
|
11
|
-
- PKCE (Proof Key for Code Exchange) for
|
|
12
|
+
- PKCE (Proof Key for Code Exchange) for OAuth flows
|
|
12
13
|
- State parameter for CSRF protection
|
|
13
14
|
- Nonce for ID token replay protection
|
|
14
15
|
- Token validation with JWKS
|
|
15
|
-
- Clean separation: providers/ for
|
|
16
|
+
- Clean separation: providers/ for auth logic, middleware.py for FastAPI integration
|
|
17
|
+
|
|
18
|
+
Email Auth Flow:
|
|
19
|
+
1. POST /api/auth/email/send-code with {email}
|
|
20
|
+
2. User receives code via email
|
|
21
|
+
3. POST /api/auth/email/verify with {email, code}
|
|
22
|
+
4. Session created, user authenticated
|
|
16
23
|
"""
|
|
17
24
|
|
|
18
25
|
from .providers.base import OAuthProvider
|
|
26
|
+
from .providers.email import EmailAuthProvider, EmailAuthResult
|
|
19
27
|
from .providers.google import GoogleOAuthProvider
|
|
20
28
|
from .providers.microsoft import MicrosoftOAuthProvider
|
|
21
29
|
|
|
22
30
|
__all__ = [
|
|
23
31
|
"OAuthProvider",
|
|
32
|
+
"EmailAuthProvider",
|
|
33
|
+
"EmailAuthResult",
|
|
24
34
|
"GoogleOAuthProvider",
|
|
25
35
|
"MicrosoftOAuthProvider",
|
|
26
36
|
]
|