users.py 16 KB

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