【snake】Section: 12 Creating a Complete User System
requirements.txt に関して
psycopg2はpostgresqlのデータベースと接続する。
Flask-SQLAlchemはextentionを簡素化する。
Flask-Loginはログイン機能のextentionである。
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ファイルに記載する。
4行目でserviceをpostgresに書き換えている。
6行目でymlファイルも.envを参照するようにされている。
また、postgreでvolume、port番号も変更している。
43行目でvolumeをpostgresとredisとしている。
ちなみに前回は以下のようになっていた。
setting.pyでsignalsをアプリ内で使わないのでmodificationをfalseにしている。
SQLALCHEMY_TRACK_MODIFICATIONS = False
32行目でクッキーの設定を行う。90日間限定に設定する。 本当のパスワードやIDはinstance/setting.pyファイルに登録する。
Adding the User Blueprint
7行目でインポートする。
13行目と14行目でextentionをインポートする。
17行目でcelery_taskを追加する。
66行目でレジスターする。
83行目と84行目でdbとlogin_mangerをイニシャライズする。
9〜14行目でimportされているextensionsに関して
よく分からないけど、色々インポートされている。
Exploring the User Model (データベースに関して)
database schema(スキーマ)とは、データベースの構造を決める。
リレーショナルデータベースとは、新しくrowを設定したり、沢山のrowがある中で簡単に検索などをすることができる。
blueprints/user/modes.py を読み解く
use functionでは、ResourceMixinとdb.Modelを受け継ぐようにする。
ResourceMixin何かを見に行く。
アップデートした時間と作成日が定義されている。
また、created_on, updated_on, save, deleteなども定義されている。
from lib.util_datetime import tzware_datetime
31行目のtzware_datetimeは、util_datetime.pyで定義されている。
utc time stampで定義されている。
36行目はセーブインスタンス。
42行目と43行目に分けて安全にセーブを実行する。
daleteも同じである。
ここからデータベースの設定
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
実行すると
init, reset, seed の3つのコマンドがある。
click optionが設定されている。
30行目でデータベースcreatとdropが設定されている。
33行目でtest codeが書かれている。test.dbと本当のdbは分けてテストしている。
43行目ではseed codeが書かれている。
49行目で既にseedが存在しているかをテストしている。
58行目はseedのデータをsaveしている。
**paramの意味は、param辞書にキーワードを登録する。
models.py
44行目で全てのキーワードを格納する。
48行目で全てを暗号化している。
63行目で実装している。generate_password_hashファンクション はflaskの一部。
PBKDF2はかなりレベルの高い暗号化。
64行目は最後のファンクション でresetファンクション 。
実際にコードを実行する。
$ docker-compose exec website snakeeyes db reset --with-testdb
この部分が実行される。 以下のエラーがでる。 Background on this error at: http://sqlalche.me/e/e3q8
よくわからないので先に進む
Logging Users in and Out
ログインページではFooterがなくなる。
また、ログインすると右上がAccountに変わる。
htmlページの設定方法を紐解いてみる。
この章からはhtmlが3つ設定されている。
base.htmlを確認する。
以下の4つをinjectするように設定してある。
{% block header %}
{% block heading %}
{% block body %}
{% block footer %}
次にapp.htmlを確認する。
base.htmlに以下の2つをインジェクと出来るようになっている。
{% block header %}
{% block footer %}
次にlogin.htmlを確認する。
headerは画像のみを読み込む。
footerはSign up todayのみが記載されている。
これが実際にどうゆう仕組みかを紐解く。
app.pyを確認する。
authenticationファンクションの役割りは、
user idやトークンをデータベースで検索してロードすること。
つまり、ここでuser id を取得する。
load_token functionでトークンを作る。
理由は、クッキー内にemailとかpassをそのまま保存するのは危険だからである。
app.secret_keyにトークンで暗号化して保存する。
views.pyを確認する
views.py
flask_loginからlogin_requiredをインポートする。
@login_requiredとすることで、ログイン状態や無い場合はlogoutページに飛ばないようにする。
状態を確認して、大丈夫なら、flashメッセ付きでログアウトさせて、リダイレクトさせる。
views.pyを確認する。
@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()である。これはデコレーターである。
userフォルダーにdecolatorとしてある。
新しいのは@anonymous_required()である。
これはflask_loginにないから自分でdecoratorに作らなければならない。
すでにログイン済みかを18行目で確認してリダイレクトさせる。
次にLoginFormである。form.pyを確認する。
入力内容が有効かを確認する。
デフォルト値をnextにパスしている。
Nextを設定してない場合は、毎回settingページに飛ばされるが、
nextを設定すればログイン処理後行きたいページに飛べる。
次に3. if form.validate_on_submit():を紐解く。
models.pyでfind_by_identityで確認する。
場所はuserフォルダーである。
form.validateであるか確認して、find_by_identityを使う。
そして、find_by_identityでIDとPassが一致するかを確認する
次に4. if u and u.authenticatedである。
ここではパスワードを確認する。
models.pyのauthenticatedを確認する。
もし、passが違ったら、flash('Identity or password is incorrect.', 'error')を表示させる。
次に5. if login_userである。models.pyを確認する。
update_activity_trackingは簡単である。
182行目はカウントする。
ちなみに、remenber meが設定されていて、activeでない場合は、
アカウントが停止ということなのでflash('This account has been disabled.', 'error')を表示させる。
次にsafe_next_urlである。lib/safe_next_url.pyを確認する。
safe_next_url.pyは理由は、オープンリダイレクト攻撃の対策である。
このコードでダイレクト先を勝手に変更されないようにしている。
Registering New Users
signup.htmlはログインログアウトと同じパターンである。
views.pyを確認する。
最初に確認するのはSignupForm()である。
同じでforms.pyにファンクション が書かれている。
注目すべきはclass SignupForm(ModelForm):である。
wtfからModelFormを使っている。
ModelFormはlibフォルダーにある。
CSRF protectionを使うためにModelForm(Form)処理をしている。
ModelFormにFormを渡すことで暗号化できる。
また、Uniqueはwtfからインポートされている。
Unique( User.email, get_session=lambda: db.session )
これを使うことで、uniqueなものだけが通過できるようにする。
115行目でcreate new user
117行目でuにpassやidなどの値を入れる。
118行目で暗号化する。
121行目で問題なければ、flasメッセ付きで処理する。
Welcoming New Users
welcome.htmlはログインログアウトと同じパターンである。
form_tag とform_groupで入力内容を確認する。ただし、マクロよくわからん。
view.pyで仕組みを確認する。
129行目でログイン状態であるか確認する。
131行目は、既に同じuser名がある場合は、settingへ飛ばす。
131行目は、もし同じuserがない場合は、welcomeformへ飛ばす。
以下のように制限をかける。
^\w+$ は正規表現である。
DataRequired(), Length(1, 16), Regexp('^\w+$', message=username_message)
Allowing Users to Update Their Settings 設定変更方法
views.py(骨組み)→form.py(形式があっているか確認)→validation.py(内容の確認)
16行目でuser名があるかを確認して、ある場合は表示して、無い場合は設定へ飛ばす。
今回も同じである。
update_credentials.htmlでは、マクロで確認するようになっている?
156行目がform = UpdateCredentialsなのでform.pyを確認する。
同じように制限を設けている。
[DataRequired(), Length(8, 128), ensure_existing_password_matches]
今回は、ensure_existing_password_matchesを使っている。
from snakeeyes.blueprints.user.validations からインポートしている。
validations.pyを確認するると、
28行目でuserIDを確認して、
30行目でfield.dataへpasswordを渡してマッチするかを確認する。
また、form.pyの71行目はOptionalとなっている。
新しパスワードが記載された場合のみ、views.pyの162行目に進む。
そして、新しいパスワードが保存される仕組みである。
password = PasswordField('Password', [Optional(), Length(8, 128)])
最後にif current_user.is_authenticatedとなっている。
ログイン状態なら、ヘッダーは変わる。
Dealing with Password Resets
これも同じ流れ。views.py(骨組み) → forms.py(形式確認) →
あればlibフォルダーor validator.py(内容確認) → models.py
views.pyに関して
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に関して
validations.pyに関して
dbに繋げて、idあるか確認するだけ。
views.pyの77行目のinitialize_password_reset に関して
107行目でIDの確認をする。
108行目でトークンを暗号化する。
上記の108行目のserialize_ tokenは159行目以降で設定されている。
1時間のみ有効にしている。{'user_email': self.email}とすることでどのメルアドに何が登録されたかをわかるようにする。
task.py
19行目でuserを検索する。
24行目でコンテキストにuserとtokenを入れる。
send_template_messageへ送る。user_idを使う。
password_reset.txtでメールの本文を確定させる。
メールの本文内でもurl_forを使える。
_external=Trueは新しいタブを開かせる。
password_reset処理に関して
views.pyに関して
1. PasswordResetForm 2. Submit 3. deserialize_token 4. もしtokenなければ→massage 5. トークンが合致すればはpopulate_obj(u)して、暗号化して、save
PasswordResetFormで形式を確認→submit→deserialize_token
form.pyに関して
models.pyに関して
deserialize_tokenでtokenを渡す。
89行目で時間を確認する
92行目でトークンをdecodeする
94行目で登録のemailと同じトークンかを確認する。