dominus-sdk-python-dev 2.1.6__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.
@@ -0,0 +1,504 @@
1
+ """
2
+ Portal Namespace - User authentication and session orchestration.
3
+
4
+ Provides login, logout, session management, profile, and navigation access.
5
+ """
6
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..start import Dominus
10
+
11
+
12
+ class PortalNamespace:
13
+ """
14
+ User authentication and session namespace.
15
+
16
+ All portal operations go through /api/portal/* endpoints.
17
+ Orchestrates Guardian (user data) + Warden (JWT minting).
18
+
19
+ Usage:
20
+ # User login
21
+ session = await dominus.portal.login(
22
+ username="john@example.com",
23
+ password="secret123",
24
+ tenant_id="tenant-uuid"
25
+ )
26
+
27
+ # Get current user
28
+ me = await dominus.portal.me()
29
+
30
+ # Switch tenant
31
+ await dominus.portal.switch_tenant("other-tenant-uuid")
32
+
33
+ # Get navigation
34
+ nav = await dominus.portal.get_navigation()
35
+ """
36
+
37
+ def __init__(self, client: "Dominus"):
38
+ self._client = client
39
+
40
+ # ========================================
41
+ # AUTHENTICATION
42
+ # ========================================
43
+
44
+ async def login(
45
+ self,
46
+ username: str,
47
+ password: str,
48
+ tenant_id: Optional[str] = None
49
+ ) -> Dict[str, Any]:
50
+ """
51
+ Login user with password.
52
+
53
+ Args:
54
+ username: Username or email
55
+ password: User password
56
+ tenant_id: Optional tenant UUID/slug. If not provided, uses user's first available tenant.
57
+
58
+ Returns:
59
+ Dict with user info, active tenant, available tenants list, and session_id
60
+ """
61
+ body: Dict[str, str] = {
62
+ "username": username,
63
+ "password": password
64
+ }
65
+ if tenant_id:
66
+ body["tenant_id"] = tenant_id
67
+
68
+ return await self._client._request(
69
+ endpoint="/api/portal/auth/login",
70
+ body=body
71
+ )
72
+
73
+ async def login_client(
74
+ self,
75
+ psk: str,
76
+ tenant_id: Optional[str] = None
77
+ ) -> Dict[str, Any]:
78
+ """
79
+ Login service client with PSK.
80
+
81
+ Client is looked up by PSK directly - no client_id needed.
82
+
83
+ Args:
84
+ psk: Pre-shared key
85
+ tenant_id: Optional tenant UUID. If not provided, uses client's first assigned tenant.
86
+
87
+ Returns:
88
+ Dict with access_token, token_type, expires_in, session_id
89
+ """
90
+ body: Dict[str, str] = {"psk": psk}
91
+ if tenant_id:
92
+ body["tenant_id"] = tenant_id
93
+ return await self._client._request(
94
+ endpoint="/api/portal/auth/login-client",
95
+ body=body
96
+ )
97
+
98
+ async def logout(self, user_token: Optional[str] = None) -> Dict[str, Any]:
99
+ """
100
+ End session and clear cookie.
101
+
102
+ Args:
103
+ user_token: Optional user JWT for user-authenticated requests
104
+ """
105
+ return await self._client._request(
106
+ endpoint="/api/portal/auth/logout",
107
+ body={},
108
+ user_token=user_token
109
+ )
110
+
111
+ async def refresh(self, user_token: Optional[str] = None) -> Dict[str, Any]:
112
+ """
113
+ Refresh JWT token using existing session.
114
+
115
+ Args:
116
+ user_token: Optional user JWT for user-authenticated requests
117
+ """
118
+ return await self._client._request(
119
+ endpoint="/api/portal/auth/refresh",
120
+ body={},
121
+ user_token=user_token
122
+ )
123
+
124
+ async def me(self, user_token: Optional[str] = None) -> Dict[str, Any]:
125
+ """
126
+ Get current user/client info.
127
+
128
+ Args:
129
+ user_token: Optional user JWT for user-authenticated requests
130
+
131
+ Returns:
132
+ Dict with subject_type, user/client info, tenants, scopes, roles
133
+ """
134
+ return await self._client._request(
135
+ endpoint="/api/portal/auth/me",
136
+ method="GET",
137
+ user_token=user_token
138
+ )
139
+
140
+ async def switch_tenant(self, tenant_id: str, user_token: Optional[str] = None) -> Dict[str, Any]:
141
+ """
142
+ Switch active tenant context.
143
+
144
+ Args:
145
+ tenant_id: Tenant UUID to switch to
146
+ user_token: Optional user JWT for user-authenticated requests
147
+
148
+ Returns:
149
+ Dict with success status and new tenant info
150
+ """
151
+ return await self._client._request(
152
+ endpoint="/api/portal/auth/switch-tenant",
153
+ body={"tenant_id": tenant_id},
154
+ user_token=user_token
155
+ )
156
+
157
+ # ========================================
158
+ # SECURITY
159
+ # ========================================
160
+
161
+ async def change_password(
162
+ self,
163
+ current_password: str,
164
+ new_password: str,
165
+ user_token: Optional[str] = None
166
+ ) -> Dict[str, Any]:
167
+ """
168
+ Change current user's password.
169
+
170
+ Args:
171
+ current_password: Current password for verification
172
+ new_password: New password to set
173
+ user_token: Optional user JWT for user-authenticated requests
174
+
175
+ Returns:
176
+ Dict with success status
177
+ """
178
+ return await self._client._request(
179
+ endpoint="/api/portal/security/change-password",
180
+ body={
181
+ "current_password": current_password,
182
+ "new_password": new_password
183
+ },
184
+ user_token=user_token
185
+ )
186
+
187
+ async def request_password_reset(self, email: str) -> Dict[str, Any]:
188
+ """
189
+ Request password reset email.
190
+
191
+ Args:
192
+ email: User's email address
193
+
194
+ Returns:
195
+ Dict with success status (always true for security)
196
+ """
197
+ return await self._client._request(
198
+ endpoint="/api/portal/security/request-reset",
199
+ body={"email": email}
200
+ )
201
+
202
+ async def confirm_password_reset(
203
+ self,
204
+ token: str,
205
+ new_password: str
206
+ ) -> Dict[str, Any]:
207
+ """
208
+ Confirm password reset with token.
209
+
210
+ Args:
211
+ token: Reset token from email
212
+ new_password: New password to set
213
+
214
+ Returns:
215
+ Dict with success status
216
+ """
217
+ return await self._client._request(
218
+ endpoint="/api/portal/security/confirm-reset",
219
+ body={"token": token, "new_password": new_password}
220
+ )
221
+
222
+ async def list_sessions(self, user_token: Optional[str] = None) -> List[Dict[str, Any]]:
223
+ """
224
+ List all active sessions for current user.
225
+
226
+ Args:
227
+ user_token: Optional user JWT for user-authenticated requests
228
+ """
229
+ return await self._client._request(
230
+ endpoint="/api/portal/security/sessions",
231
+ method="GET",
232
+ user_token=user_token
233
+ )
234
+
235
+ async def revoke_session(self, session_id: str, user_token: Optional[str] = None) -> Dict[str, Any]:
236
+ """
237
+ Revoke a specific session.
238
+
239
+ Args:
240
+ session_id: Session ID to revoke
241
+ user_token: Optional user JWT for user-authenticated requests
242
+ """
243
+ return await self._client._request(
244
+ endpoint=f"/api/portal/security/sessions/{session_id}",
245
+ method="DELETE",
246
+ user_token=user_token
247
+ )
248
+
249
+ async def revoke_all_sessions(self, user_token: Optional[str] = None) -> Dict[str, Any]:
250
+ """
251
+ Revoke all sessions except current.
252
+
253
+ Args:
254
+ user_token: Optional user JWT for user-authenticated requests
255
+ """
256
+ return await self._client._request(
257
+ endpoint="/api/portal/security/sessions/revoke-all",
258
+ body={},
259
+ user_token=user_token
260
+ )
261
+
262
+ # ========================================
263
+ # PROFILE
264
+ # ========================================
265
+
266
+ async def get_profile(self, user_token: Optional[str] = None) -> Dict[str, Any]:
267
+ """
268
+ Get current user's profile.
269
+
270
+ Args:
271
+ user_token: Optional user JWT for user-authenticated requests
272
+ """
273
+ return await self._client._request(
274
+ endpoint="/api/portal/profile",
275
+ method="GET",
276
+ user_token=user_token
277
+ )
278
+
279
+ async def update_profile(
280
+ self,
281
+ display_name: Optional[str] = None,
282
+ avatar_url: Optional[str] = None,
283
+ bio: Optional[str] = None,
284
+ phone: Optional[str] = None,
285
+ extra: Optional[Dict[str, Any]] = None,
286
+ user_token: Optional[str] = None
287
+ ) -> Dict[str, Any]:
288
+ """
289
+ Update user profile.
290
+
291
+ Args:
292
+ display_name: Display name
293
+ avatar_url: Avatar image URL
294
+ bio: User biography
295
+ phone: Phone number
296
+ extra: Additional metadata (JSONB)
297
+ user_token: Optional user JWT for user-authenticated requests
298
+
299
+ Returns:
300
+ Updated profile
301
+ """
302
+ body = {}
303
+ if display_name is not None:
304
+ body["display_name"] = display_name
305
+ if avatar_url is not None:
306
+ body["avatar_url"] = avatar_url
307
+ if bio is not None:
308
+ body["bio"] = bio
309
+ if phone is not None:
310
+ body["phone"] = phone
311
+ if extra is not None:
312
+ body["extra"] = extra
313
+
314
+ return await self._client._request(
315
+ endpoint="/api/portal/profile",
316
+ method="PUT",
317
+ body=body,
318
+ user_token=user_token
319
+ )
320
+
321
+ async def get_preferences(self, user_token: Optional[str] = None) -> Dict[str, Any]:
322
+ """
323
+ Get current user's preferences.
324
+
325
+ Args:
326
+ user_token: Optional user JWT for user-authenticated requests
327
+ """
328
+ return await self._client._request(
329
+ endpoint="/api/portal/profile/preferences",
330
+ method="GET",
331
+ user_token=user_token
332
+ )
333
+
334
+ async def update_preferences(
335
+ self,
336
+ theme: Optional[str] = None,
337
+ language: Optional[str] = None,
338
+ timezone: Optional[str] = None,
339
+ sidebar_collapsed: Optional[bool] = None,
340
+ notifications_enabled: Optional[bool] = None,
341
+ email_notifications: Optional[bool] = None,
342
+ extra: Optional[Dict[str, Any]] = None,
343
+ user_token: Optional[str] = None
344
+ ) -> Dict[str, Any]:
345
+ """
346
+ Update user preferences.
347
+
348
+ Args:
349
+ theme: UI theme (light/dark/system)
350
+ language: Preferred language code
351
+ timezone: Timezone (e.g., "America/New_York")
352
+ sidebar_collapsed: Sidebar state
353
+ notifications_enabled: Push notifications
354
+ email_notifications: Email notifications
355
+ extra: Additional preferences (JSONB)
356
+ user_token: Optional user JWT for user-authenticated requests
357
+
358
+ Returns:
359
+ Updated preferences
360
+ """
361
+ body = {}
362
+ if theme is not None:
363
+ body["theme"] = theme
364
+ if language is not None:
365
+ body["language"] = language
366
+ if timezone is not None:
367
+ body["timezone"] = timezone
368
+ if sidebar_collapsed is not None:
369
+ body["sidebar_collapsed"] = sidebar_collapsed
370
+ if notifications_enabled is not None:
371
+ body["notifications_enabled"] = notifications_enabled
372
+ if email_notifications is not None:
373
+ body["email_notifications"] = email_notifications
374
+ if extra is not None:
375
+ body["extra"] = extra
376
+
377
+ return await self._client._request(
378
+ endpoint="/api/portal/profile/preferences",
379
+ method="PUT",
380
+ body=body,
381
+ user_token=user_token
382
+ )
383
+
384
+ # ========================================
385
+ # NAVIGATION
386
+ # ========================================
387
+
388
+ async def get_navigation(self, user_token: Optional[str] = None) -> Dict[str, Any]:
389
+ """
390
+ Get navigation tree for current user's tenant.
391
+
392
+ Args:
393
+ user_token: Optional user JWT for user-authenticated requests
394
+
395
+ Returns:
396
+ Hierarchical nav structure with access-filtered items.
397
+ """
398
+ return await self._client._request(
399
+ endpoint="/api/portal/nav/tree",
400
+ method="GET",
401
+ user_token=user_token
402
+ )
403
+
404
+ async def check_page_access(self, path: str, user_token: Optional[str] = None) -> Dict[str, Any]:
405
+ """
406
+ Check if current user can access a page.
407
+
408
+ Args:
409
+ path: Page path to check
410
+ user_token: Optional user JWT for user-authenticated requests
411
+
412
+ Returns:
413
+ Dict with allowed (bool) and reason
414
+ """
415
+ return await self._client._request(
416
+ endpoint="/api/portal/nav/check-access",
417
+ body={"path": path},
418
+ user_token=user_token
419
+ )
420
+
421
+ # ========================================
422
+ # REGISTRATION
423
+ # ========================================
424
+
425
+ async def register(
426
+ self,
427
+ username: str,
428
+ email: str,
429
+ password: str,
430
+ tenant_id: str
431
+ ) -> Dict[str, Any]:
432
+ """
433
+ Self-register new user.
434
+
435
+ Requires tenant to allow public registration.
436
+
437
+ Args:
438
+ username: Desired username
439
+ email: Email address
440
+ password: Password
441
+ tenant_id: Tenant to register with
442
+
443
+ Returns:
444
+ Dict with user info and verification status
445
+ """
446
+ return await self._client._request(
447
+ endpoint="/api/portal/register",
448
+ body={
449
+ "username": username,
450
+ "email": email,
451
+ "password": password,
452
+ "tenant_id": tenant_id
453
+ }
454
+ )
455
+
456
+ async def verify_email(self, token: str) -> Dict[str, Any]:
457
+ """
458
+ Verify email with token.
459
+
460
+ Args:
461
+ token: Verification token from email
462
+
463
+ Returns:
464
+ Dict with success status
465
+ """
466
+ return await self._client._request(
467
+ endpoint="/api/portal/register/verify",
468
+ body={"token": token}
469
+ )
470
+
471
+ async def resend_verification(self, email: str) -> Dict[str, Any]:
472
+ """
473
+ Resend verification email.
474
+
475
+ Args:
476
+ email: User's email address
477
+
478
+ Returns:
479
+ Dict with success status
480
+ """
481
+ return await self._client._request(
482
+ endpoint="/api/portal/register/resend-verification",
483
+ body={"email": email}
484
+ )
485
+
486
+ async def accept_invitation(
487
+ self,
488
+ token: str,
489
+ password: str
490
+ ) -> Dict[str, Any]:
491
+ """
492
+ Accept admin invitation and set password.
493
+
494
+ Args:
495
+ token: Invitation token from email
496
+ password: Password to set
497
+
498
+ Returns:
499
+ Dict with user info and login token
500
+ """
501
+ return await self._client._request(
502
+ endpoint="/api/portal/register/accept-invitation",
503
+ body={"token": token, "password": password}
504
+ )