フラスコ静止API -パート5


パート5 :パスワードリセット


危ない!シリーズの前で、我々はフラスコでエラーを取り扱う方法を学び、意味のあるエラーメッセージをクライアントに送りました.
この部分では、我々のアプリケーションでパスワードをリセット機能を実装する予定です.
ここでは、パスワードのリセットフローがどのように見えるかの簡単な図です.

パスワードリセットフロー図
我々は、使用するつもりですflask-jwt-extended パスワードをリセットトークンを生成するライブラリは、良いことは、我々はすでに認証を実装しながら、それをインストールしているです.メールを通してユーザにリセットトークンを送る必要がありますFlask Mail .
pipenv install flask-mail
このメールサーバを登録しましょうapp.py :
#~/movie-bag/app.py

from flask import Flask
 from flask_bcrypt import Bcrypt
 from flask_jwt_extended import JWTManager
+from flask_mail import Mail

 ...

 api = Api(app, errors=errors)
 bcrypt = Bcrypt(app)
 jwt = JWTManager(app)
+mail = Mail(app)

 app.config['MONGODB_SETTINGS'] = {
     'host': 'mongodb://localhost/movie-bag'
...
さて、クライアントにメールを送るサービスを作成しましょうservices と新しいファイルmail_service.py インサイド.新しく作成したファイルに次のコンテンツを追加します.
mkdir services
cd services
touch mail_service.py
#~/movie-bag/services/mail_service.py

from threading import Thread
from flask_mail import Message

from app import app
from app import mail


def send_async_email(app, msg):
    with app.app_context():
        try:
            mail.send(msg)
        except ConnectionRefusedError:
            raise InternalServerError("[MAIL SERVER] not working")


def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    Thread(target=send_async_email, args=(app, msg)).start()
ここでは、我々は機能を作成して見ることができますsend_mail() どのテイクsubject , sender , recipients , text_body and html_body 引数として.メッセージオブジェクトを生成し、実行するsend_async_email() 別のスレッドでは、クライアントにメールを送信しながら、GoogleやOutlookなどの別のサービスに中継する必要があります.
これらのサービスは、実際にメールを送信するのに時間がかかるので、私たちは、クライアントが要求を成功させ、別のスレッドでメールを送信するように指示するつもりです.
パスワードリセットを実装する準備が整いました.上の図に示すように、2つの異なるエンドポイントを作成します.
1)./forget : このエンドポイントはemail アカウントが変更される必要があるユーザの.この終点は、パスワードをリセットするためにリセットトークンを含むリンクでユーザーにメールを送ります.
2)./reset : このエンドポイントreset_token 電子メールと新しいpassword .
を作成しましょうreset_password.py インサイドresources フォルダ.次のコードを使用します.
#~/movie-bag/resources/reset_password.py

from flask import request, render_template
from flask_jwt_extended import create_access_token, decode_token
from database.models import User
from flask_restful import Resource
import datetime
from resources.errors import SchemaValidationError, InternalServerError, \
    EmailDoesnotExistsError, BadTokenError
from jwt.exceptions import ExpiredSignatureError, DecodeError, \
    InvalidTokenError
from services.mail_service import send_email

class ForgotPassword(Resource):
    def post(self):
        url = request.host_url + 'reset/'
        try:
            body = request.get_json()
            email = body.get('email')
            if not email:
                raise SchemaValidationError

            user = User.objects.get(email=email)
            if not user:
                raise EmailDoesnotExistsError

            expires = datetime.timedelta(hours=24)
            reset_token = create_access_token(str(user.id), expires_delta=expires)

            return send_email('[Movie-bag] Reset Your Password',
                              sender='[email protected]',
                              recipients=[user.email],
                              text_body=render_template('email/reset_password.txt',
                                                        url=url + reset_token),
                              html_body=render_template('email/reset_password.html',
                                                        url=url + reset_token))
        except SchemaValidationError:
            raise SchemaValidationError
        except EmailDoesnotExistsError:
            raise EmailDoesnotExistsError
        except Exception as e:
            raise InternalServerError


class ResetPassword(Resource):
    def post(self):
        url = request.host_url + 'reset/'
        try:
            body = request.get_json()
            reset_token = body.get('reset_token')
            password = body.get('password')

            if not reset_token or not password:
                raise SchemaValidationError

            user_id = decode_token(reset_token)['identity']

            user = User.objects.get(id=user_id)

            user.modify(password=password)
            user.hash_password()
            user.save()

            return send_email('[Movie-bag] Password reset successful',
                              sender='[email protected]',
                              recipients=[user.email],
                              text_body='Password reset was successful',
                              html_body='<p>Password reset was successful</p>')

        except SchemaValidationError:
            raise SchemaValidationError
        except ExpiredSignatureError:
            raise ExpiredTokenError
        except (DecodeError, InvalidTokenError):
            raise BadTokenError
        except Exception as e:
            raise InternalServerError
ここではForgotPassword リソースについては、まずemail クライアントによって提供されます.私たちはその後create_access_token() トークンを作成するuser.id そして、このトークンは24時間で期限が切れます.その後、クライアントにメールを送信しています.メールの両方が含まれますHTML とテキスト形式の情報.
同様にResetPassword ResournalトークンからユーザIDに基づいてユーザを取得し、ユーザが提供するパスワードに基づいてユーザのパスワードをリセットします.最後に、リセット成功メールがユーザーに送信されます.
新しい例外を作りましょうEmailDoesnotExistsError and BadTokenError 我々の中でerrors.py .
#~/movie-bag/resources/errors.py

 class UnauthorizedError(Exception):
     pass

+class EmailDoesnotExistsError(Exception):
+    pass
+
+class BadTokenError(Exception):
+    pass
+
 errors = {
     "InternalServerError": {
         "message": "Something went wrong",
@@ -54,5 +60,13 @@ errors = {
      "UnauthorizedError": {
          "message": "Invalid username or password",
          "status": 401
+     },
+     "EmailDoesnotExistsError": {
+         "message": "Couldn't find the user with given email address",
+         "status": 400
+     },
+     "BadTokenError": {
+         "message": "Invalid token",
+         "status": 403
      }
 }
我々は、クライアントに送信する必要があるHTMLやテキストファイルのテンプレートを作成する必要があります.つくりましょうtemplates ルートディレクトリ内のフォルダと内部templates フォルダを作るemail ここで2つの新しいファイルを作成しているreset_password.html and reset_password.txt .
mkdir templates
cd templates
mkdir email
cd email
touch reset_password.html
touch reset_password.txt
RetsetRageパスワードで.以下を追加します.
<!-- #~/movie-bag/templates/email/reset-password.html -->

<p>Dear, User</p>
<p>
    To reset your password
    <a href="{{ url }}">
        click here
    </a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely</p>
<p>Movie-bag Support Team</p>

ヒア{{ url }} 私たちが以前に送ったURLをrender_template() 関数.
同様に、次のreset_password.txt :
Dear, User

To reset your password click on the following link:

{{ url }}

If you have not requested a password reset simply ignore this message.

Sincerely

Movie-bag Support Team
今、我々はこれを配線する準備が整いましたResources 我々にroutes.py .
 from .movie import MoviesApi, MovieApi
 from .auth import SignupApi, LoginApi
+from .reset_password import ForgotPassword, ResetPassword

 def initialize_routes(api):

     ...

     api.add_resource(LoginApi, '/api/auth/login')
+
+    api.add_resource(ForgotPassword, '/api/auth/forgot')
+    api.add_resource(ResetPassword, '/api/auth/reset')

さて、アプリケーションを実行しようとするとpython app.py以下のようなエラーが表示されます.
ImportError: cannot import name 'initialize_routes' from 'resources.routes' (/home/paurakh/blog/flask/flask-restapi-series/movie-bag/resources/routes.py)
これはPythonの循環依存問題のためです.我々の中でreset_password.py , インポートするsend_mail インポート先app からapp.pyapp まだ定義されていないapp.py .

この問題を解決するためにrun.py 我々のルートディレクトリでは、我々のアプリを実行する責任があります.また、我々のアプリケーションを初期化した後、我々のルート/ビュー機能を初期化する必要があります.
touch run.py
さて、我々app.py 次のようになります.
#~/movie-bag/app.py

 from database.db import initialize_db
 from flask_restful import Api
-from resources.routes import initialize_routes
 from resources.errors import errors

 app = Flask(__name__)
 app.config.from_envvar('ENV_FILE_LOCATION')
+mail = Mail(app)
+
+# imports requiring app and mail
+from resources.routes import initialize_routes

 api = Api(app, errors=errors)
 bcrypt = Bcrypt(app)
 jwt = JWTManager(app)
-mail = Mail(app)

...

 initialize_db(app)
 initialize_routes(api)
-
-app.run()
我々の中でrun.py 我々は、アプリケーションを実行します
#~/movie-bag/run.py

from app import app

app.run()
設定の追加MAIL_SERVER イン.env

JWT_SECRET_KEY = 't1NP63m4wnBg6nyHYKfmc2TpCOGI4nss'
+MAIL_SERVER = "localhost"
+MAIL_PORT = "1025"
+MAIL_USERNAME = "[email protected]"
+MAIL_PASSWORD = ""
次の端末でSMTPサーバを起動します.
python -m smtpd -n -c DebuggingServer localhost:1025
これは、私たちの電子メール機能をテストするためのSMTPサーバーを作成します.
今すぐアプリを実行する
python run.py
注:エクスポートを覚えてENV_FILE_LOCATION
電子メールの場合は、既存のユーザーの場合はsmtp サーバとして:

<p>Dear, User</p>
<p>
    To reset your password
    <a href="http://localhost:3000/reset/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NzgzOTU0ODUsIm5iZiI6MTU3ODM5NTQ4NSwianRpIjoiZTEyZDg3ODgtMTkwZS00NWI1LWI0YzYtZTdkMTYzZjc5ZGZlIiwiZXhwIjoxNTc4NDgxODg1LCJpZGVudGl0eSI6IjVlMTQxNTJmOWRlNzQxZDNjNGYwYmNiYiIsImZyZXNoIjpmYWxzZSwidHlwZSI6ImFjY2VzcyJ9.dLJnhYTYMnLuLg_cHDdqi-jsXeISeMq75mb-ozaNxlw">
        click here
    </a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>http://localhost:3000/reset/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NzgzOTU0ODUsIm5iZiI6MTU3ODM5NTQ4NSwianRpIjoiZTEyZDg3ODgtMTkwZS00NWI1LWI0YzYtZTdkMTYzZjc5ZGZlIiwiZXhwIjoxNTc4NDgxODg1LCJpZGVudGl0eSI6IjVlMTQxNTJmOWRlNzQxZDNjNGYwYmNiYiIsImZyZXNoIjpmYWxzZSwidHlwZSI6ImFjY2VzcyJ9.dLJnhYTYMnLuLg_cHDdqi-jsXeISeMq75mb-ozaNxlw</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely</p>
<p>Movie-bag Support Team</p>
URLは次のようになります.http://localhost:3000/reset/<reset_token> , このトークンを手動でコピーする必要があります/reset エンドポイント.
注:私たちはどのように自動的に我々のフロントエンドシリーズでリセットするために実装する方法を学びますが、今のところ我々は手動でresetRankトークンをコピーする必要があります

おめでとう、パスワードが正常に変更されます.今、あなたは新しいパスワードでログインできます.
また、あなたのパスワードが正常にリセットされたことを示すメールを取得する必要があります.
<p>Password reset was successful</p>
今まで書いたすべてのコードを見つけることができますhere

我々がシリーズのこの部分から学んだこと?

  • ユーザーパスワードをリセットするためにトークンを作成する方法
  • ユーザーにメールを送る方法Flask-mail
  • ユーザーパスワードをリセットする方法
  • フラスコの循環依存性を避ける方法.
  • シリーズの次の部分で、我々は我々のフラスコ休みAPIをテストすることについて学ぶつもりです.
    それまで、ハッピーコーディング😊