mominah commited on
Commit
c760cc8
·
verified ·
1 Parent(s): c359dc4

Update auth.py

Browse files
Files changed (1) hide show
  1. auth.py +155 -13
auth.py CHANGED
@@ -1,12 +1,16 @@
1
  import os
2
  import uuid
3
  import logging
 
 
 
 
4
  from datetime import datetime, timedelta
5
  from urllib.parse import quote_plus
6
  from typing import List, Optional, Any
7
 
8
  from dotenv import load_dotenv
9
- from fastapi import APIRouter, HTTPException, Depends, Request, UploadFile, File, Form
10
  from fastapi.responses import JSONResponse
11
  from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
12
  from jose import JWTError, jwt
@@ -15,6 +19,12 @@ from pydantic import BaseModel, EmailStr, Field, validator
15
  from pymongo import MongoClient
16
  import os.path
17
 
 
 
 
 
 
 
18
  load_dotenv()
19
 
20
  # Setup logging
@@ -34,7 +44,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
34
  # Create an APIRouter instance
35
  router = APIRouter()
36
 
37
- # Pydantic models
38
  class User(BaseModel):
39
  name: str = Field(..., min_length=3, max_length=50)
40
  email: EmailStr
@@ -86,7 +96,12 @@ class LoginResponse(Token):
86
  class TokenData(BaseModel):
87
  email: Optional[str] = None
88
 
89
- # Password hashing
 
 
 
 
 
90
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
91
 
92
  def verify_password(plain_password: str, hashed_password: str) -> bool:
@@ -95,6 +110,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
95
  def get_password_hash(password: str) -> str:
96
  return pwd_context.hash(password)
97
 
 
98
  def get_user(email: str) -> Optional[dict]:
99
  return users_collection.find_one({"email": email})
100
 
@@ -132,7 +148,7 @@ def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
132
  except JWTError:
133
  raise HTTPException(status_code=401, detail="Invalid token")
134
 
135
- # Setup for avatar file saving
136
  AVATAR_DIR = "avatars"
137
  if not os.path.exists(AVATAR_DIR):
138
  os.makedirs(AVATAR_DIR)
@@ -160,7 +176,67 @@ def save_avatar_file(file: UploadFile) -> str:
160
  file.file.close()
161
  return file_path
162
 
163
- # ----- Auth Endpoints -----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
  @router.post("/signup", response_model=Token)
166
  async def signup(
@@ -196,21 +272,87 @@ async def signup(
196
  "token_type": "bearer"
197
  }
198
 
199
- @router.post("/login", response_model=LoginResponse)
200
- async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
201
- user = authenticate_user(form_data.username, form_data.password)
 
 
 
 
202
  if not user:
203
- logger.warning(f"Failed login attempt for: {form_data.username}")
204
- raise HTTPException(status_code=401, detail="Incorrect username or password")
205
- logger.info(f"User logged in: {user['email']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  return {
207
- "access_token": create_access_token(user["email"]),
208
- "refresh_token": create_refresh_token(user["email"]),
209
  "token_type": "bearer",
210
  "name": user["name"],
211
  "avatar": user.get("avatar")
212
  }
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  @router.get("/user/data")
215
  async def get_user_data(request: Request, current_user: dict = Depends(get_current_user)):
216
  return {
 
1
  import os
2
  import uuid
3
  import logging
4
+ import secrets
5
+ import smtplib
6
+ from email.mime.multipart import MIMEMultipart
7
+ from email.mime.text import MIMEText
8
  from datetime import datetime, timedelta
9
  from urllib.parse import quote_plus
10
  from typing import List, Optional, Any
11
 
12
  from dotenv import load_dotenv
13
+ from fastapi import APIRouter, HTTPException, Depends, Request, UploadFile, File, Form, Body
14
  from fastapi.responses import JSONResponse
15
  from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
16
  from jose import JWTError, jwt
 
19
  from pymongo import MongoClient
20
  import os.path
21
 
22
+ # Optionally import markdown to convert markdown text to HTML
23
+ try:
24
+ import markdown
25
+ except ImportError:
26
+ markdown = None
27
+
28
  load_dotenv()
29
 
30
  # Setup logging
 
44
  # Create an APIRouter instance
45
  router = APIRouter()
46
 
47
+ # ------------------ Pydantic Models ------------------
48
  class User(BaseModel):
49
  name: str = Field(..., min_length=3, max_length=50)
50
  email: EmailStr
 
96
  class TokenData(BaseModel):
97
  email: Optional[str] = None
98
 
99
+ # Model for OTP-based login
100
+ class OTPLogin(BaseModel):
101
+ email: EmailStr
102
+ otp: str
103
+
104
+ # ------------------ Password Hashing ------------------
105
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
106
 
107
  def verify_password(plain_password: str, hashed_password: str) -> bool:
 
110
  def get_password_hash(password: str) -> str:
111
  return pwd_context.hash(password)
112
 
113
+ # ------------------ User & Auth Helpers ------------------
114
  def get_user(email: str) -> Optional[dict]:
115
  return users_collection.find_one({"email": email})
116
 
 
148
  except JWTError:
149
  raise HTTPException(status_code=401, detail="Invalid token")
150
 
151
+ # ------------------ Avatar File Saving ------------------
152
  AVATAR_DIR = "avatars"
153
  if not os.path.exists(AVATAR_DIR):
154
  os.makedirs(AVATAR_DIR)
 
176
  file.file.close()
177
  return file_path
178
 
179
+ # ------------------ OTP and Email Functions ------------------
180
+ def generate_otp(length=6):
181
+ """Generate a numeric OTP of specified length."""
182
+ digits = "0123456789"
183
+ otp = ''.join(secrets.choice(digits) for _ in range(length))
184
+ return otp
185
+
186
+ def send_email_func(sender_email, sender_password, receiver_email, subject, body):
187
+ smtp_server = "smtp.gmail.com"
188
+ port = 587
189
+ message = MIMEMultipart("alternative")
190
+ message["Subject"] = subject
191
+ message["From"] = sender_email
192
+ message["To"] = receiver_email
193
+
194
+ part1 = MIMEText(body, "plain")
195
+ if markdown:
196
+ html_content = markdown.markdown(body)
197
+ else:
198
+ html_content = f"<pre>{body}</pre>"
199
+
200
+ html_template = f"""\
201
+ <html>
202
+ <head>
203
+ <style>
204
+ body {{
205
+ font-family: Arial, sans-serif;
206
+ line-height: 1.6;
207
+ margin: 20px;
208
+ }}
209
+ h1, h2, h3 {{
210
+ color: #333;
211
+ }}
212
+ pre {{
213
+ background: #f4f4f4;
214
+ padding: 10px;
215
+ border: 1px solid #ddd;
216
+ }}
217
+ </style>
218
+ </head>
219
+ <body>
220
+ {html_content}
221
+ </body>
222
+ </html>
223
+ """
224
+ part2 = MIMEText(html_template, "html")
225
+ message.attach(part1)
226
+ message.attach(part2)
227
+
228
+ try:
229
+ with smtplib.SMTP(smtp_server, port, timeout=10) as server:
230
+ server.starttls()
231
+ server.login(sender_email, sender_password)
232
+ server.sendmail(sender_email, receiver_email, message.as_string())
233
+ logger.info("Successfully sent email")
234
+ except Exception as e:
235
+ logger.error(f"Error sending email: {e}")
236
+ raise e
237
+ # -------------------------------------------------------------
238
+
239
+ # ------------------ Auth Endpoints ------------------
240
 
241
  @router.post("/signup", response_model=Token)
242
  async def signup(
 
272
  "token_type": "bearer"
273
  }
274
 
275
+ @router.post("/login_otp", response_model=LoginResponse)
276
+ async def login_otp(otp_data: OTPLogin):
277
+ """
278
+ OTP-based login endpoint.
279
+ The user must supply their email and the OTP received via email.
280
+ """
281
+ user = get_user(otp_data.email)
282
  if not user:
283
+ logger.warning(f"Login OTP attempt for non-existent email: {otp_data.email}")
284
+ raise HTTPException(status_code=401, detail="User not found")
285
+
286
+ # Check if OTP exists and is valid
287
+ stored_otp = user.get("otp")
288
+ otp_expires = user.get("otp_expires")
289
+ if not stored_otp or not otp_expires:
290
+ raise HTTPException(status_code=400, detail="OTP not generated. Please request a new OTP.")
291
+
292
+ # Check if OTP is expired (using ISO format stored in DB)
293
+ if datetime.utcnow() > datetime.fromisoformat(otp_expires):
294
+ raise HTTPException(status_code=400, detail="OTP has expired. Please request a new OTP.")
295
+
296
+ if otp_data.otp != stored_otp:
297
+ raise HTTPException(status_code=401, detail="Invalid OTP.")
298
+
299
+ # OTP is valid. Remove OTP fields.
300
+ users_collection.update_one(
301
+ {"email": otp_data.email},
302
+ {"$unset": {"otp": "", "otp_expires": ""}}
303
+ )
304
+ logger.info(f"User logged in with OTP: {otp_data.email}")
305
  return {
306
+ "access_token": create_access_token(otp_data.email),
307
+ "refresh_token": create_refresh_token(otp_data.email),
308
  "token_type": "bearer",
309
  "name": user["name"],
310
  "avatar": user.get("avatar")
311
  }
312
 
313
+ @router.post("/send_otp")
314
+ async def send_otp(receiver_email: EmailStr = Body(..., embed=True)):
315
+ """
316
+ Generates an OTP, stores it in the user's document with an expiration,
317
+ and sends it via email to the provided receiver_email.
318
+ (For demonstration purposes, the OTP is returned. Remove in production.)
319
+ """
320
+ user = get_user(receiver_email)
321
+ if not user:
322
+ raise HTTPException(status_code=404, detail="User not found. Please sign up first.")
323
+
324
+ sender_email = os.getenv("SENDER_EMAIL")
325
+ sender_password = os.getenv("SENDER_PASSWORD")
326
+ if not sender_email or not sender_password:
327
+ raise HTTPException(status_code=500, detail="Email sender credentials are not configured.")
328
+
329
+ otp = generate_otp(6)
330
+ # Set OTP to expire in 5 minutes
331
+ otp_expires = (datetime.utcnow() + timedelta(minutes=1)).isoformat()
332
+ subject = "Your One-Time Password (OTP)"
333
+ body = f"""\
334
+ # Your OTP
335
+
336
+ Here is your one-time password (OTP):
337
+
338
+ `{otp}`
339
+
340
+ Please use the code above to proceed with your login.
341
+ """
342
+ try:
343
+ send_email_func(sender_email, sender_password, receiver_email, subject, body)
344
+ except Exception as e:
345
+ raise HTTPException(status_code=500, detail=f"Error sending OTP email: {str(e)}")
346
+
347
+ # Update user document with OTP and expiration
348
+ users_collection.update_one(
349
+ {"email": receiver_email},
350
+ {"$set": {"otp": otp, "otp_expires": otp_expires}}
351
+ )
352
+
353
+ # For demonstration, return the OTP (remove in production)
354
+ return {"message": "OTP sent successfully", "otp": otp}
355
+
356
  @router.get("/user/data")
357
  async def get_user_data(request: Request, current_user: dict = Depends(get_current_user)):
358
  return {