auth.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. from fastapi import APIRouter, Request, Depends, HTTPException, WebSocket, WebSocketDisconnect, Query, Body
  2. from typing import Optional, List
  3. from services.global_manager import global_manager
  4. from services.rate_limit_service import rate_limit_service
  5. import auth_utils
  6. import db
  7. import schemas
  8. import session_utils
  9. import uuid
  10. from datetime import datetime, timedelta
  11. import locales
  12. from dependencies import get_current_user, require_admin
  13. import config
  14. import secrets
  15. from services.email_service import send_verification_email, send_password_reset_email
  16. from services.token_service import token_service
  17. from services.audit_service import audit_service
  18. try:
  19. from google.oauth2 import id_token
  20. from google.auth.transport import requests as google_requests
  21. except ImportError:
  22. id_token = None
  23. google_requests = None
  24. router = APIRouter(prefix="/auth", tags=["auth"])
  25. @router.get("/setup-debug-admin-9988")
  26. async def setup_debug_admin():
  27. # Attempt to create or just promote if exists
  28. pwd = auth_utils.get_password_hash("agent_debug_2026")
  29. existing = db.execute_query("SELECT id FROM users WHERE email = %s", ("antigravity_test@radionica3d.me",))
  30. if existing:
  31. db.execute_commit("UPDATE users SET role = 'admin', is_active = 1, password_hash = %s WHERE id = %s", (pwd, existing[0]['id']))
  32. else:
  33. db.execute_commit("INSERT INTO users (email, password_hash, role, is_active, first_name, last_name) VALUES (%s, %s, %s, %s, %s, %s)",
  34. ("antigravity_test@radionica3d.me", pwd, "admin", 1, "Antigravity", "Debug"))
  35. return {"status": "ok", "message": "User antigravity_test@radionica3d.me is now an active admin"}
  36. @router.post("/register", response_model=schemas.UserResponse)
  37. async def register(request: Request, user: schemas.UserCreate, lang: str = "en"):
  38. existing_user = db.execute_query("SELECT id FROM users WHERE email = %s", (user.email,))
  39. if existing_user:
  40. raise HTTPException(status_code=400, detail=locales.translate_error("email_already_registered", lang))
  41. ip_address = request.client.host if request.client else None
  42. hashed_password = auth_utils.get_password_hash(user.password)
  43. query = """
  44. INSERT INTO users (email, password_hash, first_name, last_name, phone, shipping_address, preferred_language, role, ip_address, is_company, company_name, company_pib, company_address, is_active)
  45. VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 0)
  46. """
  47. params = (user.email, hashed_password, user.first_name, user.last_name, user.phone, user.shipping_address, user.preferred_language, 'user', ip_address, user.is_company, user.company_name, user.company_pib, user.company_address)
  48. user_id = db.execute_commit(query, params)
  49. # Generate Verification Token (Redis)
  50. token = token_service.create_verification_token(user_id)
  51. # Send Email
  52. send_verification_email(user.email, token, user.preferred_language or lang)
  53. new_user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (user_id,))
  54. await audit_service.log(
  55. user_id=user_id,
  56. action="user_register",
  57. target_type="user",
  58. target_id=user_id,
  59. details={"email": user.email, "ip": ip_address},
  60. request=request
  61. )
  62. return new_user[0]
  63. @router.get("/verify-email")
  64. async def verify_email(token: str, lang: str = "en"):
  65. user_id = token_service.verify_email_token(token)
  66. if not user_id:
  67. raise HTTPException(status_code=400, detail="Invalid or expired verification token")
  68. db.execute_commit("UPDATE users SET is_active = 1 WHERE id = %s", (user_id,))
  69. token_service.delete_verification_token(token)
  70. return {"message": "Email verified successfully. You can now log in."}
  71. @router.post("/login", response_model=schemas.Token)
  72. async def login(request: Request, user_data: schemas.UserLogin, lang: str = "en"):
  73. ip = request.client.host if request.client else "unknown"
  74. email = user_data.email.lower()
  75. # 1. Check Global Rate Limit
  76. if rate_limit_service.is_rate_limited(email, ip):
  77. raise HTTPException(
  78. status_code=429,
  79. detail=locales.translate_error("too_many_attempts", lang)
  80. )
  81. # 2. Check if Captcha is Required
  82. if rate_limit_service.is_captcha_required(email, ip):
  83. if not user_data.captcha_token:
  84. raise HTTPException(
  85. status_code=403,
  86. detail=locales.translate_error("captcha_required", lang)
  87. )
  88. # 3. Verify Captcha
  89. if not await rate_limit_service.verify_captcha(user_data.captcha_token):
  90. raise HTTPException(
  91. status_code=403,
  92. detail=locales.translate_error("invalid_token", lang)
  93. )
  94. # 4. Attempt Authentication
  95. user = db.execute_query("SELECT * FROM users WHERE email = %s", (email,))
  96. if not user or not auth_utils.verify_password(user_data.password, user[0]['password_hash']):
  97. # Log failure
  98. rate_limit_service.record_failed_attempt(email, ip)
  99. raise HTTPException(status_code=401, detail=locales.translate_error("incorrect_credentials", lang))
  100. if not user[0].get('is_active', True):
  101. raise HTTPException(status_code=403, detail=locales.translate_error("account_not_active", lang))
  102. # 5. Success - Reset Rate Limits
  103. rate_limit_service.reset_attempts(email, ip)
  104. access_token = auth_utils.create_access_token(
  105. data={"sub": user[0]['email'], "id": user[0]['id'], "role": user[0]['role']}
  106. )
  107. await audit_service.log(
  108. user_id=user[0]['id'],
  109. action="user_login",
  110. target_type="user",
  111. target_id=user[0]['id'],
  112. details={"ip": ip, "method": "credentials"},
  113. request=request
  114. )
  115. return {"access_token": access_token, "token_type": "bearer"}
  116. @router.post("/social-login", response_model=schemas.Token)
  117. async def social_login(request: Request, data: schemas.SocialLogin):
  118. email = data.email.lower() if data.email else None
  119. first_name = data.first_name
  120. last_name = data.last_name
  121. # 1. Verify token if provider is Google
  122. if data.provider == 'google':
  123. print(f"DEBUG: Social Login attempt. id_token library available: {id_token is not None}")
  124. print(f"DEBUG: config.GOOGLE_CLIENT_ID exists: {bool(config.GOOGLE_CLIENT_ID)}")
  125. print(f"DEBUG: config.GOOGLE_CLIENT_ID value: {config.GOOGLE_CLIENT_ID}")
  126. if not id_token or not config.GOOGLE_CLIENT_ID:
  127. msg = f"Config error: id_token_lib={id_token is not None}, client_id_set={bool(config.GOOGLE_CLIENT_ID)}"
  128. raise HTTPException(status_code=500, detail=f"Google Auth not configured on server ({msg})")
  129. try:
  130. # Verify the ID token
  131. idinfo = id_token.verify_oauth2_token(data.token, google_requests.Request(), config.GOOGLE_CLIENT_ID)
  132. # ID token is valid. Get user's Google info
  133. if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
  134. raise ValueError('Wrong issuer.')
  135. email = idinfo['email'].lower()
  136. first_name = idinfo.get('given_name', first_name)
  137. last_name = idinfo.get('family_name', last_name)
  138. except Exception as e:
  139. print(f"Google Token Verification Error: {e}")
  140. raise HTTPException(status_code=401, detail="Invalid Google token")
  141. if not email:
  142. raise HTTPException(status_code=400, detail="Email is required")
  143. # 2. Proceed with login/registration
  144. user = db.execute_query("SELECT id, email, role, is_active FROM users WHERE email = %s", (email,))
  145. if user:
  146. if not user[0].get('is_active', True):
  147. raise HTTPException(status_code=403, detail="Your account has been suspended.")
  148. access_token = auth_utils.create_access_token(
  149. data={"sub": user[0]['email'], "id": user[0]['id'], "role": user[0]['role']}
  150. )
  151. return {"access_token": access_token, "token_type": "bearer"}
  152. else:
  153. ip_address = request.client.host if request.client else None
  154. hashed_password = auth_utils.get_password_hash(str(uuid.uuid4()))
  155. query = """
  156. INSERT INTO users (email, password_hash, first_name, last_name, preferred_language, role, ip_address, is_active)
  157. VALUES (%s, %s, %s, %s, %s, %s, %s, 1)
  158. """
  159. params = (email, hashed_password, first_name, last_name, data.preferred_language, 'user', ip_address)
  160. user_id = db.execute_commit(query, params)
  161. access_token = auth_utils.create_access_token(data={"sub": email, "id": user_id, "role": 'user'})
  162. return {"access_token": access_token, "token_type": "bearer"}
  163. @router.post("/logout")
  164. async def logout(user: dict = Depends(get_current_user)):
  165. sid = user.get("sid")
  166. if sid: session_utils.delete_session(sid)
  167. return {"message": "Successfully logged out"}
  168. @router.post("/forgot-password")
  169. async def forgot_password(request: schemas.ForgotPassword, lang: str = "en"):
  170. user = db.execute_query("SELECT id, preferred_language FROM users WHERE email = %s", (request.email,))
  171. if not user: raise HTTPException(status_code=404, detail="Email not found")
  172. # Generate Reset Token (Redis - 10 min TTL)
  173. token = token_service.create_reset_token(user[0]['id'])
  174. # Send Email
  175. user_lang = user[0]['preferred_language'] or lang
  176. send_password_reset_email(request.email, token, user_lang)
  177. return {"message": "Reset instructions sent to your email"}
  178. @router.post("/verify-reset-token")
  179. async def verify_reset_token_post(data: schemas.TokenVerify, lang: str = "en"):
  180. return await _verify_token_internal(data.token, lang)
  181. @router.get("/verify-reset-token")
  182. async def verify_reset_token_get(token: str, lang: str = "en"):
  183. return await _verify_token_internal(token, lang)
  184. async def _verify_token_internal(token: str, lang: str):
  185. if not token:
  186. raise HTTPException(status_code=400, detail="Token required")
  187. user_id = token_service.verify_reset_token(token)
  188. if not user_id:
  189. msg = "Invalid or expired reset token"
  190. if lang == "ru": msg = "Ссылка истекла или недействительна"
  191. elif lang == "me": msg = "Link je istekao ili je nevažeći"
  192. elif lang == "ua": msg = "Посилання закінчилося або є недійсним"
  193. raise HTTPException(status_code=400, detail=msg)
  194. return {"message": "Token is valid"}
  195. @router.post("/reset-password")
  196. async def reset_password(request: schemas.ResetPassword):
  197. user_id = token_service.verify_reset_token(request.token)
  198. if not user_id:
  199. raise HTTPException(status_code=400, detail="Invalid or expired reset token")
  200. hashed_password = auth_utils.get_password_hash(request.new_password)
  201. db.execute_commit("UPDATE users SET password_hash = %s WHERE id = %s", (hashed_password, user_id))
  202. # Successful reset - Cleanup ALL reset tokens AND sessions for this user
  203. token_service.cleanup_reset_tokens(user_id)
  204. session_utils.delete_all_user_sessions(user_id)
  205. await audit_service.log(
  206. user_id=user_id,
  207. action="password_reset_success",
  208. target_type="user",
  209. target_id=user_id
  210. )
  211. return {"message": "Password updated successfully"}
  212. @router.get("/me", response_model=schemas.UserResponse)
  213. async def get_me(user: dict = Depends(get_current_user)):
  214. user_id = user.get("id")
  215. user_data = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (user_id,))
  216. if not user_data: raise HTTPException(status_code=404, detail="User not found")
  217. return user_data[0]
  218. @router.put("/me", response_model=schemas.UserResponse)
  219. async def update_me(data: schemas.UserUpdate, user: dict = Depends(get_current_user)):
  220. user_id = user.get("id")
  221. update_fields = []
  222. params = []
  223. for field, value in data.dict(exclude_unset=True).items():
  224. update_fields.append(f"{field} = %s")
  225. params.append(value)
  226. if update_fields:
  227. query = f"UPDATE users SET {', '.join(update_fields)} WHERE id = %s"
  228. params.append(user_id)
  229. db.execute_commit(query, tuple(params))
  230. user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (user_id,))
  231. return user[0]
  232. @router.get("/admin/users")
  233. async def admin_get_users(page: int = 1, size: int = 50, search: Optional[str] = None, admin: dict = Depends(require_admin)):
  234. offset = (page - 1) * size
  235. base_query = "SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users"
  236. count_query = "SELECT COUNT(*) as total FROM users"
  237. params = []
  238. if search and search.strip():
  239. where_clause = " WHERE email LIKE %s OR first_name LIKE %s OR last_name LIKE %s OR phone LIKE %s"
  240. base_query += where_clause
  241. count_query += where_clause
  242. pattern = f"%{search.strip()}%"
  243. params = [pattern] * 4
  244. base_query += " ORDER BY id DESC LIMIT %s OFFSET %s"
  245. users = db.execute_query(base_query, tuple(params + [size, offset]))
  246. total = db.execute_query(count_query, tuple(params))[0]['total']
  247. return {"users": users, "total": total, "page": page, "size": size}
  248. @router.post("/admin/users", response_model=schemas.UserResponse)
  249. async def admin_create_user(request: Request, data: schemas.UserCreate, admin: dict = Depends(require_admin)):
  250. existing_user = db.execute_query("SELECT id FROM users WHERE email = %s", (data.email,))
  251. if existing_user:
  252. raise HTTPException(status_code=400, detail="Email already registered")
  253. hashed_password = auth_utils.get_password_hash(data.password)
  254. user_id = db.execute_commit(
  255. "INSERT INTO users (email, password_hash, first_name, last_name, phone, role, can_chat) VALUES (%s, %s, %s, %s, %s, %s, %s)",
  256. (data.email, hashed_password, data.first_name, data.last_name, data.phone, 'user', True)
  257. )
  258. await audit_service.log(
  259. user_id=admin.get("id"),
  260. action="admin_create_user",
  261. target_type="user",
  262. target_id=user_id,
  263. details={"email": data.email, "role": "user"},
  264. request=request
  265. )
  266. user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (user_id,))
  267. return user[0]
  268. @router.patch("/users/{target_id}/admin", response_model=schemas.UserResponse)
  269. async def admin_update_user(request: Request, target_id: int, data: schemas.AdminUserUpdate, admin: dict = Depends(require_admin)):
  270. update_fields = []
  271. params = []
  272. update_dict = data.dict(exclude_unset=True)
  273. # Handle password hashing
  274. if "password" in update_dict:
  275. password = update_dict.pop("password")
  276. update_dict["password_hash"] = auth_utils.get_password_hash(password)
  277. for field, value in update_dict.items():
  278. update_fields.append(f"`{field}` = %s")
  279. params.append(value)
  280. if update_fields:
  281. query = f"UPDATE users SET {', '.join(update_fields)} WHERE id = %s"
  282. params.append(target_id)
  283. db.execute_commit(query, tuple(params))
  284. await audit_service.log(
  285. user_id=admin.get("id"),
  286. action="admin_update_user",
  287. target_type="user",
  288. target_id=target_id,
  289. details={"updated_fields": {k: ('***' if k == 'password_hash' else v) for k, v in update_dict.items()}},
  290. request=request
  291. )
  292. # If user was deactivated, kick from active sessions
  293. if update_dict.get("is_active") is False:
  294. await global_manager.kick_user(target_id)
  295. user = db.execute_query("SELECT id, email, first_name, last_name, phone, shipping_address, preferred_language, role, can_chat, is_active, is_company, company_name, company_pib, company_address, ip_address, created_at FROM users WHERE id = %s", (target_id,))
  296. if not user: raise HTTPException(status_code=404, detail="User not found")
  297. return user[0]
  298. # WebSocket implementation moved to main.py to handle path prefixing issues
  299. # @router.websocket("/ws/global")
  300. # async def ws_global(websocket: WebSocket, token: str = Query(...)):
  301. # ...