users.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. from datetime import datetime
  2. import json
  3. from logging import getLogger
  4. import re
  5. from uuid import uuid4
  6. from models import user
  7. import redis
  8. from cryptography.fernet import Fernet
  9. from fastapi import APIRouter, Depends, Request
  10. from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
  11. from fastapi.exceptions import HTTPException
  12. from auth.security import generate_token
  13. from auth.security import get_current_user
  14. from config.mails import REGISTER_MAIL, PIN_RECOVERY_MAIL, PIN_SUCCESSFULLY
  15. from config.messages import ErrorResponse, SuccessResponse, UserResponse
  16. from config.settings import APPNAME, DEVELOPMENT, PIN_KEY
  17. from models.user import LoginRequest, PinRecoveryRequest, PinRecoveryValidateRequest, PinUserRequest, RegisterUserRequest, User, UserIDRequest, UserMail, UserRewardRequest
  18. from services.data_service import BlacklistDataService, UserDataService
  19. from services.email_service import get_email_sender
  20. from services.print_service import print_ticket
  21. import services.recovery_service as recovery_service
  22. from utils.rut import validate_rut
  23. fernet = Fernet(PIN_KEY.encode())
  24. logger = getLogger(__name__)
  25. user_data_service = UserDataService()
  26. blacklist_data_service = BlacklistDataService()
  27. user_router = APIRouter()
  28. redis_client = redis.Redis(host='localhost', port=6379, db=1 if DEVELOPMENT else 0, decode_responses=True)
  29. def unique_pin_generate():
  30. """Generate a unique 4-digit PIN"""
  31. import random
  32. pin = str(random.randint(1000, 9999))
  33. return pin
  34. @user_router.post("/exists")
  35. async def exists_user(request: UserIDRequest):
  36. """Check if user exists"""
  37. user = user_data_service.get_by_id(request.id)
  38. if user:
  39. return JSONResponse(status_code=200, content={"exists": True, "message": UserResponse.USER_EXISTS})
  40. else:
  41. return JSONResponse(status_code=404, content={"exists": False, "message": UserResponse.USER_DOES_NOT_EXIST})
  42. @user_router.post("/register")
  43. async def register_user(request: RegisterUserRequest):
  44. """Register a new user"""
  45. logger.info(f"Registration attempt for email: {request.email}")
  46. # Validate RUT
  47. if not validate_rut(request.rut):
  48. logger.warning(f"Registration failed for {request.email}: invalid RUT {request.rut}")
  49. raise HTTPException(status_code=400, detail=ErrorResponse.INVALID_RUT)
  50. # Check if user already exists by email
  51. try:
  52. user = user_data_service.get_by_email(request.email)
  53. if user:
  54. logger.warning(f"Registration failed for {request.email}: user already exists")
  55. return HTTPException(status_code=400, detail=UserResponse.USER_ALREADY_EXISTS)
  56. # Check if RUT already exists
  57. user = user_data_service.get_by_rut(request.rut)
  58. if user:
  59. logger.warning(f"Registration failed for {request.email}: RUT already exists")
  60. return HTTPException(status_code=400, detail=UserResponse.USER_ALREADY_EXISTS)
  61. except Exception as e:
  62. error_msg = f"Database error during user validation: {e}"
  63. logger.error(error_msg)
  64. logger.info(f"Registering user: {request.email}")
  65. try:
  66. verification_code = str(uuid4())
  67. user_data = {
  68. "name": request.name,
  69. "email": request.email,
  70. "rut": request.rut
  71. }
  72. redis_client.set(f"verify:{verification_code}", json.dumps(user_data))
  73. redis_client.expire(f"verify:{verification_code}", 3600) # Expire in 1 hour
  74. logger.info(f"Verification code generated for {request.email}, code: {verification_code}")
  75. # Send verification email
  76. get_email_sender().send_email(
  77. REGISTER_MAIL["subject"],
  78. REGISTER_MAIL["body"],
  79. [request.email],
  80. name=request.name,
  81. app_name=APPNAME,
  82. verification_code=verification_code
  83. )
  84. return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS})
  85. except Exception as e:
  86. error_msg = f"Error during registration process for {request.email}: {e}"
  87. logger.error(error_msg)
  88. return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
  89. @user_router.post("/create-user")
  90. async def create_user(request: PinUserRequest, q: str):
  91. """Create a new user with PIN"""
  92. data = redis_client.get(f"verify:{q}")
  93. if not redis_client.get(f"verify:{q}"):
  94. return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_VERIFICATION_CODE})
  95. else:
  96. data = json.loads(str(data))
  97. name = data.get("name")
  98. email = data.get("email")
  99. rut = data.get("rut")
  100. pin = request.pin
  101. if not request.pin or len(request.pin) != 4:
  102. return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_PIN})
  103. userID = user_data_service.create(name, email, rut, pin)
  104. if userID == -1:
  105. return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
  106. user = user_data_service.get_by_id(userID)
  107. if not user:
  108. logger.error(f"User creation failed for {email}: user not found after creation")
  109. return JSONResponse(status_code=500, content={"message": ErrorResponse.USER_CREATION_ERROR})
  110. logger.info(f"User created successfully: {email}")
  111. return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
  112. **user.model_dump(exclude={"pin_hash"}),
  113. "token": generate_token(user.email)
  114. }})
  115. @user_router.post("/login")
  116. async def login_user(request: LoginRequest, http_request: Request):
  117. """Login user with email and PIN"""
  118. logger.info(f"Login attempt for email: {request.email}")
  119. try:
  120. is_blocked = redis_client.get(f"blocked:{request.email}")
  121. if is_blocked:
  122. try:
  123. blocked_time_raw = redis_client.ttl(f"blocked:{request.email}")
  124. blocked_minutes = max(0, int(blocked_time_raw) // 60) if blocked_time_raw and int(blocked_time_raw) > 0 else 0 # type: ignore
  125. except (ValueError, TypeError):
  126. blocked_minutes = 0
  127. logger.warning(f"Login attempt for blocked user: {request.email}, blocked for {blocked_minutes} minutes")
  128. return JSONResponse(
  129. status_code=403,
  130. content={"message": UserResponse.USER_FORMAT_BLOCKED.format(time=f"{blocked_minutes} minutos")}
  131. )
  132. # Attempt login
  133. user = user_data_service.login(request.email, request.pin)
  134. if user:
  135. if blacklist_data_service.is_user_blacklisted(user.id):
  136. logger.warning(f"Login attempt for blacklisted user: {request.email}")
  137. return JSONResponse(
  138. status_code=403,
  139. content={"message": UserResponse.USER_BLACKLISTED}
  140. )
  141. # Successful login
  142. logger.info(f"Successful login for user: {request.email}")
  143. # Check admin access
  144. referer = http_request.headers.get("Origin", "")
  145. logger.info(f"Login request referer: {referer}")
  146. if referer and "admin" in referer:
  147. user_permissions = user_data_service.permissions(user.id)
  148. if user_permissions == 0:
  149. logger.warning(f"Unauthorized admin access attempt by {request.email}")
  150. return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
  151. # Clear login attempts and log successful login
  152. redis_client.delete(f"login_attempts:{request.email}")
  153. return JSONResponse(status_code=200, content={
  154. "message": SuccessResponse.LOGIN_SUCCESS,
  155. "data": {
  156. "id": user.id,
  157. "name": user.name,
  158. "email": user.email,
  159. "kleincoins": user.kleincoins,
  160. "created_at": user.created_at,
  161. "token": generate_token(user.email),
  162. "reward_progress": user.reward_progress,
  163. }
  164. })
  165. else:
  166. # Failed login: increment attempts in Redis
  167. redis_client.incr(f"login_attempts:{request.email}")
  168. redis_client.expire(f"login_attempts:{request.email}", 3600)
  169. redis_attempts = redis_client.get(f"login_attempts:{request.email}")
  170. attempts = int(redis_attempts) if redis_attempts else 0 # type: ignore
  171. if attempts >= 5:
  172. # Too many attempts, block login
  173. redis_client.set(f"blocked:{request.email}", "true")
  174. redis_client.expire(f"blocked:{request.email}", 3600)
  175. logger.warning(f"Too many login attempts for {request.email}. User blocked.")
  176. return JSONResponse(status_code=429, content={"message": ErrorResponse.TOO_MANY_ATTEMPTS})
  177. else:
  178. logger.warning(f"Failed login attempt for {request.email}. Attempts: {attempts}")
  179. # Return unauthorized with attempts remaining
  180. return JSONResponse(status_code=401, content={
  181. "message": ErrorResponse.INVALID_CREDENTIALS,
  182. "attempts_remaining": 5 - attempts if attempts else 5
  183. })
  184. except redis.RedisError as e:
  185. error_msg = f"Redis error during login for {request.email}: {e}"
  186. logger.error(error_msg)
  187. return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
  188. except Exception as e:
  189. error_msg = f"Unexpected error during login for {request.email}: {e}"
  190. logger.error(error_msg)
  191. return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
  192. @user_router.delete("/delete")
  193. async def delete_user(request: UserIDRequest):
  194. """Delete a user by ID"""
  195. user = user_data_service.delete(request.id)
  196. if user:
  197. return JSONResponse(status_code=200, content={"message": SuccessResponse.USER_DELETED_SUCCESS, "data": user})
  198. else:
  199. return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND})
  200. @user_router.post("/pin-recovery")
  201. async def change_pin(request: PinRecoveryRequest):
  202. """Change a user's PIN"""
  203. user = user_data_service.get_by_email(request.email)
  204. if not user:
  205. return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)})
  206. real_token = recovery_service.get_token(user.id)
  207. if real_token and real_token != request.token:
  208. return JSONResponse(status_code=400, content={"message": "Invalid token"})
  209. logger.info(f"Pin change, to {request.new_pin} for user {user.email}")
  210. user_data_service.update(user_id=user.id, pin_hash=request.new_pin)
  211. sender = get_email_sender()
  212. sender.send_email(
  213. to=[user.email],
  214. subject=PIN_SUCCESSFULLY["subject"],
  215. body=PIN_SUCCESSFULLY["body"].format(app_name=APPNAME, date=datetime.now().strftime("%Y-%m-%d"), time=datetime.now().strftime("%H:%M:%S"), name=user.name)
  216. )
  217. return JSONResponse(status_code=200, content={"message": "Recovery email sent"})
  218. @user_router.post("/reward")
  219. async def reward_user(request: UserRewardRequest, user: User = Depends(get_current_user)):
  220. """Reward a user with 1 free beer"""
  221. if user.reward_progress < 100:
  222. return JSONResponse(status_code=400, content={"message": UserResponse.REWARD_INSUFFICIENT_PROGRESS.format(progress=user.reward_progress)})
  223. if not user:
  224. return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.id)})
  225. user_data_service.set_reward_progress(user.id, 0)
  226. print_ticket(request.tableNumber)
  227. return JSONResponse(status_code=200, content={"message": SuccessResponse.REWARD_SUCCESS, "data": {
  228. "id": user.id,
  229. "name": user.name,
  230. "email": user.email,
  231. "reward_progress": 0
  232. }})
  233. @user_router.get("/user")
  234. async def get_cur_user(current_user:User = Depends(get_current_user)):
  235. """Get current user information"""
  236. return JSONResponse(status_code=200, content={"data": current_user.model_dump(exclude={"pin_hash", "kleincoins", "rut"})})
  237. @user_router.get("/all")
  238. async def get_all_users():
  239. """Get all users"""
  240. users = list(map(lambda u: u.model_dump(), user_data_service.get_all()))
  241. return JSONResponse(status_code=200, content={"data": users})
  242. from fastapi import Query
  243. verify_router = APIRouter()
  244. @verify_router.get("/")
  245. async def verify_user(q: str = Query(..., description="q parameter")):
  246. """Verify a user by ID"""
  247. # get url params
  248. if not redis_client.get(f"verify:{q}"):
  249. return HTMLResponse(
  250. content="<h1>Invalid verification code</h1>",
  251. status_code=400
  252. )
  253. return FileResponse(
  254. "public/verify.html",
  255. media_type="text/html",
  256. )
  257. recovery_pin_router = APIRouter()
  258. @recovery_pin_router.get("/")
  259. async def pin_forgot_get():
  260. """Render the PIN forgot page"""
  261. return FileResponse(
  262. "public/pin_forgot.html",
  263. media_type="text/html",
  264. )
  265. @recovery_pin_router.post("/")
  266. async def pin_forgot_post(request: UserMail):
  267. """Handle the PIN forgot form submission"""
  268. user = user_data_service.get_by_email(request.email)
  269. if not user:
  270. return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)})
  271. recovery_key = recovery_service.generate_recovery_key(user.id)
  272. sender = get_email_sender()
  273. sender.send_email(
  274. to=[user.email],
  275. subject=PIN_RECOVERY_MAIL["subject"],
  276. body=PIN_RECOVERY_MAIL["body"].format(app_name=APPNAME, verification_code=recovery_key,name=user.name)
  277. )
  278. # Send recovery_key to user's email
  279. return JSONResponse(status_code=200, content={"message": SuccessResponse.RECOVERY_EMAIL_SENT})
  280. @recovery_pin_router.post("/validate")
  281. async def pin_forgot_validate(request: PinRecoveryValidateRequest):
  282. """Validate the PIN recovery code"""
  283. user = user_data_service.get_by_email(request.email)
  284. if not user:
  285. return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)})
  286. recovery_data = recovery_service.get_recovery_data(user.id)
  287. logger.info(f"Recovery data for {request.email}: {recovery_data}|{request.code}")
  288. if recovery_data.code == -1:
  289. return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.email)})
  290. if recovery_data.code != request.code:
  291. return JSONResponse(status_code=400, content={"message": "Invalid recovery code"})
  292. token = uuid4().hex
  293. recovery_service.add_token(user.id, token)
  294. return JSONResponse(status_code=200, content={"message": "Recovery code validated successfully", "token": token})