rohanshaw commited on
Commit
bad62e6
·
verified ·
1 Parent(s): eeac764

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -0
app.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from fastapi import FastAPI, HTTPException, status, Body
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from pydantic import BaseModel, Field, EmailStr
6
+ from motor.motor_asyncio import AsyncIOMotorClient
7
+ from passlib.context import CryptContext
8
+ from dotenv import load_dotenv
9
+ from typing import Optional, List
10
+
11
+ # --- Load Environment Variables ---
12
+ load_dotenv()
13
+ MONGODB_URI = os.getenv("MONGODB_URI")
14
+ if not MONGODB_URI:
15
+ raise ValueError("MONGODB_URI environment variable not set.")
16
+
17
+ # --- Configuration ---
18
+ DATABASE_NAME = "userAuthDB"
19
+ COLLECTION_NAME = "users"
20
+
21
+ # --- Logging Setup ---
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # --- Password Hashing Setup ---
26
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
27
+
28
+ # --- FastAPI App Initialization ---
29
+ app = FastAPI(
30
+ title="User Authentication API",
31
+ description="API for user signup and login for WhereIParked.",
32
+ version="1.0.0",
33
+ )
34
+
35
+ # --- CORS Configuration ---
36
+ origins = ["*"]
37
+
38
+ app.add_middleware(
39
+ CORSMiddleware,
40
+ allow_origins=origins,
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+
47
+ # --- Database Connection ---
48
+ client: Optional[AsyncIOMotorClient] = None
49
+ db = None
50
+
51
+ @app.on_event("startup")
52
+ async def startup_db_client():
53
+ """Connect to MongoDB on application startup."""
54
+ global client, db
55
+ logger.info("Connecting to MongoDB...")
56
+ try:
57
+ client = AsyncIOMotorClient(MONGODB_URI)
58
+ db = client[DATABASE_NAME]
59
+ # Ping the server to check connection
60
+ await client.admin.command('ping')
61
+ logger.info(f"Successfully connected to MongoDB database: {DATABASE_NAME}")
62
+ except Exception as e:
63
+ logger.error(f"Failed to connect to MongoDB: {e}")
64
+ # Depending on your requirement, you might want to exit or handle differently
65
+ raise RuntimeError(f"Could not connect to MongoDB: {e}")
66
+
67
+
68
+ @app.on_event("shutdown")
69
+ async def shutdown_db_client():
70
+ """Disconnect from MongoDB on application shutdown."""
71
+ global client
72
+ if client:
73
+ logger.info("Closing MongoDB connection...")
74
+ client.close()
75
+ logger.info("MongoDB connection closed.")
76
+
77
+ # --- Pydantic Models ---
78
+
79
+ class UserBase(BaseModel):
80
+ """Base model for user data."""
81
+ email: EmailStr = Field(..., example="user@example.com")
82
+
83
+ class UserCreate(UserBase):
84
+ """Model for user creation (signup)."""
85
+ username: str = Field(..., min_length=3, max_length=50, example="john_doe")
86
+ password: str = Field(..., min_length=8, example="strongpassword123")
87
+ class UserLogin(BaseModel):
88
+ """Model for user login."""
89
+ email: EmailStr = Field(..., example="user@example.com")
90
+ password: str = Field(..., example="strongpassword123")
91
+
92
+ class UserInDB(UserBase):
93
+ """Model representing user data stored in the database."""
94
+ username: str
95
+ hashed_password: str
96
+
97
+ class Token(BaseModel):
98
+ """Model for returning success/token (optional)."""
99
+ message: str
100
+ # access_token: str # Example if using JWT
101
+ # token_type: str # Example if using JWT
102
+
103
+ class SignupResponse(BaseModel):
104
+ """Response model for successful signup."""
105
+ message: str = Field(..., example="Signup successful!")
106
+
107
+ class LoginSuccessResponse(BaseModel):
108
+ """Response model for successful login."""
109
+ message: str = Field(..., example="Login successful!")
110
+ email: EmailStr = Field(..., example="user@example.com")
111
+
112
+ # --- Helper Functions ---
113
+ def verify_password(plain_password, hashed_password):
114
+ return pwd_context.verify(plain_password, hashed_password)
115
+
116
+ def get_password_hash(password):
117
+ return pwd_context.hash(password)
118
+
119
+ async def get_user_by_email(email: str) -> Optional[dict]:
120
+ """Retrieves a user from the database by email."""
121
+ if db is None:
122
+ # Log the error and raise an appropriate HTTP exception
123
+ logger.error("Database connection is not available in get_user_by_email")
124
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service is unavailable.")
125
+ try:
126
+ user = await db[COLLECTION_NAME].find_one({"email": email})
127
+ return user # Returns dict or None
128
+ except Exception as e:
129
+ logger.error(f"Error fetching user by email ({email}): {e}")
130
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error accessing user data.")
131
+
132
+
133
+ async def get_user_by_username(username: str) -> Optional[dict]:
134
+ """Retrieves a user from the database by username."""
135
+ if db is None:
136
+ logger.error("Database connection is not available in get_user_by_username")
137
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service is unavailable.")
138
+ try:
139
+ user = await db[COLLECTION_NAME].find_one({"username": username})
140
+ return user # Returns dict or None
141
+ except Exception as e:
142
+ logger.error(f"Error fetching user by username ({username}): {e}")
143
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error accessing user data.")
144
+
145
+
146
+ # --- API Endpoints ---
147
+
148
+ @app.post("/signup", response_model=SignupResponse, status_code=status.HTTP_201_CREATED)
149
+ async def signup(user_data: UserCreate = Body(...)):
150
+ """Handles user registration."""
151
+ if db is None: # Check added for safety, though startup should handle it
152
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service is unavailable.")
153
+
154
+ existing_user_email = await get_user_by_email(user_data.email)
155
+ if existing_user_email:
156
+ raise HTTPException(
157
+ status_code=status.HTTP_400_BAD_REQUEST,
158
+ detail="Email already registered.",
159
+ )
160
+ existing_user_username = await get_user_by_username(user_data.username)
161
+ if existing_user_username:
162
+ raise HTTPException(
163
+ status_code=status.HTTP_400_BAD_REQUEST,
164
+ detail="Username already taken.",
165
+ )
166
+
167
+ hashed_password = get_password_hash(user_data.password)
168
+ user_in_db = UserInDB(
169
+ username=user_data.username,
170
+ email=user_data.email,
171
+ hashed_password=hashed_password
172
+ )
173
+
174
+ try:
175
+ new_user = await db[COLLECTION_NAME].insert_one(user_in_db.dict(by_alias=True)) # Use dict() for Pydantic v1/v2 compatibility
176
+ logger.info(f"User created successfully with ID: {new_user.inserted_id}")
177
+ except Exception as e:
178
+ logger.error(f"Error inserting user into database: {e}")
179
+ raise HTTPException(
180
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
181
+ detail="An error occurred while creating the account.",
182
+ )
183
+
184
+ return SignupResponse(message="Signup successful!")
185
+
186
+
187
+ # Updated: Changed response_model to LoginSuccessResponse
188
+ @app.post("/login", response_model=LoginSuccessResponse)
189
+ async def login(login_data: UserLogin = Body(...)):
190
+ """Handles user login. Returns user email on success."""
191
+ if db is None:
192
+ raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service is unavailable.")
193
+
194
+ user = await get_user_by_email(login_data.email)
195
+
196
+ if not user or not verify_password(login_data.password, user["hashed_password"]):
197
+ raise HTTPException(
198
+ status_code=status.HTTP_401_UNAUTHORIZED,
199
+ detail="Incorrect email or password.",
200
+ headers={"WWW-Authenticate": "Bearer"},
201
+ )
202
+
203
+ # Return success message and user email
204
+ logger.info(f"User {user['email']} logged in successfully.")
205
+ return LoginSuccessResponse(
206
+ message="Login successful!",
207
+ email=user["email"]
208
+ # username=user["username"] # Optionally include username
209
+ )
210
+
211
+
212
+ @app.get("/", status_code=status.HTTP_200_OK)
213
+ async def read_root():
214
+ """Root endpoint to check if the API is running."""
215
+ return {"message": "Authentication API is running!"}
216
+