auth_service.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import uuid, bcrypt, random, string
  2. from sqlalchemy.orm import Session
  3. from datetime import datetime, timedelta
  4. from redis.asyncio import Redis
  5. from app.utils.redis_utils import redis_qpush
  6. from app.core.auth import get_current_user
  7. from app.core.biz_exception import NotFoundError, PermissionDeniedError, BizLogicError
  8. from app.models.user import VasUser
  9. from app.models.session import VasSession
  10. from app.models.email_verification import VasEmailVerification
  11. from app.schemas.auth import AutoRegisterRequest, SendBindCodeRequest, SendResetCodeRequest, BindEmailRequest, ResetPasswordRequest, LoginRequest
  12. from app.services.notification_service import NotificationService
  13. def _random_password(length=16):
  14. return ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%", k=length))
  15. class AuthService:
  16. # -----------------------
  17. # 自动注册
  18. # -----------------------
  19. @staticmethod
  20. def auto_register(db: Session, req:AutoRegisterRequest):
  21. uid = f'usr-{uuid.uuid4().hex[:8]}'
  22. user = VasUser(
  23. id=uid,
  24. role="user",
  25. nickname="anonymous visitor",
  26. preferred_language="en",
  27. timezone="Asia/Shanghai",
  28. register_ip=req.register_ip,
  29. )
  30. db.add(user)
  31. db.commit()
  32. # 创建 session
  33. token = f"tok_{uuid.uuid4().hex}"
  34. session = VasSession(
  35. id=token,
  36. user_id=uid,
  37. user_agent=req.user_agent or "",
  38. ip=req.register_ip,
  39. expire_at=datetime.utcnow() + timedelta(days=7)
  40. )
  41. db.add(session)
  42. db.commit()
  43. return {
  44. "user": user,
  45. "token": token
  46. }
  47. def send_bind_code(db: Session, payload: SendBindCodeRequest, auth_user: VasUser, redis_client:Redis):
  48. token = uuid.uuid4().hex[0:6]
  49. record = VasEmailVerification(
  50. user_id=auth_user.id,
  51. email=payload.email,
  52. token=token,
  53. expire_at=datetime.utcnow() + timedelta(minutes=30)
  54. )
  55. db.add(record)
  56. db.commit()
  57. print(f"📧 send verification email token={token}")
  58. NotificationService.create(
  59. redis_client=redis_client,
  60. ntype="email verification email",
  61. user_id=auth_user.id,
  62. channels=["email"],
  63. template_id="email_verification_for_bind",
  64. payload={
  65. "token": token
  66. }
  67. )
  68. def send_reset_code(db: Session, payload: SendResetCodeRequest, redis_client:Redis):
  69. user = db.query(VasUser).filter(
  70. VasUser.email == payload.email,
  71. VasUser.email_verified == 1
  72. ).first()
  73. if not user:
  74. raise BizLogicError("User not exist")
  75. token = uuid.uuid4().hex[0:6]
  76. record = VasEmailVerification(
  77. user_id=user.id,
  78. email=payload.email,
  79. token=token,
  80. expire_at=datetime.utcnow() + timedelta(minutes=30)
  81. )
  82. db.add(record)
  83. db.commit()
  84. print(f"📧 send verification email token={token}")
  85. NotificationService.create(
  86. redis_client=redis_client,
  87. ntype="email verification email",
  88. user_id=user.id,
  89. channels=["email"],
  90. template_id="email_verification_for_reset",
  91. payload={
  92. "token": token
  93. }
  94. )
  95. # -----------------------
  96. # 绑定邮箱
  97. # -----------------------
  98. @staticmethod
  99. def bind_email(db: Session, payload: BindEmailRequest, auth_user: VasUser, redis_client:Redis):
  100. user = db.query(VasUser).filter(
  101. VasUser.email == payload.email,
  102. VasUser.email_verified == 1
  103. ).first()
  104. if user:
  105. raise BizLogicError("Email has been bound")
  106. record = (
  107. db.query(VasEmailVerification)
  108. .filter_by(token=payload.code, used=0)
  109. .first()
  110. )
  111. if not record:
  112. raise BizLogicError("Token invalid")
  113. if record.expire_at < datetime.utcnow():
  114. raise BizLogicError("Token expired")
  115. # 更新 user.email
  116. user = db.query(VasUser).filter_by(id=record.user_id).first()
  117. user.email = payload.email
  118. # 随机密码
  119. plain = _random_password()
  120. hashed = bcrypt.hashpw(plain.encode(), bcrypt.gensalt()).decode()
  121. user.password_hash = hashed
  122. user.email_verified = 1
  123. record.used = 1
  124. # 创建 session
  125. session_id = "tok_" + uuid.uuid4().hex
  126. session = VasSession(
  127. id=session_id,
  128. user_id=user.id,
  129. user_agent="",
  130. ip="",
  131. expire_at=datetime.utcnow() + timedelta(days=30)
  132. )
  133. db.add(session)
  134. db.commit()
  135. db.refresh(user)
  136. print(f"📧 send login email and password")
  137. NotificationService.create(
  138. redis_client=redis_client,
  139. ntype="login credentials",
  140. user_id=user.id,
  141. channels=["email"],
  142. template_id="login_credentials",
  143. payload={
  144. "username": payload.email,
  145. "password": plain
  146. }
  147. )
  148. return {
  149. "user": user,
  150. "token": session_id
  151. }
  152. def reset_password(db: Session, payload: ResetPasswordRequest):
  153. user = db.query(VasUser).filter(
  154. VasUser.email == payload.email,
  155. VasUser.email_verified == 1
  156. ).first()
  157. if not user:
  158. raise BizLogicError("User not exist")
  159. record = (
  160. db.query(VasEmailVerification)
  161. .filter_by(token=payload.code, used=0)
  162. .first()
  163. )
  164. if not record:
  165. raise BizLogicError("Token invalid")
  166. if record.expire_at < datetime.utcnow():
  167. raise BizLogicError("Token expired")
  168. hashed = bcrypt.hashpw(payload.new_password.encode(), bcrypt.gensalt()).decode()
  169. user.password_hash = hashed
  170. record.used = 1
  171. db.commit()
  172. return True
  173. # -----------------------
  174. # 用户登录
  175. # -----------------------
  176. @staticmethod
  177. def login(db: Session, req:LoginRequest):
  178. user = db.query(VasUser).filter_by(email=req.email).first()
  179. if not user:
  180. raise NotFoundError("User not found")
  181. # 对比密码
  182. if not bcrypt.checkpw(req.password.encode(), user.password_hash.encode()):
  183. raise PermissionDeniedError("Password incorrect")
  184. # 创建 session
  185. token = "tok_" + uuid.uuid4().hex
  186. session = VasSession(
  187. id=token,
  188. user_id=user.id,
  189. user_agent="",
  190. ip="",
  191. expire_at=datetime.utcnow() + timedelta(days=7)
  192. )
  193. db.add(session)
  194. db.commit()
  195. return {
  196. "user": user,
  197. "token": token
  198. }