main
py 403 lines 13.6 KB
Raw
1 import os
2 from datetime import timedelta
3
4 from fastapi import APIRouter
5 from fastapi import Depends
6 from fastapi import HTTPException
7 from fastapi import Security
8 from fastapi import status
9 from fastapi.security import OAuth2PasswordRequestForm
10 from loguru import logger
11 from sqlalchemy.ext.asyncio import AsyncSession
12
13 from app.auth.models.users import PasswordReset
14 from app.auth.models.users import RoleEnum
15 from app.auth.models.users import User
16 from app.auth.models.users import UserInput
17 from app.auth.models.users import UserLogin
18 from app.auth.schema.auth import Token
19 from app.auth.schema.auth import UpdateUserRoleRequest
20 from app.auth.schema.auth import UserLoginResponse
21 from app.auth.schema.auth import UserResponse
22 from app.auth.schema.user import UserBaseResponse
23 from app.auth.services.totp import is_2fa_enabled
24 from app.auth.services.universal import delete_user
25 from app.auth.services.universal import find_user
26 from app.auth.services.universal import select_all_users
27 from app.auth.utils import AuthHandler
28 from app.db.db_session import get_db
29
30 ACCESS_TOKEN_EXPIRE_MINUTES = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", "1440"))
31
32 auth_router = APIRouter()
33 auth_handler = AuthHandler()
34
35
36 @auth_router.post("/token")
37 async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), session: AsyncSession = Depends(get_db)):
38 """
39 Authenticates a user and generates an access token.
40
41 Args:
42 form_data (OAuth2PasswordRequestForm): The form data containing the username and password.
43 session (AsyncSession): The database session.
44
45 Returns:
46 dict: A dictionary containing the access token and token type.
47
48 Raises:
49 HTTPException: If user is customer_user role trying to access main portal.
50 """
51 user = await auth_handler.authenticate_user(form_data.username, form_data.password)
52 if not user:
53 raise HTTPException(
54 status_code=status.HTTP_401_UNAUTHORIZED,
55 detail="Incorrect username or password",
56 headers={"WWW-Authenticate": "Bearer"},
57 )
58
59 # Check if user is customer_user role
60 if user.role_id == RoleEnum.customer_user.value:
61 logger.warning(f"Customer user {user.username} attempted to log in to main portal")
62 raise HTTPException(
63 status_code=status.HTTP_403_FORBIDDEN,
64 detail="This account is registered for the Customer Portal only. Please log in at the Customer Portal to access your account.",
65 headers={"WWW-Authenticate": "Bearer"},
66 )
67
68 # Check if user has 2FA enabled
69 if await is_2fa_enabled(user.id):
70 from app.auth.routes.totp import _create_temp_token
71
72 temp_token = _create_temp_token(user.username)
73 logger.info(f"User {user.username} requires 2FA verification")
74 return {
75 "access_token": temp_token,
76 "token_type": "bearer",
77 "requires_2fa": True,
78 }
79
80 access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
81 access_token = await auth_handler.encode_token(user.username, access_token_expires)
82 logger.info(f"User {user.username} logged in successfully")
83 return {"access_token": access_token, "token_type": "bearer"}
84
85
86 @auth_router.post("/token/customer-portal", response_model=Token)
87 async def login_for_customer_portal(
88 form_data: OAuth2PasswordRequestForm = Depends(),
89 session: AsyncSession = Depends(get_db),
90 ):
91 user = await auth_handler.authenticate_user(form_data.username, form_data.password)
92 if not user:
93 raise HTTPException(
94 status_code=status.HTTP_401_UNAUTHORIZED,
95 detail="Incorrect username or password",
96 headers={"WWW-Authenticate": "Bearer"},
97 )
98
99 if user.role_id != RoleEnum.customer_user.value:
100 raise HTTPException(
101 status_code=status.HTTP_403_FORBIDDEN,
102 detail="This account does not have access to the Customer Portal.",
103 headers={"WWW-Authenticate": "Bearer"},
104 )
105
106 # Fetch assigned customer codes
107 from sqlalchemy import select
108
109 from app.auth.models.users import UserCustomerAccess
110
111 result = await session.execute(select(UserCustomerAccess.customer_code).where(UserCustomerAccess.user_id == user.id))
112 customer_codes = result.scalars().all()
113
114 access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
115 access_token = await auth_handler.encode_token(
116 user.username,
117 access_token_expires,
118 extra_claims={"customer_codes": customer_codes},
119 )
120 return {"access_token": access_token, "token_type": "bearer"}
121
122
123 @auth_router.get("/refresh", response_model=Token)
124 async def refresh_token(current_user: User = Depends(auth_handler.get_current_user)):
125 """
126 Refreshes the access token for the current user.
127
128 Parameters:
129 - current_user (User): The current authenticated user.
130
131 Returns:
132 - dict: A dictionary containing the refreshed access token and token type.
133 """
134 access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
135 access_token = await auth_handler.encode_token(
136 current_user.username,
137 access_token_expires,
138 )
139 return {"access_token": access_token, "token_type": "bearer"}
140
141
142 @auth_router.post(
143 "/register",
144 response_model=UserResponse,
145 status_code=201,
146 description="Register new user",
147 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
148 )
149 async def register(user: UserInput, session: AsyncSession = Depends(get_db)):
150 """
151 Register a new user.
152
153 Args:
154 user (UserInput): The user input data.
155 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
156
157 Returns:
158 dict: A dictionary containing the message and success status.
159 """
160 users = await select_all_users()
161 if any(x.email == user.email for x in users):
162 raise HTTPException(status_code=400, detail="Email is already registered")
163 if any(x.username == user.username for x in users):
164 raise HTTPException(status_code=400, detail="Username is taken")
165 hashed_pwd = auth_handler.get_password_hash(user.password)
166 u = User(
167 username=user.username,
168 password=hashed_pwd,
169 email=user.email,
170 role_id=user.role_id.value if user.role_id else 2,
171 )
172 logger.info(f"User: {u}")
173 session.add(u)
174 await session.commit()
175 return {"message": "User created successfully", "success": True}
176
177
178 @auth_router.post(
179 "/login",
180 response_model=UserLoginResponse,
181 description="Login user",
182 deprecated=True,
183 )
184 async def login(user: UserLogin, session: AsyncSession = Depends(get_db)):
185 """
186 Logs in a user.
187
188 Args:
189 user (UserLogin): The user login credentials.
190 session (AsyncSession): The database session.
191
192 Returns:
193 dict: A dictionary containing the authentication token, success status, and a message.
194
195 Raises:
196 HTTPException: If user is customer_user role trying to access main portal.
197 """
198 user_found = await find_user(user.username)
199 if not user_found:
200 raise HTTPException(status_code=401, detail="Invalid username and/or password")
201
202 verified = auth_handler.verify_password(user.password, user_found.password)
203 if not verified:
204 raise HTTPException(status_code=401, detail="Invalid username and/or password")
205
206 # Check if user is customer_user role
207 if user_found.role_id == RoleEnum.customer_user.value:
208 logger.warning(f"Customer user {user_found.username} attempted to log in to main portal")
209 raise HTTPException(
210 status_code=status.HTTP_403_FORBIDDEN,
211 detail="This account is registered for the Customer Portal only. Please log in at the Customer Portal to access your account.",
212 )
213
214 token = await auth_handler.encode_token(user_found.username)
215 return {"token": token, "success": True, "message": "Login successful"}
216
217
218 # Get all users
219 @auth_router.get(
220 "/users",
221 response_model=UserBaseResponse,
222 description="Get all users",
223 dependencies=[Security(AuthHandler().require_any_scope("analyst", "admin"))],
224 )
225 async def get_users(session: AsyncSession = Depends(get_db)):
226 """
227 Retrieve all users from the database.
228
229 Parameters:
230 - session: AsyncSession - The database session.
231
232 Returns:
233 - UserBaseResponse: The response containing the retrieved users.
234
235 Raises:
236 - None
237
238 """
239 users = await select_all_users()
240
241 # Transform users to include role_name
242 user_list = []
243 for user in users:
244 user_dict = {
245 "id": user.id,
246 "username": user.username,
247 "email": user.email,
248 "role_id": user.role_id,
249 "role_name": user.role.name if user.role else None,
250 }
251 user_list.append(user_dict)
252
253 return UserBaseResponse(
254 users=user_list,
255 message="Users retrieved successfully",
256 success=True,
257 )
258
259
260 # Reset a user's password via the username, must be an admin
261 @auth_router.post(
262 "/reset-password",
263 status_code=200,
264 description="Reset user's password via username",
265 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
266 )
267 async def reset_password_via_username(
268 request: PasswordReset,
269 session: AsyncSession = Depends(get_db),
270 ):
271 """
272 Reset a user's password via the username. Must be an admin.
273
274 Args:
275 request (PasswordReset): The password reset data.
276 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
277
278 Returns:
279 dict: A dictionary containing the message and success status.
280 """
281 user = await find_user(request.username)
282 if not user:
283 raise HTTPException(status_code=404, detail="User not found")
284 hashed_pwd = auth_handler.get_password_hash(request.new_password)
285 user.password = hashed_pwd
286 session.add(user)
287 await session.commit()
288 return {"message": "Password reset successfully", "success": True}
289
290
291 # Reset a users password for themselves. The user must be logged in and the token decoded to get the username
292 @auth_router.post(
293 "/reset-password/me",
294 status_code=200,
295 description="Reset user's password",
296 dependencies=[Security(AuthHandler().require_any_scope("analyst", "admin", "customer_user"))],
297 )
298 async def reset_password_me(
299 request: PasswordReset,
300 token: str = Depends(AuthHandler().security),
301 session: AsyncSession = Depends(get_db),
302 ):
303 """
304 Reset a user's password.
305
306 Args:
307 request (PasswordReset): The password reset data.
308 token (str, optional): The authentication token. Defaults to Depends(AuthHandler().security).
309 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
310
311 Returns:
312 dict: A dictionary containing the message and success status.
313 """
314 user = await find_user(request.username)
315 if not user:
316 raise HTTPException(status_code=404, detail="User not found")
317 await auth_handler.verify_reset_token_me(token, user)
318 if request.current_password is not None and not auth_handler.verify_password(request.current_password, user.password):
319 raise HTTPException(status_code=400, detail="Current password is incorrect")
320 hashed_pwd = auth_handler.get_password_hash(request.new_password)
321 user.password = hashed_pwd
322 session.add(user)
323 await session.commit()
324 return {"message": "Password reset successfully", "success": True}
325
326
327 # ! Delete a user by user id ! #
328 @auth_router.delete(
329 "/delete/{user_id}",
330 status_code=200,
331 description="Delete a user by user id",
332 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
333 )
334 async def delete_user_by_username(
335 user_id: int,
336 session: AsyncSession = Depends(get_db),
337 ):
338 """
339 Delete a user by user id.
340
341 Args:
342 user_id (int): The ID of the user to delete.
343 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
344
345 Returns:
346 dict: A dictionary containing the message and success status.
347 """
348 return await delete_user(user_id, session)
349
350
351 @auth_router.put(
352 "/users/{user_id}/role/by-name",
353 status_code=200,
354 description="Update a user's role by role name",
355 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
356 )
357 async def update_user_role_by_name(
358 user_id: int,
359 request: UpdateUserRoleRequest,
360 session: AsyncSession = Depends(get_db),
361 ):
362 """
363 Update a user's role by role name. Must be an admin.
364
365 Args:
366 user_id (int): The ID of the user to update.
367 request (UpdateUserRoleRequest): The role update request containing role name.
368 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
369
370 Returns:
371 dict: A dictionary containing the message and success status.
372 """
373 # First, find the user
374 user = await session.get(User, user_id)
375 if not user:
376 raise HTTPException(status_code=404, detail="User not found")
377
378 # Map role names to IDs
379 role_mapping = {
380 "admin": RoleEnum.admin.value,
381 "analyst": RoleEnum.analyst.value,
382 "scheduler": RoleEnum.scheduler.value,
383 "customer_user": RoleEnum.customer_user.value,
384 }
385
386 role_name_lower = request.role_name.lower()
387 if role_name_lower not in role_mapping:
388 raise HTTPException(status_code=400, detail=f"Invalid role name. Valid roles are: {list(role_mapping.keys())}")
389
390 role_id = role_mapping[role_name_lower]
391
392 # Update the user's role
393 user.role_id = role_id
394 session.add(user)
395 await session.commit()
396
397 return {
398 "message": f"User {user.username} role updated successfully to {request.role_name}",
399 "success": True,
400 "user_id": user_id,
401 "new_role_name": request.role_name,
402 "new_role_id": role_id,
403 }