add reset password api
This commit is contained in:
parent
5341bb80a5
commit
65fa744ea5
|
@ -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,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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 = ""):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue
Block a user