users.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. import json
  2. from logging import getLogger
  3. from uuid import uuid4
  4. import redis
  5. from cryptography.fernet import Fernet
  6. from fastapi import APIRouter, Depends, Request
  7. from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
  8. from fastapi.exceptions import HTTPException
  9. from auth.security import generate_token
  10. from auth.security import get_current_user
  11. from config.mails import REGISTER_MAIL
  12. from config.messages import ErrorResponse, SuccessResponse, UserResponse
  13. from config.settings import APPNAME, PIN_KEY
  14. from models.user import LoginRequest, PinUserRequest, RegisterUserRequest, User, User, UserIDRequest, UserRewardRequest
  15. from services.data_service import UserDataService
  16. from services.email_service import get_email_sender
  17. from services.print_service import print_ticket
  18. from services.logging_service import structured_logger, LogLevel
  19. from utils.rut import validate_rut
  20. fernet = Fernet(PIN_KEY.encode())
  21. logger = getLogger(__name__)
  22. user_data_service = UserDataService()
  23. user_router = APIRouter()
  24. def unique_pin_generate():
  25. """Generate a unique 4-digit PIN"""
  26. import random
  27. pin = str(random.randint(1000, 9999))
  28. return pin
  29. @user_router.post("/exists")
  30. async def exists_user(request: UserIDRequest):
  31. """Check if user exists"""
  32. user = user_data_service.get_by_id(request.id)
  33. if user:
  34. return JSONResponse(status_code=200, content={"exists": True, "message": UserResponse.USER_EXISTS})
  35. else:
  36. return JSONResponse(status_code=404, content={"exists": False, "message": UserResponse.USER_DOES_NOT_EXIST})
  37. @user_router.post("/register")
  38. async def register_user(request: RegisterUserRequest):
  39. """Register a new user"""
  40. logger.info(f"Registration attempt for email: {request.email}")
  41. structured_logger.log_user_event(
  42. f"User registration attempt",
  43. LogLevel.INFO,
  44. {
  45. "email": request.email,
  46. "name": request.name,
  47. "rut": request.rut
  48. },
  49. user_email=request.email
  50. )
  51. # Validate RUT
  52. if not validate_rut(request.rut):
  53. logger.warning(f"Registration failed for {request.email}: invalid RUT {request.rut}")
  54. structured_logger.log_user_event(
  55. "Registration failed: invalid RUT",
  56. LogLevel.WARNING,
  57. {
  58. "email": request.email,
  59. "rut": request.rut,
  60. "reason": "Invalid RUT format"
  61. },
  62. user_email=request.email
  63. )
  64. raise HTTPException(status_code=400, detail=ErrorResponse.INVALID_RUT)
  65. # Check if user already exists by email
  66. try:
  67. user = user_data_service.get_by_email(request.email)
  68. if user:
  69. logger.warning(f"Registration failed for {request.email}: user already exists")
  70. structured_logger.log_user_event(
  71. "Registration failed: user already exists",
  72. LogLevel.WARNING,
  73. {
  74. "email": request.email,
  75. "existing_user_id": user.id,
  76. "reason": "Email already registered"
  77. },
  78. user_id=user.id,
  79. user_email=request.email
  80. )
  81. return HTTPException(status_code=400, detail=UserResponse.USER_ALREADY_EXISTS)
  82. # Check if RUT already exists
  83. user = user_data_service.get_by_rut(request.rut)
  84. if user:
  85. logger.warning(f"Registration failed for {request.email}: RUT already exists")
  86. structured_logger.log_user_event(
  87. "Registration failed: RUT already exists",
  88. LogLevel.WARNING,
  89. {
  90. "email": request.email,
  91. "rut": request.rut,
  92. "existing_user_id": user.id,
  93. "reason": "RUT already registered"
  94. },
  95. user_email=request.email
  96. )
  97. return HTTPException(status_code=400, detail=UserResponse.USER_ALREADY_EXISTS)
  98. except Exception as e:
  99. error_msg = f"Database error during user validation: {e}"
  100. logger.error(error_msg)
  101. structured_logger.log_database_event(
  102. "User validation error during registration",
  103. LogLevel.ERROR,
  104. {
  105. "email": request.email,
  106. "error": str(e),
  107. "error_type": type(e).__name__
  108. },
  109. user_email=request.email
  110. )
  111. raise HTTPException(status_code=500, detail={"message": "Error interno del servidor"})
  112. logger.info(f"Registering user: {request.email}")
  113. try:
  114. # Setup Redis client and verification code
  115. redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
  116. verification_code = str(uuid4())
  117. user_data = {
  118. "name": request.name,
  119. "email": request.email,
  120. "rut": request.rut
  121. }
  122. redis_client.set(f"verify:{verification_code}", json.dumps(user_data))
  123. redis_client.expire(f"verify:{verification_code}", 3600) # Expire in 1 hour
  124. structured_logger.log_user_event(
  125. "Verification code generated for user registration",
  126. LogLevel.INFO,
  127. {
  128. "email": request.email,
  129. "verification_code": verification_code,
  130. "expires_in": 3600
  131. },
  132. user_email=request.email
  133. )
  134. # Send verification email
  135. get_email_sender().send_email(
  136. REGISTER_MAIL["subject"],
  137. REGISTER_MAIL["body"],
  138. [request.email],
  139. name=request.name,
  140. app_name=APPNAME,
  141. verification_code=verification_code
  142. )
  143. structured_logger.log_email_event(
  144. "Registration verification email sent",
  145. LogLevel.INFO,
  146. {
  147. "email": request.email,
  148. "verification_code": verification_code
  149. },
  150. user_email=request.email
  151. )
  152. logger.info(f"Registration initiated successfully for {request.email}")
  153. return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS})
  154. except Exception as e:
  155. error_msg = f"Error during registration process for {request.email}: {e}"
  156. logger.error(error_msg)
  157. structured_logger.log_user_event(
  158. "Registration process failed",
  159. LogLevel.ERROR,
  160. {
  161. "email": request.email,
  162. "error": str(e),
  163. "error_type": type(e).__name__,
  164. "step": "verification_setup_or_email"
  165. },
  166. user_email=request.email
  167. )
  168. return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
  169. @user_router.post("/create-user")
  170. async def create_user(request: PinUserRequest, q: str):
  171. """Create a new user with PIN"""
  172. redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
  173. data = redis_client.get(f"verify:{q}")
  174. if not redis_client.get(f"verify:{q}"):
  175. return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_VERIFICATION_CODE})
  176. else:
  177. data = json.loads(str(data))
  178. name = data.get("name")
  179. email = data.get("email")
  180. rut = data.get("rut")
  181. pin = request.pin
  182. if not request.pin or len(request.pin) != 4:
  183. return JSONResponse(status_code=400, content={"message": ErrorResponse.INVALID_PIN})
  184. userID = user_data_service.create(name, email, rut, pin)
  185. if userID == -1:
  186. return JSONResponse(status_code=400, content={"message": UserResponse.USER_ALREADY_EXISTS})
  187. user = user_data_service.get_by_id(userID)
  188. if not user:
  189. return JSONResponse(status_code=500, content={"message": ErrorResponse.USER_CREATION_ERROR})
  190. return JSONResponse(status_code=201, content={"message": SuccessResponse.USER_CREATED_SUCCESS, "data": {
  191. **user.model_dump(exclude={"pin_hash"}),
  192. "token": generate_token(user.email)
  193. }})
  194. @user_router.post("/login")
  195. async def login_user(request: LoginRequest, http_request: Request):
  196. """Login user with email and PIN"""
  197. logger.info(f"Login attempt for email: {request.email}")
  198. structured_logger.log_security_event(
  199. f"Login attempt for user {request.email}",
  200. LogLevel.INFO,
  201. {
  202. "email": request.email,
  203. "user_agent": http_request.headers.get("user-agent", "unknown"),
  204. "referer": http_request.headers.get("referer", "unknown"),
  205. "client_ip": http_request.client.host if http_request.client else "unknown"
  206. },
  207. user_email=request.email
  208. )
  209. try:
  210. redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
  211. is_blocked = redis_client.get(f"blocked:{request.email}")
  212. if is_blocked:
  213. try:
  214. blocked_time_raw = redis_client.ttl(f"blocked:{request.email}")
  215. blocked_minutes = max(0, int(blocked_time_raw) // 60) if blocked_time_raw and int(blocked_time_raw) > 0 else 0 # type: ignore
  216. except (ValueError, TypeError):
  217. blocked_minutes = 0
  218. logger.warning(f"Login attempt for blocked user: {request.email}, blocked for {blocked_minutes} minutes")
  219. structured_logger.log_security_event(
  220. f"Login attempt by blocked user",
  221. LogLevel.WARNING,
  222. {
  223. "email": request.email,
  224. "blocked_time_remaining_minutes": blocked_minutes,
  225. "user_agent": http_request.headers.get("user-agent", "unknown")
  226. },
  227. user_email=request.email
  228. )
  229. return JSONResponse(
  230. status_code=403,
  231. content={"message": UserResponse.USER_FORMAT_BLOCKED.format(time=f"{blocked_minutes} minutos")}
  232. )
  233. # Attempt login
  234. user = user_data_service.login(request.email, request.pin)
  235. if user:
  236. # Successful login
  237. logger.info(f"Successful login for user: {request.email}")
  238. # Check admin access
  239. referer = http_request.headers.get("referer", "")
  240. if referer and "admin" in referer:
  241. user_permissions = user_data_service.permissions(user.id)
  242. if user_permissions == 0:
  243. logger.warning(f"Unauthorized admin access attempt by {request.email}")
  244. structured_logger.log_security_event(
  245. f"Unauthorized admin access attempt",
  246. LogLevel.WARNING,
  247. {
  248. "email": request.email,
  249. "user_id": user.id,
  250. "permissions": user_permissions,
  251. "referer": referer
  252. },
  253. user_id=user.id,
  254. user_email=request.email
  255. )
  256. return JSONResponse(status_code=403, content={"message": UserResponse.NOT_PERMITTED})
  257. # Clear login attempts and log successful login
  258. redis_client.delete(f"login_attempts:{request.email}")
  259. structured_logger.log_security_event(
  260. f"Successful login",
  261. LogLevel.INFO,
  262. {
  263. "email": request.email,
  264. "user_id": user.id,
  265. "user_name": user.name,
  266. "permissions": user_data_service.permissions(user.id),
  267. "is_admin_login": "admin" in referer,
  268. "reward_progress": user.reward_progress
  269. },
  270. user_id=user.id,
  271. user_email=request.email
  272. )
  273. return JSONResponse(status_code=200, content={
  274. "message": SuccessResponse.LOGIN_SUCCESS,
  275. "data": {
  276. "id": user.id,
  277. "name": user.name,
  278. "email": user.email,
  279. "kleincoins": user.kleincoins,
  280. "created_at": user.created_at,
  281. "token": generate_token(user.email),
  282. "reward_progress": user.reward_progress,
  283. }
  284. })
  285. else:
  286. # Failed login: increment attempts in Redis
  287. redis_client.incr(f"login_attempts:{request.email}")
  288. redis_client.expire(f"login_attempts:{request.email}", 3600)
  289. redis_attempts = redis_client.get(f"login_attempts:{request.email}")
  290. attempts = int(redis_attempts) if redis_attempts else 0 # type: ignore
  291. if attempts >= 5:
  292. # Too many attempts, block login
  293. redis_client.set(f"blocked:{request.email}", "true")
  294. redis_client.expire(f"blocked:{request.email}", 3600)
  295. logger.warning(f"Too many login attempts for {request.email}. User blocked.")
  296. structured_logger.log_security_event(
  297. f"User blocked due to too many failed login attempts",
  298. LogLevel.WARNING,
  299. {
  300. "email": request.email,
  301. "failed_attempts": attempts,
  302. "blocked_duration_seconds": 3600
  303. },
  304. user_email=request.email
  305. )
  306. return JSONResponse(status_code=429, content={"message": ErrorResponse.TOO_MANY_ATTEMPTS})
  307. else:
  308. logger.warning(f"Failed login attempt for {request.email}. Attempts: {attempts}")
  309. structured_logger.log_security_event(
  310. f"Failed login attempt",
  311. LogLevel.WARNING,
  312. {
  313. "email": request.email,
  314. "failed_attempts": attempts,
  315. "attempts_remaining": 5 - attempts,
  316. "reason": "Invalid credentials"
  317. },
  318. user_email=request.email
  319. )
  320. # Return unauthorized with attempts remaining
  321. return JSONResponse(status_code=401, content={
  322. "message": ErrorResponse.INVALID_CREDENTIALS,
  323. "attempts_remaining": 5 - attempts if attempts else 5
  324. })
  325. except redis.RedisError as e:
  326. error_msg = f"Redis error during login for {request.email}: {e}"
  327. logger.error(error_msg)
  328. structured_logger.log_system_event(
  329. "Redis error during login process",
  330. LogLevel.ERROR,
  331. {
  332. "email": request.email,
  333. "error": str(e),
  334. "error_type": "RedisError"
  335. }
  336. )
  337. return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
  338. except Exception as e:
  339. error_msg = f"Unexpected error during login for {request.email}: {e}"
  340. logger.error(error_msg)
  341. structured_logger.log_security_event(
  342. "Unexpected error during login",
  343. LogLevel.ERROR,
  344. {
  345. "email": request.email,
  346. "error": str(e),
  347. "error_type": type(e).__name__
  348. },
  349. user_email=request.email
  350. )
  351. return JSONResponse(status_code=500, content={"message": "Error interno del servidor"})
  352. @user_router.delete("/delete")
  353. async def delete_user(request: UserIDRequest):
  354. """Delete a user by ID"""
  355. user = user_data_service.delete(request.id)
  356. if user:
  357. return JSONResponse(status_code=200, content={"message": SuccessResponse.USER_DELETED_SUCCESS, "data": user})
  358. else:
  359. return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND})
  360. @user_router.post("/reward")
  361. async def reward_user(request: UserRewardRequest, user: User = Depends(get_current_user)):
  362. """Reward a user with 1 free beer"""
  363. if user.reward_progress < 100:
  364. return JSONResponse(status_code=400, content={"message": UserResponse.REWARD_INSUFFICIENT_PROGRESS.format(progress=user.reward_progress)})
  365. if not user:
  366. return JSONResponse(status_code=404, content={"message": UserResponse.USER_NOT_FOUND.format(user_id=request.id)})
  367. user_data_service.set_reward_progress(user.id, 0)
  368. print_ticket(request.tableNumber)
  369. return JSONResponse(status_code=200, content={"message": SuccessResponse.REWARD_SUCCESS, "data": {
  370. "id": user.id,
  371. "name": user.name,
  372. "email": user.email,
  373. "reward_progress": 0
  374. }})
  375. @user_router.get("/user")
  376. async def get_cur_user(current_user:User = Depends(get_current_user)):
  377. """Get current user information"""
  378. return JSONResponse(status_code=200, content={"data": current_user.model_dump(exclude={"pin_hash", "kleincoins", "rut"})})
  379. @user_router.get("/all")
  380. async def get_all_users():
  381. """Get all users"""
  382. users = list(map(lambda u: u.model_dump(), user_data_service.get_all()))
  383. return JSONResponse(status_code=200, content={"data": users})
  384. from fastapi import Query
  385. verify_router = APIRouter()
  386. @verify_router.get("/")
  387. async def verify_user(q: str = Query(..., description="q parameter")):
  388. """Verify a user by ID"""
  389. # get url params
  390. redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
  391. if not redis_client.get(f"verify:{q}"):
  392. return HTMLResponse(
  393. content="<h1>Invalid verification code</h1>",
  394. status_code=400
  395. )
  396. return FileResponse(
  397. "public/verify.html",
  398. media_type="text/html",
  399. )