add reset password api
This commit is contained in:
parent
5341bb80a5
commit
65fa744ea5
|
@ -1,6 +1,8 @@
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from graphql import GraphQLResolveInfo
|
from graphql import GraphQLResolveInfo
|
||||||
from jwt import DecodeError, ExpiredSignatureError
|
from jwt import DecodeError, ExpiredSignatureError
|
||||||
from starlette.authentication import AuthenticationBackend
|
from starlette.authentication import AuthenticationBackend
|
||||||
|
@ -8,7 +10,7 @@ from starlette.requests import HTTPConnection
|
||||||
|
|
||||||
from auth.credentials import AuthCredentials, AuthUser
|
from auth.credentials import AuthCredentials, AuthUser
|
||||||
from auth.token import Token
|
from auth.token import Token
|
||||||
from auth.authorize import Authorize
|
from auth.authorize import Authorize, TokenStorage
|
||||||
from exceptions import InvalidToken, OperationNotAllowed
|
from exceptions import InvalidToken, OperationNotAllowed
|
||||||
from orm import User, UserStorage
|
from orm import User, UserStorage
|
||||||
from orm.base import local_session
|
from orm.base import local_session
|
||||||
|
@ -47,8 +49,7 @@ class _Authenticate:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def exists(cls, user_id, token):
|
async def exists(cls, user_id, token):
|
||||||
token = await redis.execute("GET", f"{user_id}-{token}")
|
return await TokenStorage.exist(f"{user_id}-{token}")
|
||||||
return token is not None
|
|
||||||
|
|
||||||
|
|
||||||
class JWTAuthenticate(AuthenticationBackend):
|
class JWTAuthenticate(AuthenticationBackend):
|
||||||
|
@ -104,6 +105,28 @@ class EmailAuthenticate:
|
||||||
auth_token = await Authorize.authorize(user)
|
auth_token = await Authorize.authorize(user)
|
||||||
return (auth_token, 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):
|
def login_required(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
||||||
|
|
|
@ -5,22 +5,26 @@ from redis import redis
|
||||||
from settings import JWT_LIFE_SPAN
|
from settings import JWT_LIFE_SPAN
|
||||||
from auth.validations import User
|
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:
|
class Authorize:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def authorize(user: User, device: str = "pc", life_span = JWT_LIFE_SPAN, auto_delete=True) -> str:
|
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)
|
exp = datetime.utcnow() + timedelta(seconds=life_span)
|
||||||
token = Token.encode(user, exp=exp, device=device)
|
token = Token.encode(user, exp=exp, device=device)
|
||||||
await redis.execute("SET", f"{user.id}-{token}", "True")
|
await TokenStorage.save(f"{user.id}-{token}", life_span, auto_delete)
|
||||||
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
|
return token
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -2,9 +2,9 @@ import requests
|
||||||
from starlette.responses import PlainTextResponse
|
from starlette.responses import PlainTextResponse
|
||||||
from starlette.exceptions import HTTPException
|
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_API_URL = "https://api.mailgun.net/v3/%s/messages" % (MAILGUN_DOMAIN)
|
||||||
MAILGUN_FROM = "postmaster <postmaster@%s>" % (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):
|
async def send_confirm_email(user):
|
||||||
text = "<html><body>To confirm registration follow the <a href='%s'>link</link></body></html>"
|
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):
|
async def send_auth_email(user):
|
||||||
text = "<html><body>To enter the site follow the <a href='%s'>link</link></body></html>"
|
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)
|
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)
|
to = "%s <%s>" % (user.username, user.email)
|
||||||
auth_url_with_token = "%s/%s" % (AUTH_URL, token)
|
url_with_token = "%s/%s" % (url, token)
|
||||||
text = text % (auth_url_with_token)
|
text = text % (url_with_token)
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
MAILGUN_API_URL,
|
MAILGUN_API_URL,
|
||||||
auth = ("api", MAILGUN_API_KEY),
|
auth = ("api", MAILGUN_API_KEY),
|
||||||
|
|
|
@ -3,15 +3,15 @@ from datetime import datetime, timedelta
|
||||||
from transliterate import translit
|
from transliterate import translit
|
||||||
from urllib.parse import quote_plus
|
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.authorize import Authorize
|
||||||
from auth.identity import Identity
|
from auth.identity import Identity
|
||||||
from auth.password import Password
|
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 import User, UserStorage, Role, UserRole
|
||||||
from orm.base import local_session
|
from orm.base import local_session
|
||||||
from resolvers.base import mutation, query
|
from resolvers.base import mutation, query
|
||||||
from exceptions import InvalidPassword
|
from exceptions import InvalidPassword, InvalidToken
|
||||||
|
|
||||||
from settings import JWT_AUTH_HEADER
|
from settings import JWT_AUTH_HEADER
|
||||||
|
|
||||||
|
@ -54,6 +54,32 @@ async def register(*_, email: str, password: str = ""):
|
||||||
token = await Authorize.authorize(user)
|
token = await Authorize.authorize(user)
|
||||||
return {"user": user, "token": token }
|
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")
|
@query.field("signIn")
|
||||||
async def login(_, info: GraphQLResolveInfo, email: str, password: str = ""):
|
async def login(_, info: GraphQLResolveInfo, email: str, password: str = ""):
|
||||||
|
|
|
@ -99,12 +99,9 @@ type Mutation {
|
||||||
|
|
||||||
# auth
|
# auth
|
||||||
confirmEmail(token: String!): AuthResult!
|
confirmEmail(token: String!): AuthResult!
|
||||||
requestPasswordReset(email: String!): Boolean!
|
|
||||||
confirmPasswordReset(token: String!): Boolean!
|
|
||||||
registerUser(email: String!, password: String): AuthResult!
|
registerUser(email: String!, password: String): AuthResult!
|
||||||
# updatePassword(password: String!, token: String!): Token!
|
requestPasswordUpdate(email: String!): Result!
|
||||||
# invalidateAllTokens: Boolean!
|
updatePassword(password: String!, token: String!): Result!
|
||||||
# invalidateTokenById(id: Int!): Boolean!
|
|
||||||
# requestEmailConfirmation: User!
|
# requestEmailConfirmation: User!
|
||||||
|
|
||||||
# shout
|
# shout
|
||||||
|
|
|
@ -5,6 +5,7 @@ PORT = 8080
|
||||||
|
|
||||||
BACKEND_URL = environ.get("BACKEND_URL") or "https://localhost:8080"
|
BACKEND_URL = environ.get("BACKEND_URL") or "https://localhost:8080"
|
||||||
OAUTH_CALLBACK_URL = environ.get("OAUTH_CALLBACK_URL") or "https://localhost:8080/authorized"
|
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"
|
DB_URL = environ.get("DATABASE_URL") or environ.get("DB_URL") or "sqlite:///db.sqlite3"
|
||||||
JWT_ALGORITHM = "HS256"
|
JWT_ALGORITHM = "HS256"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user