dominusnode 1.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.
dominusnode/auth.py ADDED
@@ -0,0 +1,271 @@
1
+ """Auth resource -- registration, login, MFA, token refresh, and password management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .http_client import AsyncHttpClient, SyncHttpClient
8
+ from .token_manager import AsyncTokenManager, TokenManager
9
+ from .types import LoginResult, MfaSetup, MfaStatus, User
10
+
11
+
12
+ def _parse_login_result(data: dict) -> LoginResult:
13
+ """Parse a login/register response into a LoginResult."""
14
+ if data.get("mfaRequired"):
15
+ return LoginResult(
16
+ mfa_required=True,
17
+ mfa_challenge_token=data.get("mfaChallengeToken"),
18
+ )
19
+
20
+ user_data = data.get("user")
21
+ user = None
22
+ if user_data:
23
+ user = User(
24
+ id=user_data["id"],
25
+ email=user_data["email"],
26
+ created_at=str(user_data.get("created_at", "")),
27
+ is_admin=user_data.get("is_admin", False),
28
+ )
29
+
30
+ return LoginResult(
31
+ user=user,
32
+ token=data.get("token"),
33
+ refresh_token=data.get("refreshToken"),
34
+ )
35
+
36
+
37
+ # ──────────────────────────────────────────────────────────────────────
38
+ # Sync
39
+ # ──────────────────────────────────────────────────────────────────────
40
+
41
+
42
+ class AuthResource:
43
+ """Synchronous auth operations."""
44
+
45
+ def __init__(self, http: SyncHttpClient, tokens: TokenManager) -> None:
46
+ self._http = http
47
+ self._tokens = tokens
48
+
49
+ def register(self, email: str, password: str) -> LoginResult:
50
+ """Create a new account and return tokens."""
51
+ data = self._http.post("/api/auth/register", json={"email": email, "password": password})
52
+ result = _parse_login_result(data)
53
+ if result.token and result.refresh_token:
54
+ self._tokens.set_tokens(result.token, result.refresh_token)
55
+ return result
56
+
57
+ def login(self, email: str, password: str) -> LoginResult:
58
+ """Authenticate with email/password. May require MFA."""
59
+ data = self._http.post("/api/auth/login", json={"email": email, "password": password})
60
+ result = _parse_login_result(data)
61
+ if result.token and result.refresh_token:
62
+ self._tokens.set_tokens(result.token, result.refresh_token)
63
+ return result
64
+
65
+ def verify_mfa(
66
+ self,
67
+ code: str,
68
+ *,
69
+ mfa_challenge_token: Optional[str] = None,
70
+ is_backup_code: bool = False,
71
+ ) -> LoginResult:
72
+ """Complete MFA challenge with TOTP code or backup code."""
73
+ payload: dict = {"code": code, "isBackupCode": is_backup_code}
74
+ if mfa_challenge_token:
75
+ payload["mfaChallengeToken"] = mfa_challenge_token
76
+ data = self._http.post("/api/auth/mfa/verify", json=payload)
77
+ result = _parse_login_result(data)
78
+ if result.token and result.refresh_token:
79
+ self._tokens.set_tokens(result.token, result.refresh_token)
80
+ return result
81
+
82
+ def refresh(self, refresh_token: Optional[str] = None) -> tuple[str, str]:
83
+ """Exchange refresh token for new token pair.
84
+
85
+ Returns (access_token, refresh_token) tuple.
86
+ """
87
+ rt = refresh_token or self._tokens.refresh_token
88
+ if not rt:
89
+ raise ValueError("No refresh token available")
90
+ data = self._http.post("/api/auth/refresh", json={"refreshToken": rt})
91
+ access = data["token"]
92
+ new_refresh = data["refreshToken"]
93
+ self._tokens.set_tokens(access, new_refresh)
94
+ return (access, new_refresh)
95
+
96
+ def logout(self) -> None:
97
+ """Revoke all refresh tokens for the current user."""
98
+ self._http.post("/api/auth/logout")
99
+ self._tokens.clear()
100
+
101
+ def me(self) -> User:
102
+ """Get current authenticated user."""
103
+ data = self._http.get("/api/auth/me")
104
+ u = data["user"]
105
+ return User(
106
+ id=u["id"],
107
+ email=u["email"],
108
+ created_at=str(u.get("created_at", "")),
109
+ is_admin=u.get("is_admin", False),
110
+ )
111
+
112
+ def change_password(self, current_password: str, new_password: str) -> None:
113
+ """Change the current user's password."""
114
+ self._http.post(
115
+ "/api/auth/change-password",
116
+ json={"currentPassword": current_password, "newPassword": new_password},
117
+ )
118
+
119
+ def verify_key(self, api_key: str) -> LoginResult:
120
+ """Verify an API key and get JWT tokens."""
121
+ data = self._http.post("/api/auth/verify-key", json={"apiKey": api_key})
122
+ result = LoginResult(
123
+ token=data.get("token"),
124
+ refresh_token=data.get("refreshToken"),
125
+ user=User(
126
+ id=data.get("userId", ""),
127
+ email=data.get("email", ""),
128
+ created_at="",
129
+ ) if data.get("userId") else None,
130
+ )
131
+ if result.token and result.refresh_token:
132
+ self._tokens.set_tokens(result.token, result.refresh_token)
133
+ return result
134
+
135
+ def mfa_setup(self) -> MfaSetup:
136
+ """Generate TOTP secret and backup codes for MFA setup."""
137
+ data = self._http.post("/api/auth/mfa/setup")
138
+ return MfaSetup(
139
+ secret=data["secret"],
140
+ otpauth_uri=data["otpauthUri"],
141
+ backup_codes=data.get("backupCodes", []),
142
+ )
143
+
144
+ def mfa_enable(self, code: str) -> bool:
145
+ """Verify initial TOTP code to activate MFA."""
146
+ data = self._http.post("/api/auth/mfa/enable", json={"code": code})
147
+ return data.get("enabled", False)
148
+
149
+ def mfa_disable(self, password: str, code: str) -> bool:
150
+ """Disable MFA (requires password + TOTP/backup code)."""
151
+ data = self._http.post("/api/auth/mfa/disable", json={"password": password, "code": code})
152
+ return not data.get("enabled", True)
153
+
154
+ def mfa_status(self) -> MfaStatus:
155
+ """Check current MFA status."""
156
+ data = self._http.get("/api/auth/mfa/status")
157
+ return MfaStatus(
158
+ enabled=data["enabled"],
159
+ backup_codes_remaining=data.get("backupCodesRemaining", 0),
160
+ )
161
+
162
+
163
+ # ──────────────────────────────────────────────────────────────────────
164
+ # Async
165
+ # ──────────────────────────────────────────────────────────────────────
166
+
167
+
168
+ class AsyncAuthResource:
169
+ """Asynchronous auth operations."""
170
+
171
+ def __init__(self, http: AsyncHttpClient, tokens: AsyncTokenManager) -> None:
172
+ self._http = http
173
+ self._tokens = tokens
174
+
175
+ async def register(self, email: str, password: str) -> LoginResult:
176
+ data = await self._http.post("/api/auth/register", json={"email": email, "password": password})
177
+ result = _parse_login_result(data)
178
+ if result.token and result.refresh_token:
179
+ self._tokens.set_tokens(result.token, result.refresh_token)
180
+ return result
181
+
182
+ async def login(self, email: str, password: str) -> LoginResult:
183
+ data = await self._http.post("/api/auth/login", json={"email": email, "password": password})
184
+ result = _parse_login_result(data)
185
+ if result.token and result.refresh_token:
186
+ self._tokens.set_tokens(result.token, result.refresh_token)
187
+ return result
188
+
189
+ async def verify_mfa(
190
+ self,
191
+ code: str,
192
+ *,
193
+ mfa_challenge_token: Optional[str] = None,
194
+ is_backup_code: bool = False,
195
+ ) -> LoginResult:
196
+ payload: dict = {"code": code, "isBackupCode": is_backup_code}
197
+ if mfa_challenge_token:
198
+ payload["mfaChallengeToken"] = mfa_challenge_token
199
+ data = await self._http.post("/api/auth/mfa/verify", json=payload)
200
+ result = _parse_login_result(data)
201
+ if result.token and result.refresh_token:
202
+ self._tokens.set_tokens(result.token, result.refresh_token)
203
+ return result
204
+
205
+ async def refresh(self, refresh_token: Optional[str] = None) -> tuple[str, str]:
206
+ rt = refresh_token or self._tokens.refresh_token
207
+ if not rt:
208
+ raise ValueError("No refresh token available")
209
+ data = await self._http.post("/api/auth/refresh", json={"refreshToken": rt})
210
+ access = data["token"]
211
+ new_refresh = data["refreshToken"]
212
+ self._tokens.set_tokens(access, new_refresh)
213
+ return (access, new_refresh)
214
+
215
+ async def logout(self) -> None:
216
+ await self._http.post("/api/auth/logout")
217
+ self._tokens.clear()
218
+
219
+ async def me(self) -> User:
220
+ data = await self._http.get("/api/auth/me")
221
+ u = data["user"]
222
+ return User(
223
+ id=u["id"],
224
+ email=u["email"],
225
+ created_at=str(u.get("created_at", "")),
226
+ is_admin=u.get("is_admin", False),
227
+ )
228
+
229
+ async def change_password(self, current_password: str, new_password: str) -> None:
230
+ await self._http.post(
231
+ "/api/auth/change-password",
232
+ json={"currentPassword": current_password, "newPassword": new_password},
233
+ )
234
+
235
+ async def verify_key(self, api_key: str) -> LoginResult:
236
+ data = await self._http.post("/api/auth/verify-key", json={"apiKey": api_key})
237
+ result = LoginResult(
238
+ token=data.get("token"),
239
+ refresh_token=data.get("refreshToken"),
240
+ user=User(
241
+ id=data.get("userId", ""),
242
+ email=data.get("email", ""),
243
+ created_at="",
244
+ ) if data.get("userId") else None,
245
+ )
246
+ if result.token and result.refresh_token:
247
+ self._tokens.set_tokens(result.token, result.refresh_token)
248
+ return result
249
+
250
+ async def mfa_setup(self) -> MfaSetup:
251
+ data = await self._http.post("/api/auth/mfa/setup")
252
+ return MfaSetup(
253
+ secret=data["secret"],
254
+ otpauth_uri=data["otpauthUri"],
255
+ backup_codes=data.get("backupCodes", []),
256
+ )
257
+
258
+ async def mfa_enable(self, code: str) -> bool:
259
+ data = await self._http.post("/api/auth/mfa/enable", json={"code": code})
260
+ return data.get("enabled", False)
261
+
262
+ async def mfa_disable(self, password: str, code: str) -> bool:
263
+ data = await self._http.post("/api/auth/mfa/disable", json={"password": password, "code": code})
264
+ return not data.get("enabled", True)
265
+
266
+ async def mfa_status(self) -> MfaStatus:
267
+ data = await self._http.get("/api/auth/mfa/status")
268
+ return MfaStatus(
269
+ enabled=data["enabled"],
270
+ backup_codes_remaining=data.get("backupCodesRemaining", 0),
271
+ )