【snake】Section: 12 Creating a Complete User System

requirements.txt に関して

psycopg2はpostgresqlのデータベースと接続する。
Flask-SQLAlchemはextentionを簡素化する。
Flask-Loginはログイン機能のextentionである。 f:id:yukking3:20180714162050p:plain

Configuring the App to Handle Users

SQLAlchemyを使うには接続するための情報を設定しなければならない。
redisと同じようpostgreの設定を行う。

user: postgresql
pass: snakeeyes
host name: postgres
host port#: 5432
database name: snakeeyes

起動と同時にデータベースがdocker内に作られる。

db_uri = 'postgresql://snakeeyes:devpassword@postgres:5432/snakeeyes'

しかし、上記のパスワードやid を使うわけではない。
本当のパスワードは.envファイルに記載する。
f:id:yukking3:20180714163604p:plain

4行目でserviceをpostgresに書き換えている。
6行目でymlファイルも.envを参照するようにされている。
また、postgreでvolume、port番号も変更している。
43行目でvolumeをpostgresとredisとしている。

f:id:yukking3:20180714164107p:plain ちなみに前回は以下のようになっていた。 f:id:yukking3:20180714164153p:plain

setting.pyでsignalsをアプリ内で使わないのでmodificationをfalseにしている。

SQLALCHEMY_TRACK_MODIFICATIONS = False

f:id:yukking3:20180814015717p:plain 32行目でクッキーの設定を行う。90日間限定に設定する。 f:id:yukking3:20180814020200p:plain 本当のパスワードやIDはinstance/setting.pyファイルに登録する。

Adding the User Blueprint

7行目でインポートする。
13行目と14行目でextentionをインポートする。
17行目でcelery_taskを追加する。
66行目でレジスターする。
83行目と84行目でdbとlogin_mangerをイニシャライズする。
f:id:yukking3:20180714182556p:plain f:id:yukking3:20180714182618p:plain

9〜14行目でimportされているextensionsに関して
f:id:yukking3:20180814021009p:plain よく分からないけど、色々インポートされている。 f:id:yukking3:20180814021216p:plain


Exploring the User Model (データベースに関して)

database schema(スキーマ)とは、データベースの構造を決める。
リレーショナルデータベースとは、新しくrowを設定したり、沢山のrowがある中で簡単に検索などをすることができる。


blueprints/user/modes.py を読み解く f:id:yukking3:20180814023106p:plain use functionでは、ResourceMixinとdb.Modelを受け継ぐようにする。 f:id:yukking3:20180714185912p:plain

ResourceMixin何かを見に行く。
f:id:yukking3:20180814023316p:plain アップデートした時間と作成日が定義されている。
また、created_on, updated_on, save, deleteなども定義されている。

from lib.util_datetime import tzware_datetime

f:id:yukking3:20180714190525p:plain 31行目のtzware_datetimeは、util_datetime.pyで定義されている。
utc time stampで定義されている。

f:id:yukking3:20180814024454p:plain 36行目はセーブインスタンス
42行目と43行目に分けて安全にセーブを実行する。
daleteも同じである。


ここからデータベースの設定

f:id:yukking3:20180714194202p:plain
24行目でデータベースを作る。
28行目でrole コラムを定義する。roleとはadminと一般メンバーを分けるため。
roleは19行目ですでに定義されている。小文字はcode用で大文字は人間が読みやすくする為。server_default='1'と設定しておくことで、後々deactivateさせたりすることができる。
以下がよくわからない。enumって何?

role = db.Column(db.Enum(*ROLE, name='role_types', native_enum=False),
                     index=True, nullable=False, server_default='member')

32行目のunique=Trueは、同じデータが存在しないようにするため。



Initializing the Database

$ docker-compose exec website snakeeyes db

実行すると f:id:yukking3:20180714210152p:plain init, reset, seed の3つのコマンドがある。

click optionが設定されている。
f:id:yukking3:20180714210525p:plain 30行目でデータベースcreatとdropが設定されている。
33行目でtest codeが書かれている。test.dbと本当のdbは分けてテストしている。
43行目ではseed codeが書かれている。
49行目で既にseedが存在しているかをテストしている。
58行目はseedのデータをsaveしている。
**paramの意味は、param辞書にキーワードを登録する。

models.py
f:id:yukking3:20180814031425p:plain 44行目で全てのキーワードを格納する。
48行目で全てを暗号化している。
63行目で実装している。generate_password_hashファンクション はflaskの一部。
PBKDF2はかなりレベルの高い暗号化。
64行目は最後のファンクション でresetファンクション 。

実際にコードを実行する。

$ docker-compose exec website snakeeyes db reset --with-testdb

f:id:yukking3:20180814031804p:plain この部分が実行される。 以下のエラーがでる。 Background on this error at: http://sqlalche.me/e/e3q8

よくわからないので先に進む



Logging Users in and Out

ログインページではFooterがなくなる。
また、ログインすると右上がAccountに変わる。
f:id:yukking3:20180715094252p:plain
htmlページの設定方法を紐解いてみる。
この章からはhtmlが3つ設定されている。
f:id:yukking3:20180814202310p:plain base.htmlを確認する。 f:id:yukking3:20180814202557p:plain 以下の4つをinjectするように設定してある。
{% block header %}
{% block heading %}
{% block body %}
{% block footer %}

次にapp.htmlを確認する。 f:id:yukking3:20180814202750p:plain base.htmlに以下の2つをインジェクと出来るようになっている。
{% block header %}
{% block footer %}

次にlogin.htmlを確認する。 f:id:yukking3:20180814203103p:plain headerは画像のみを読み込む。
footerはSign up todayのみが記載されている。


これが実際にどうゆう仕組みかを紐解く。

app.pyを確認する。 f:id:yukking3:20180814205346p:plain authenticationファンクションの役割りは、
user idやトークンをデータベースで検索してロードすること。
つまり、ここでuser id を取得する。

load_token functionでトークンを作る。
理由は、クッキー内にemailとかpassをそのまま保存するのは危険だからである。
app.secret_keyにトークンで暗号化して保存する。


views.pyを確認する
f:id:yukking3:20180814210013p:plain views.py
f:id:yukking3:20180814210314p:plain flask_loginからlogin_requiredをインポートする。
@login_requiredとすることで、ログイン状態や無い場合はlogoutページに飛ばないようにする。
状態を確認して、大丈夫なら、flashメッセ付きでログアウトさせて、リダイレクトさせる。


views.pyを確認する。 f:id:yukking3:20180814210754p:plain @user.route('/login', methods=['GET', 'POST'])に関して

以下の順で確認作業が行われる。

1.  @anonymous_required()
2.  form = LoginForm(next=request.args.get('next'))
3.  if form.validate_on_submit():
4.  if u and u.authenticated
5.  if login_user(u, remember=True) and u.is_active():


最初に1. @anonymous_required()である。これはデコレーターである。 f:id:yukking3:20180814211314p:plain userフォルダーにdecolatorとしてある。 f:id:yukking3:20180814211512p:plain 新しいのは@anonymous_required()である。
これはflask_loginにないから自分でdecoratorに作らなければならない。
すでにログイン済みかを18行目で確認してリダイレクトさせる。

次にLoginFormである。form.pyを確認する。 f:id:yukking3:20180814211913p:plain 入力内容が有効かを確認する。
デフォルト値をnextにパスしている。
Nextを設定してない場合は、毎回settingページに飛ばされるが、
nextを設定すればログイン処理後行きたいページに飛べる。


次に3. if form.validate_on_submit():を紐解く。
models.pyでfind_by_identityで確認する。 f:id:yukking3:20180814212624p:plain 場所はuserフォルダーである。 f:id:yukking3:20180814212414p:plain form.validateであるか確認して、find_by_identityを使う。
そして、find_by_identityでIDとPassが一致するかを確認する


次に4. if u and u.authenticatedである。
f:id:yukking3:20180814214021p:plain ここではパスワードを確認する。
f:id:yukking3:20180814213204p:plain models.pyのauthenticatedを確認する。
もし、passが違ったら、flash('Identity or password is incorrect.', 'error')を表示させる。


次に5. if login_userである。models.pyを確認する。 f:id:yukking3:20180814214248p:plain update_activity_trackingは簡単である。
182行目はカウントする。

ちなみに、remenber meが設定されていて、activeでない場合は、
アカウントが停止ということなのでflash('This account has been disabled.', 'error')を表示させる。

次にsafe_next_urlである。lib/safe_next_url.pyを確認する。 f:id:yukking3:20180814214931p:plain safe_next_url.pyは理由は、オープンリダイレクト攻撃の対策である。
f:id:yukking3:20180814215029p:plain このコードでダイレクト先を勝手に変更されないようにしている。



Registering New Users

signup.htmlはログインログアウトと同じパターンである。
views.pyを確認する。
f:id:yukking3:20180715155330p:plain 最初に確認するのはSignupForm()である。
同じでforms.pyにファンクション が書かれている。
f:id:yukking3:20180715160101p:plain 注目すべきはclass SignupForm(ModelForm):である。
wtfからModelFormを使っている。
f:id:yukking3:20180715160431p:plain ModelFormはlibフォルダーにある。
CSRF protectionを使うためにModelForm(Form)処理をしている。
ModelFormにFormを渡すことで暗号化できる。

また、Uniqueはwtfからインポートされている。

Unique(
            User.email,
            get_session=lambda: db.session
        )

これを使うことで、uniqueなものだけが通過できるようにする。

f:id:yukking3:20180715155330p:plain 115行目でcreate new user
117行目でuにpassやidなどの値を入れる。
118行目で暗号化する。
121行目で問題なければ、flasメッセ付きで処理する。



Welcoming New Users

f:id:yukking3:20180815212020p:plain welcome.htmlはログインログアウトと同じパターンである。
f:id:yukking3:20180815213852p:plain form_tag とform_groupで入力内容を確認する。ただし、マクロよくわからん。 f:id:yukking3:20180815213945p:plain view.pyで仕組みを確認する。 f:id:yukking3:20180715161603p:plain 129行目でログイン状態であるか確認する。
131行目は、既に同じuser名がある場合は、settingへ飛ばす。
131行目は、もし同じuserがない場合は、welcomeformへ飛ばす。
f:id:yukking3:20180715162037p:plain
以下のように制限をかける。
^\w+$ は正規表現である。

 DataRequired(),
        Length(1, 16),
        Regexp('^\w+$', message=username_message)



Allowing Users to Update Their Settings 設定変更方法

views.py(骨組み)→form.py(形式があっているか確認)→validation.py(内容の確認) f:id:yukking3:20180815214730p:plain 16行目でuser名があるかを確認して、ある場合は表示して、無い場合は設定へ飛ばす。 f:id:yukking3:20180815215324p:plain 今回も同じである。 f:id:yukking3:20180815215518p:plain update_credentials.htmlでは、マクロで確認するようになっている? f:id:yukking3:20180715163317p:plain
156行目がform = UpdateCredentialsなのでform.pyを確認する。 f:id:yukking3:20180715163546p:plain 同じように制限を設けている。

[DataRequired(),
           Length(8, 128),
           ensure_existing_password_matches]

今回は、ensure_existing_password_matchesを使っている。
from snakeeyes.blueprints.user.validations からインポートしている。
f:id:yukking3:20180715163956p:plain validations.pyを確認するると、
28行目でuserIDを確認して、
30行目でfield.dataへpasswordを渡してマッチするかを確認する。
f:id:yukking3:20180715163546p:plain また、form.pyの71行目はOptionalとなっている。
f:id:yukking3:20180715163317p:plain 新しパスワードが記載された場合のみ、views.pyの162行目に進む。
そして、新しいパスワードが保存される仕組みである。

password = PasswordField('Password', [Optional(), Length(8, 128)])

最後にif current_user.is_authenticatedとなっている。
ログイン状態なら、ヘッダーは変わる。
f:id:yukking3:20180715165224p:plain


Dealing with Password Resets

これも同じ流れ。views.py(骨組み) → forms.py(形式確認) →
あればlibフォルダーor validator.py(内容確認) → models.py

views.pyに関して f:id:yukking3:20180715170913p:plain

1.   BeginPasswordResetForm()
       -ensure_identity_exists
2.  initialize_password_reset
       -serialize_token
       -initialize_password_reset
            -deliver_password_reset_email
                   -password_reset.txt
3.  populate_obj


forms.pyに関して f:id:yukking3:20180715170950p:plain validations.pyに関して f:id:yukking3:20180715171114p:plain dbに繋げて、idあるか確認するだけ。


views.pyの77行目のinitialize_password_reset に関して
f:id:yukking3:20180715171252p:plain 107行目でIDの確認をする。
108行目でトークンを暗号化する。
上記の108行目のserialize_ tokenは159行目以降で設定されている。
f:id:yukking3:20180715172209p:plain 1時間のみ有効にしている。{'user_email': self.email}とすることでどのメルアドに何が登録されたかをわかるようにする。

task.py
f:id:yukking3:20180715182136p:plain 19行目でuserを検索する。
24行目でコンテキストにuserとtokenを入れる。
send_template_messageへ送る。user_idを使う。


f:id:yukking3:20180715182226p:plain password_reset.txtでメールの本文を確定させる。
メールの本文内でもurl_forを使える。
_external=Trueは新しいタブを開かせる。



password_reset処理に関して

views.pyに関して f:id:yukking3:20180715182542p:plain

1.  PasswordResetForm
2.  Submit
3.  deserialize_token
4.  もしtokenなければ→massage
5.  トークンが合致すればはpopulate_obj(u)して、暗号化して、save

PasswordResetFormで形式を確認→submit→deserialize_token form.pyに関して f:id:yukking3:20180815235950p:plain
models.pyに関して
f:id:yukking3:20180715182913p:plain deserialize_tokenでtokenを渡す。
89行目で時間を確認する
92行目でトークンをdecodeする
94行目で登録のemailと同じトークンかを確認する。