@@ -89,9 +89,56 @@ def handle_error(self, error) -> str:
8989 print (f"Error: { error } " )
9090 raise Exception (f"Unable to retrieve data. { error } " )
9191
92- def login (self , email , otp ):
92+ def login (self , username : str , password : str , mfa_token : str = None ):
93+ """
94+ Login with username/password authentication.
95+
96+ Args:
97+ username: Username or email address
98+ password: User's password
99+ mfa_token: Optional TOTP code if MFA is enabled
100+
101+ Returns:
102+ JWT token on success, or response dict on failure
103+ """
104+ payload = {
105+ "username" : username ,
106+ "password" : password ,
107+ }
108+ if mfa_token :
109+ payload ["mfa_token" ] = mfa_token
110+
93111 response = requests .post (
94112 f"{ self .base_uri } /v1/login" ,
113+ json = payload ,
114+ )
115+ if self .verbose :
116+ parse_response (response )
117+
118+ result = response .json ()
119+ if response .status_code == 200 :
120+ token = result .get ("token" )
121+ if token :
122+ self .headers = {"Authorization" : token }
123+ if self .verbose :
124+ print (f"Logged in successfully" )
125+ return token
126+ return result
127+
128+ def login_magic_link (self , email : str , otp : str ):
129+ """
130+ Legacy login with magic link (email + OTP token).
131+ Maintained for backward compatibility.
132+
133+ Args:
134+ email: User's email address
135+ otp: TOTP code from authenticator app
136+
137+ Returns:
138+ JWT token on success, or response dict on failure
139+ """
140+ response = requests .post (
141+ f"{ self .base_uri } /v1/login/magic-link" ,
95142 json = {"email" : email , "token" : otp },
96143 )
97144 if self .verbose :
@@ -105,26 +152,171 @@ def login(self, email, otp):
105152 if self .verbose :
106153 print (f"Log in at { detail } " )
107154 return token
155+ return response
156+
157+ def register_user (
158+ self ,
159+ email : str ,
160+ password : str ,
161+ confirm_password : str ,
162+ first_name : str = "" ,
163+ last_name : str = "" ,
164+ username : str = None ,
165+ organization_name : str = "" ,
166+ ):
167+ """
168+ Register a new user with username/password authentication.
169+
170+ Args:
171+ email: User's email address
172+ password: User's password
173+ confirm_password: Password confirmation
174+ first_name: User's first name (optional)
175+ last_name: User's last name (optional)
176+ username: Desired username (optional, auto-generated from email if not provided)
177+ organization_name: Company/organization name (optional)
178+
179+ Returns:
180+ Response dict with user_id, username, token on success
181+ """
182+ payload = {
183+ "email" : email ,
184+ "password" : password ,
185+ "confirm_password" : confirm_password ,
186+ "first_name" : first_name ,
187+ "last_name" : last_name ,
188+ }
189+ if username :
190+ payload ["username" ] = username
191+ if organization_name :
192+ payload ["organization_name" ] = organization_name
108193
109- def register_user (self , email , first_name , last_name ):
110- login_response = requests .post (
194+ response = requests .post (
111195 f"{ self .base_uri } /v1/user" ,
196+ json = payload ,
197+ )
198+ if self .verbose :
199+ parse_response (response )
200+
201+ result = response .json ()
202+ if response .status_code == 200 :
203+ # Automatically set the token for subsequent requests
204+ token = result .get ("token" )
205+ if token :
206+ self .headers = {"Authorization" : token }
207+ if self .verbose :
208+ print (f"Registered and logged in as { result .get ('username' )} " )
209+ return result
210+
211+ def get_mfa_setup (self ):
212+ """
213+ Get MFA setup information including QR code URI.
214+
215+ Returns:
216+ Dict with provisioning_uri, secret, and mfa_enabled status
217+ """
218+ response = requests .get (
219+ f"{ self .base_uri } /v1/user/mfa/setup" ,
220+ headers = self .headers ,
221+ )
222+ if self .verbose :
223+ parse_response (response )
224+ return response .json ()
225+
226+ def enable_mfa (self , mfa_token : str ):
227+ """
228+ Enable MFA for the current user.
229+
230+ Args:
231+ mfa_token: TOTP code from authenticator app to verify setup
232+
233+ Returns:
234+ Response dict with success message
235+ """
236+ response = requests .post (
237+ f"{ self .base_uri } /v1/user/mfa/enable" ,
238+ headers = self .headers ,
239+ json = {"mfa_token" : mfa_token },
240+ )
241+ if self .verbose :
242+ parse_response (response )
243+ return response .json ()
244+
245+ def disable_mfa (self , password : str = None , mfa_token : str = None ):
246+ """
247+ Disable MFA for the current user.
248+
249+ Args:
250+ password: User's password (optional)
251+ mfa_token: Current TOTP code (optional)
252+
253+ Returns:
254+ Response dict with success message
255+ """
256+ payload = {}
257+ if password :
258+ payload ["password" ] = password
259+ if mfa_token :
260+ payload ["mfa_token" ] = mfa_token
261+
262+ response = requests .post (
263+ f"{ self .base_uri } /v1/user/mfa/disable" ,
264+ headers = self .headers ,
265+ json = payload ,
266+ )
267+ if self .verbose :
268+ parse_response (response )
269+ return response .json ()
270+
271+ def change_password (
272+ self , current_password : str , new_password : str , confirm_password : str
273+ ):
274+ """
275+ Change the current user's password.
276+
277+ Args:
278+ current_password: Current password
279+ new_password: New password
280+ confirm_password: New password confirmation
281+
282+ Returns:
283+ Response dict with success message
284+ """
285+ response = requests .post (
286+ f"{ self .base_uri } /v1/user/password/change" ,
287+ headers = self .headers ,
112288 json = {
113- "email " : email ,
114- "first_name " : first_name ,
115- "last_name " : last_name ,
289+ "current_password " : current_password ,
290+ "new_password " : new_password ,
291+ "confirm_password " : confirm_password ,
116292 },
117293 )
118294 if self .verbose :
119- parse_response (login_response )
120- response = login_response .json ()
121- if "otp_uri" in response :
122- mfa_token = str (response ["otp_uri" ]).split ("secret=" )[1 ].split ("&" )[0 ]
123- totp = pyotp .TOTP (mfa_token )
124- self .login (email = email , otp = totp .now ())
125- return response ["otp_uri" ]
126- else :
127- return response
295+ parse_response (response )
296+ return response .json ()
297+
298+ def set_password (self , new_password : str , confirm_password : str ):
299+ """
300+ Set a password for users who don't have one (migrating from magic link).
301+
302+ Args:
303+ new_password: New password
304+ confirm_password: New password confirmation
305+
306+ Returns:
307+ Response dict with success message and username
308+ """
309+ response = requests .post (
310+ f"{ self .base_uri } /v1/user/password/set" ,
311+ headers = self .headers ,
312+ json = {
313+ "new_password" : new_password ,
314+ "confirm_password" : confirm_password ,
315+ },
316+ )
317+ if self .verbose :
318+ parse_response (response )
319+ return response .json ()
128320
129321 def user_exists (self , email ):
130322 response = requests .get (f"{ self .base_uri } /v1/user/exists?email={ email } " )
0 commit comments