dominus-sdk-python 2.0.0__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,437 @@
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: str
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: Tenant UUID to login to
57
+
58
+ Returns:
59
+ Dict with user info, tenant info, and session_id
60
+ """
61
+ return await self._client._request(
62
+ endpoint="/api/portal/auth/login",
63
+ body={
64
+ "username": username,
65
+ "password": password,
66
+ "tenant_id": tenant_id
67
+ }
68
+ )
69
+
70
+ async def login_client(
71
+ self,
72
+ client_id: str,
73
+ psk: str,
74
+ tenant_id: str
75
+ ) -> Dict[str, Any]:
76
+ """
77
+ Login service client with PSK.
78
+
79
+ Args:
80
+ client_id: Client UUID
81
+ psk: Pre-shared key
82
+ tenant_id: Tenant UUID
83
+
84
+ Returns:
85
+ Dict with access_token, token_type, expires_in, session_id
86
+ """
87
+ return await self._client._request(
88
+ endpoint="/api/portal/auth/login-client",
89
+ body={
90
+ "client_id": client_id,
91
+ "psk": psk,
92
+ "tenant_id": tenant_id
93
+ }
94
+ )
95
+
96
+ async def logout(self) -> Dict[str, Any]:
97
+ """End session and clear cookie."""
98
+ return await self._client._request(
99
+ endpoint="/api/portal/auth/logout",
100
+ body={}
101
+ )
102
+
103
+ async def refresh(self) -> Dict[str, Any]:
104
+ """Refresh JWT token using existing session."""
105
+ return await self._client._request(
106
+ endpoint="/api/portal/auth/refresh",
107
+ body={}
108
+ )
109
+
110
+ async def me(self) -> Dict[str, Any]:
111
+ """
112
+ Get current user/client info.
113
+
114
+ Returns:
115
+ Dict with subject_type, user/client info, tenants, scopes, roles
116
+ """
117
+ return await self._client._request(
118
+ endpoint="/api/portal/auth/me",
119
+ method="GET"
120
+ )
121
+
122
+ async def switch_tenant(self, tenant_id: str) -> Dict[str, Any]:
123
+ """
124
+ Switch active tenant context.
125
+
126
+ Args:
127
+ tenant_id: Tenant UUID to switch to
128
+
129
+ Returns:
130
+ Dict with success status and new tenant info
131
+ """
132
+ return await self._client._request(
133
+ endpoint="/api/portal/auth/switch-tenant",
134
+ body={"tenant_id": tenant_id}
135
+ )
136
+
137
+ # ========================================
138
+ # SECURITY
139
+ # ========================================
140
+
141
+ async def change_password(
142
+ self,
143
+ current_password: str,
144
+ new_password: str
145
+ ) -> Dict[str, Any]:
146
+ """
147
+ Change current user's password.
148
+
149
+ Args:
150
+ current_password: Current password for verification
151
+ new_password: New password to set
152
+
153
+ Returns:
154
+ Dict with success status
155
+ """
156
+ return await self._client._request(
157
+ endpoint="/api/portal/security/change-password",
158
+ body={
159
+ "current_password": current_password,
160
+ "new_password": new_password
161
+ }
162
+ )
163
+
164
+ async def request_password_reset(self, email: str) -> Dict[str, Any]:
165
+ """
166
+ Request password reset email.
167
+
168
+ Args:
169
+ email: User's email address
170
+
171
+ Returns:
172
+ Dict with success status (always true for security)
173
+ """
174
+ return await self._client._request(
175
+ endpoint="/api/portal/security/request-reset",
176
+ body={"email": email}
177
+ )
178
+
179
+ async def confirm_password_reset(
180
+ self,
181
+ token: str,
182
+ new_password: str
183
+ ) -> Dict[str, Any]:
184
+ """
185
+ Confirm password reset with token.
186
+
187
+ Args:
188
+ token: Reset token from email
189
+ new_password: New password to set
190
+
191
+ Returns:
192
+ Dict with success status
193
+ """
194
+ return await self._client._request(
195
+ endpoint="/api/portal/security/confirm-reset",
196
+ body={"token": token, "new_password": new_password}
197
+ )
198
+
199
+ async def list_sessions(self) -> List[Dict[str, Any]]:
200
+ """List all active sessions for current user."""
201
+ return await self._client._request(
202
+ endpoint="/api/portal/security/sessions",
203
+ method="GET"
204
+ )
205
+
206
+ async def revoke_session(self, session_id: str) -> Dict[str, Any]:
207
+ """Revoke a specific session."""
208
+ return await self._client._request(
209
+ endpoint=f"/api/portal/security/sessions/{session_id}",
210
+ method="DELETE"
211
+ )
212
+
213
+ async def revoke_all_sessions(self) -> Dict[str, Any]:
214
+ """Revoke all sessions except current."""
215
+ return await self._client._request(
216
+ endpoint="/api/portal/security/sessions/revoke-all",
217
+ body={}
218
+ )
219
+
220
+ # ========================================
221
+ # PROFILE
222
+ # ========================================
223
+
224
+ async def get_profile(self) -> Dict[str, Any]:
225
+ """Get current user's profile."""
226
+ return await self._client._request(
227
+ endpoint="/api/portal/profile",
228
+ method="GET"
229
+ )
230
+
231
+ async def update_profile(
232
+ self,
233
+ display_name: Optional[str] = None,
234
+ avatar_url: Optional[str] = None,
235
+ bio: Optional[str] = None,
236
+ phone: Optional[str] = None,
237
+ extra: Optional[Dict[str, Any]] = None
238
+ ) -> Dict[str, Any]:
239
+ """
240
+ Update user profile.
241
+
242
+ Args:
243
+ display_name: Display name
244
+ avatar_url: Avatar image URL
245
+ bio: User biography
246
+ phone: Phone number
247
+ extra: Additional metadata (JSONB)
248
+
249
+ Returns:
250
+ Updated profile
251
+ """
252
+ body = {}
253
+ if display_name is not None:
254
+ body["display_name"] = display_name
255
+ if avatar_url is not None:
256
+ body["avatar_url"] = avatar_url
257
+ if bio is not None:
258
+ body["bio"] = bio
259
+ if phone is not None:
260
+ body["phone"] = phone
261
+ if extra is not None:
262
+ body["extra"] = extra
263
+
264
+ return await self._client._request(
265
+ endpoint="/api/portal/profile",
266
+ method="PUT",
267
+ body=body
268
+ )
269
+
270
+ async def get_preferences(self) -> Dict[str, Any]:
271
+ """Get current user's preferences."""
272
+ return await self._client._request(
273
+ endpoint="/api/portal/profile/preferences",
274
+ method="GET"
275
+ )
276
+
277
+ async def update_preferences(
278
+ self,
279
+ theme: Optional[str] = None,
280
+ language: Optional[str] = None,
281
+ timezone: Optional[str] = None,
282
+ sidebar_collapsed: Optional[bool] = None,
283
+ notifications_enabled: Optional[bool] = None,
284
+ email_notifications: Optional[bool] = None,
285
+ extra: Optional[Dict[str, Any]] = None
286
+ ) -> Dict[str, Any]:
287
+ """
288
+ Update user preferences.
289
+
290
+ Args:
291
+ theme: UI theme (light/dark/system)
292
+ language: Preferred language code
293
+ timezone: Timezone (e.g., "America/New_York")
294
+ sidebar_collapsed: Sidebar state
295
+ notifications_enabled: Push notifications
296
+ email_notifications: Email notifications
297
+ extra: Additional preferences (JSONB)
298
+
299
+ Returns:
300
+ Updated preferences
301
+ """
302
+ body = {}
303
+ if theme is not None:
304
+ body["theme"] = theme
305
+ if language is not None:
306
+ body["language"] = language
307
+ if timezone is not None:
308
+ body["timezone"] = timezone
309
+ if sidebar_collapsed is not None:
310
+ body["sidebar_collapsed"] = sidebar_collapsed
311
+ if notifications_enabled is not None:
312
+ body["notifications_enabled"] = notifications_enabled
313
+ if email_notifications is not None:
314
+ body["email_notifications"] = email_notifications
315
+ if extra is not None:
316
+ body["extra"] = extra
317
+
318
+ return await self._client._request(
319
+ endpoint="/api/portal/profile/preferences",
320
+ method="PUT",
321
+ body=body
322
+ )
323
+
324
+ # ========================================
325
+ # NAVIGATION
326
+ # ========================================
327
+
328
+ async def get_navigation(self) -> Dict[str, Any]:
329
+ """
330
+ Get navigation tree for current user's tenant.
331
+
332
+ Returns hierarchical nav structure with access-filtered items.
333
+ """
334
+ return await self._client._request(
335
+ endpoint="/api/portal/nav/tree",
336
+ method="GET"
337
+ )
338
+
339
+ async def check_page_access(self, path: str) -> Dict[str, Any]:
340
+ """
341
+ Check if current user can access a page.
342
+
343
+ Args:
344
+ path: Page path to check
345
+
346
+ Returns:
347
+ Dict with allowed (bool) and reason
348
+ """
349
+ return await self._client._request(
350
+ endpoint="/api/portal/nav/check-access",
351
+ body={"path": path}
352
+ )
353
+
354
+ # ========================================
355
+ # REGISTRATION
356
+ # ========================================
357
+
358
+ async def register(
359
+ self,
360
+ username: str,
361
+ email: str,
362
+ password: str,
363
+ tenant_id: str
364
+ ) -> Dict[str, Any]:
365
+ """
366
+ Self-register new user.
367
+
368
+ Requires tenant to allow public registration.
369
+
370
+ Args:
371
+ username: Desired username
372
+ email: Email address
373
+ password: Password
374
+ tenant_id: Tenant to register with
375
+
376
+ Returns:
377
+ Dict with user info and verification status
378
+ """
379
+ return await self._client._request(
380
+ endpoint="/api/portal/register",
381
+ body={
382
+ "username": username,
383
+ "email": email,
384
+ "password": password,
385
+ "tenant_id": tenant_id
386
+ }
387
+ )
388
+
389
+ async def verify_email(self, token: str) -> Dict[str, Any]:
390
+ """
391
+ Verify email with token.
392
+
393
+ Args:
394
+ token: Verification token from email
395
+
396
+ Returns:
397
+ Dict with success status
398
+ """
399
+ return await self._client._request(
400
+ endpoint="/api/portal/register/verify",
401
+ body={"token": token}
402
+ )
403
+
404
+ async def resend_verification(self, email: str) -> Dict[str, Any]:
405
+ """
406
+ Resend verification email.
407
+
408
+ Args:
409
+ email: User's email address
410
+
411
+ Returns:
412
+ Dict with success status
413
+ """
414
+ return await self._client._request(
415
+ endpoint="/api/portal/register/resend-verification",
416
+ body={"email": email}
417
+ )
418
+
419
+ async def accept_invitation(
420
+ self,
421
+ token: str,
422
+ password: str
423
+ ) -> Dict[str, Any]:
424
+ """
425
+ Accept admin invitation and set password.
426
+
427
+ Args:
428
+ token: Invitation token from email
429
+ password: Password to set
430
+
431
+ Returns:
432
+ Dict with user info and login token
433
+ """
434
+ return await self._client._request(
435
+ endpoint="/api/portal/register/accept-invitation",
436
+ body={"token": token, "password": password}
437
+ )