add reset password api
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
from functools import wraps
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from jwt import DecodeError, ExpiredSignatureError
|
||||
from starlette.authentication import AuthenticationBackend
|
||||
@@ -8,7 +10,7 @@ from starlette.requests import HTTPConnection
|
||||
|
||||
from auth.credentials import AuthCredentials, AuthUser
|
||||
from auth.token import Token
|
||||
from auth.authorize import Authorize
|
||||
from auth.authorize import Authorize, TokenStorage
|
||||
from exceptions import InvalidToken, OperationNotAllowed
|
||||
from orm import User, UserStorage
|
||||
from orm.base import local_session
|
||||
@@ -47,8 +49,7 @@ class _Authenticate:
|
||||
|
||||
@classmethod
|
||||
async def exists(cls, user_id, token):
|
||||
token = await redis.execute("GET", f"{user_id}-{token}")
|
||||
return token is not None
|
||||
return await TokenStorage.exist(f"{user_id}-{token}")
|
||||
|
||||
|
||||
class JWTAuthenticate(AuthenticationBackend):
|
||||
@@ -104,6 +105,28 @@ class EmailAuthenticate:
|
||||
auth_token = await Authorize.authorize(user)
|
||||
return (auth_token, user)
|
||||
|
||||
class ResetPassword:
|
||||
@staticmethod
|
||||
async def get_reset_token(user):
|
||||
exp = datetime.utcnow() + timedelta(seconds=EMAIL_TOKEN_LIFE_SPAN)
|
||||
token = Token.encode(user, exp=exp, device="pc")
|
||||
await TokenStorage.save(f"{user.id}-reset-{token}", EMAIL_TOKEN_LIFE_SPAN, True)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
async def verify(token):
|
||||
try:
|
||||
payload = Token.decode(token)
|
||||
except ExpiredSignatureError:
|
||||
raise InvalidToken("Login expired, please login again")
|
||||
except DecodeError as e:
|
||||
raise InvalidToken("token format error") from e
|
||||
else:
|
||||
if not await TokenStorage.exist(f"{payload.user_id}-reset-{token}"):
|
||||
raise InvalidToken("Login expired, please login again")
|
||||
|
||||
return payload.user_id
|
||||
|
||||
def login_required(func):
|
||||
@wraps(func)
|
||||
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
||||
|
@@ -5,35 +5,39 @@ from redis import redis
|
||||
from settings import JWT_LIFE_SPAN
|
||||
from auth.validations import User
|
||||
|
||||
class TokenStorage:
|
||||
@staticmethod
|
||||
async def save(token_key, life_span, auto_delete=True):
|
||||
await redis.execute("SET", token_key, "True")
|
||||
if auto_delete:
|
||||
expire_at = (datetime.now() + timedelta(seconds=life_span)).timestamp()
|
||||
print(expire_at)
|
||||
await redis.execute("EXPIREAT", token_key, int(expire_at))
|
||||
|
||||
@staticmethod
|
||||
async def exist(token_key):
|
||||
return await redis.execute("GET", token_key)
|
||||
|
||||
|
||||
class Authorize:
|
||||
@staticmethod
|
||||
async def authorize(user: User, device: str = "pc", life_span = JWT_LIFE_SPAN, auto_delete=True) -> str:
|
||||
"""
|
||||
:param user:
|
||||
:param device:
|
||||
:param auto_delete: Whether the expiration is automatically deleted, the default is True
|
||||
:return:
|
||||
"""
|
||||
exp = datetime.utcnow() + timedelta(seconds=life_span)
|
||||
token = Token.encode(user, exp=exp, device=device)
|
||||
await redis.execute("SET", f"{user.id}-{token}", "True")
|
||||
if auto_delete:
|
||||
expire_at = (exp + timedelta(seconds=JWT_LIFE_SPAN)).timestamp()
|
||||
await redis.execute("EXPIREAT", f"{user.id}-{token}", int(expire_at))
|
||||
return token
|
||||
@staticmethod
|
||||
async def authorize(user: User, device: str = "pc", life_span = JWT_LIFE_SPAN, auto_delete=True) -> str:
|
||||
exp = datetime.utcnow() + timedelta(seconds=life_span)
|
||||
token = Token.encode(user, exp=exp, device=device)
|
||||
await TokenStorage.save(f"{user.id}-{token}", life_span, auto_delete)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
async def revoke(token: str) -> bool:
|
||||
try:
|
||||
payload = Token.decode(token)
|
||||
except: # noqa
|
||||
pass
|
||||
else:
|
||||
await redis.execute("DEL", f"{payload.user_id}-{token}")
|
||||
return True
|
||||
@staticmethod
|
||||
async def revoke(token: str) -> bool:
|
||||
try:
|
||||
payload = Token.decode(token)
|
||||
except: # noqa
|
||||
pass
|
||||
else:
|
||||
await redis.execute("DEL", f"{payload.user_id}-{token}")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def revoke_all(user: User):
|
||||
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
||||
await redis.execute("DEL", *tokens)
|
||||
@staticmethod
|
||||
async def revoke_all(user: User):
|
||||
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
||||
await redis.execute("DEL", *tokens)
|
||||
|
@@ -2,9 +2,9 @@ import requests
|
||||
from starlette.responses import PlainTextResponse
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from auth.authenticate import EmailAuthenticate
|
||||
from auth.authenticate import EmailAuthenticate, ResetPassword
|
||||
|
||||
from settings import BACKEND_URL, MAILGUN_API_KEY, MAILGUN_DOMAIN
|
||||
from settings import BACKEND_URL, MAILGUN_API_KEY, MAILGUN_DOMAIN, RESET_PWD_URL
|
||||
|
||||
MAILGUN_API_URL = "https://api.mailgun.net/v3/%s/messages" % (MAILGUN_DOMAIN)
|
||||
MAILGUN_FROM = "postmaster <postmaster@%s>" % (MAILGUN_DOMAIN)
|
||||
@@ -13,18 +13,23 @@ AUTH_URL = "%s/email_authorize" % (BACKEND_URL)
|
||||
|
||||
async def send_confirm_email(user):
|
||||
text = "<html><body>To confirm registration follow the <a href='%s'>link</link></body></html>"
|
||||
await send_email(user, text)
|
||||
token = await EmailAuthenticate.get_email_token(user)
|
||||
await send_email(user, AUTH_URL, text, token)
|
||||
|
||||
async def send_auth_email(user):
|
||||
text = "<html><body>To enter the site follow the <a href='%s'>link</link></body></html>"
|
||||
await send_email(user, text)
|
||||
|
||||
async def send_email(user, text):
|
||||
token = await EmailAuthenticate.get_email_token(user)
|
||||
await send_email(user, AUTH_URL, text, token)
|
||||
|
||||
async def send_reset_password_email(user):
|
||||
text = "<html><body>To reset password follow the <a href='%s'>link</link></body></html>"
|
||||
token = await ResetPassword.get_reset_token(user)
|
||||
await send_email(user, RESET_PWD_URL, text, token)
|
||||
|
||||
async def send_email(user, url, text, token):
|
||||
to = "%s <%s>" % (user.username, user.email)
|
||||
auth_url_with_token = "%s/%s" % (AUTH_URL, token)
|
||||
text = text % (auth_url_with_token)
|
||||
url_with_token = "%s/%s" % (url, token)
|
||||
text = text % (url_with_token)
|
||||
response = requests.post(
|
||||
MAILGUN_API_URL,
|
||||
auth = ("api", MAILGUN_API_KEY),
|
||||
|
@@ -7,17 +7,17 @@ from auth.validations import PayLoad, User
|
||||
|
||||
|
||||
class Token:
|
||||
@staticmethod
|
||||
def encode(user: User, exp: datetime, device: str = "pc") -> str:
|
||||
payload = {"user_id": user.id, "device": device, "exp": exp, "iat": datetime.utcnow()}
|
||||
return jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM)
|
||||
@staticmethod
|
||||
def encode(user: User, exp: datetime, device: str = "pc") -> str:
|
||||
payload = {"user_id": user.id, "device": device, "exp": exp, "iat": datetime.utcnow()}
|
||||
return jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM)
|
||||
|
||||
@staticmethod
|
||||
def decode(token: str, verify_exp: bool = True) -> PayLoad:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
key=JWT_SECRET_KEY,
|
||||
options={"verify_exp": verify_exp},
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
return PayLoad(**payload)
|
||||
@staticmethod
|
||||
def decode(token: str, verify_exp: bool = True) -> PayLoad:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
key=JWT_SECRET_KEY,
|
||||
options={"verify_exp": verify_exp},
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
return PayLoad(**payload)
|
||||
|
Reference in New Issue
Block a user