add reset password api

This commit is contained in:
knst-kotov 2022-01-13 15:16:35 +03:00
parent 5341bb80a5
commit 65fa744ea5
7 changed files with 116 additions and 60 deletions

View File

@ -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):

View File

@ -5,22 +5,26 @@ 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))
await TokenStorage.save(f"{user.id}-{token}", life_span, auto_delete)
return token
@staticmethod

View File

@ -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),

View File

@ -3,15 +3,15 @@ from datetime import datetime, timedelta
from transliterate import translit
from urllib.parse import quote_plus
from auth.authenticate import login_required
from auth.authenticate import login_required, ResetPassword
from auth.authorize import Authorize
from auth.identity import Identity
from auth.password import Password
from auth.email import send_confirm_email, send_auth_email
from auth.email import send_confirm_email, send_auth_email, send_reset_password_email
from orm import User, UserStorage, Role, UserRole
from orm.base import local_session
from resolvers.base import mutation, query
from exceptions import InvalidPassword
from exceptions import InvalidPassword, InvalidToken
from settings import JWT_AUTH_HEADER
@ -54,6 +54,32 @@ async def register(*_, email: str, password: str = ""):
token = await Authorize.authorize(user)
return {"user": user, "token": token }
@mutation.field("requestPasswordUpdate")
async def request_password_update(_, info, email):
with local_session() as session:
user = session.query(User).filter(User.email == email).first()
if not user:
return {"error" : "user not exist"}
await send_reset_password_email(user)
return {}
@mutation.field("updatePassword")
async def update_password(_, info, password, token):
try:
user_id = await ResetPassword.verify(token)
except InvalidToken as e:
return {"error" : e.message}
with local_session() as session:
user = session.query(User).filter_by(id = user_id).first()
if not user:
return {"error" : "user not exist"}
user.password = Password.encode(password)
session.commit()
return {}
@query.field("signIn")
async def login(_, info: GraphQLResolveInfo, email: str, password: str = ""):

View File

@ -99,12 +99,9 @@ type Mutation {
# auth
confirmEmail(token: String!): AuthResult!
requestPasswordReset(email: String!): Boolean!
confirmPasswordReset(token: String!): Boolean!
registerUser(email: String!, password: String): AuthResult!
# updatePassword(password: String!, token: String!): Token!
# invalidateAllTokens: Boolean!
# invalidateTokenById(id: Int!): Boolean!
requestPasswordUpdate(email: String!): Result!
updatePassword(password: String!, token: String!): Result!
# requestEmailConfirmation: User!
# shout

View File

@ -5,6 +5,7 @@ PORT = 8080
BACKEND_URL = environ.get("BACKEND_URL") or "https://localhost:8080"
OAUTH_CALLBACK_URL = environ.get("OAUTH_CALLBACK_URL") or "https://localhost:8080/authorized"
RESET_PWD_URL = environ.get("RESET_PWD_URL") or "https://localhost:8080/reset_pwd"
DB_URL = environ.get("DATABASE_URL") or environ.get("DB_URL") or "sqlite:///db.sqlite3"
JWT_ALGORITHM = "HS256"