會員系統
在這一章中,我們將來試著做個簡單的會員系統,包括會員註冊、登入、登出等功能。不像 Django 這種比較功能完整、包山包海的框架,Flask 本身並沒有提供這麼多的功能,所以我們可能得自己來實作這些功能。
我會繼續用上一章的部落格系統的範例接著做,除了比較省事之外,後半段也要做到會員跟文章之間的關聯,讓每個會員只能編輯或刪除自己的文章等功能。
在上個章節我都是把程式碼寫在 app.py
檔案裡,隨著程式越寫越多,接下來的會員系統如果再加上後續還有其他追加其他功能的話,再繼續寫在同一個檔案的話,程式碼會變得又臭又長,不容易維護。所以在開始寫會員系統前,我想先稍微做點整理,把程式碼分成不同的目錄或檔案,讓專案看起來更有組織性。剛好,在 Flask 裡有個叫做 Blueprint 的設計可以讓我們做到這件事。
使用 Blueprint
Flask 的「藍圖(Blueprint)」可以讓我們把程式碼寫在不同的檔案裡,然後再把這些檔案「註冊」到 Flask 應用程式裡,這樣就可以不用把程式碼全部寫在同一個檔案裡了,如果只是一個小型 的專案,可能不會覺得有什麼不方便,但如果專案越來越大,學著怎麼整理程式碼就越來越重要。我相信各位讀這本書,應該不會以後只想做個小專案而已。
使用藍圖整理專案的手法很多種,官方文件也沒有給出建議的做法,所以 10 個團隊可能就會有 10 種不同的整理方法。我也提供一個我自己的做法,如果各位有更好的做法,都可以自己試試看。
首先,我假設這個專案裡的應用程式可能會越來越多,所以我先在專案的根目錄下建立一個叫做 apps
的目錄,裡面準備放所有的應用程式。就以上一章介紹部落格的文章 CRUD 功能為例,我認為它是一組文章相關的功能,所以我在 apps
目錄裡再開一個 post
目錄,裡面新增一個檔案 views.py
,現在的目錄結構看起來像這樣:
simple-flask-blog
├─ app.py
├─ apps
│ └─ post
│ └─ views.py # <-- 在這裡
├─ config
├─ db
├─ migrations
├─ models
├─ poetry.lock
├─ pyproject.toml
└─ templates
因為我想讓剛才新增的檔案決定使用者能「看到什麼」,所以我把它取名叫 views.py
,這在之後介紹到的 Django 框架也有類似的概念。views.py
的內容要寫些什麼?我先回到 app.py
把原本負責首頁,也就是文章列表的程式碼剪下來,貼到 apps/post/views.py
裡,再稍微調整一下:
# 檔案 apps/post/views.py
from flask import Blueprint, render_template
from models import Post
post_bp = Blueprint("post", __name__)
@post_bp.route("/")
@post_bp.route("/posts")
def index():
posts = Post.query.order_by(-Post.id).all()
return render_template("posts/index.html.jinja", posts=posts)
首先,先透過 Blueprint
類別建立了一個叫做 post_bp
的藍圖物件,後面加上的 _bp
只是我為了讓這個物件看起來跟藍圖有關而已。Blueprint
類別裡的 "post"
參數是這個藍圖的名字,請先記得它,待會在 url_for()
的時候會用上。
接著把原本的裝飾器 @app.route
改寫成 @post_bp.route()
,底下的 index()
函數的內容一個字都不用改,這樣前置做業就完成了。再來就是到 app.py
註冊剛才建立的藍圖物件:
# 檔案 app.py
from apps.post.views import post_bp
app = Flask(__name__)
app.register_blueprint(post_bp) # <-- 這裡
這樣就完成了。你可以現在再看看是不是功能一樣可以正常運作,如果有問題,可以先檢查一下程式碼有沒有打錯或是目錄、檔案放錯位置了。
首先搬完了,接下來我也把文章的 CRUD 文章的功能都搬過去,這不算複雜,只要把裝飾器名字一起改過來,然後該 import
的套件或函數也補上,基本上就沒問題了,因為程式碼有點長,可請大家檢視本次 Commit 的進度。
比較需要特別調整的,是之前如果用 url_for()
函數來產生 URL 的話,現在要加上藍圖的名字,例如原本是 url_for("show")
,現在要改成 url_for("post.show")
,這樣就可以正確產生 URL 了,這其實滿好的。所以搜尋一下專案裡用到的 url_for()
函數,然後把它們改成正確的名字。
另外,大家還記得原本在 app.py
還有一個專門處理 404 的傢伙嗎?以後也有可能會有更多處理像是 401 或 500 的情況,所以我也趁這次整理一併搬到 apps
目錄裡,建立一個 error
目錄,裡面新增檔案 handlers.py
,然後把原本的 404 處理程式碼貼過去,用相同的手法調整一下,只有一個地方需要特別注意:
from flask import Blueprint, render_template
error_bp = Blueprint("error", __name__)
@error_bp.app_errorhandler(404) # <-- 這裡
def page_not_found(_):
return render_template("errors/404.html.jinja"), 404
原本的 errorhandler()
裝飾器是給 Flask 的,不是給 Blueprint 的,所以這裡需要改成 app_errorhandler()
,這樣就完成了。最後別忘了把它註冊到 app.py
裡:
# 檔案 app.py
from apps.post.views import post_bp
from apps.error.handlers import error_bp
app = Flask(__name__)
app.register_blueprint(post_bp)
app.register_blueprint(error_bp)
現在整個專案的目錄結構看起來像這樣:
simple-flask-blog
├─ app.py
├─ apps
│ ├─ error
│ │ └─ handlers.py
│ └─ post
│ └─ views.py
├─ config
├─ db
├─ migrations
├─ models
├─ poetry.lock
├─ pyproject.toml
└─ templates
Blueprint 還有很多功能,例如可以把樣版、靜態檔案、錯誤處理等等都放在自己專屬的目錄裡,這樣可以更容易管理、組織專案,不過本章節的重點在於會員系統,所以到這裡先告一段落,有興趣的讀者可以再閱查閱官方文件。
分支名稱 14-using-blueprint
會員系統
接下來我們要來實作會員系統,功能會有會員註冊、登入、登出這些功能等功能,至於會員基本資料更新、忘記密碼、變更密碼等功能我就留給大家了。我們就先從會員註冊開始...
新增會員
其實會員系統就跟文章系統一樣,說穿了也就只是個 CRUD,只不過 CRUD 的對象不是文章而是會員資料,所以首先我先在 models
目錄裡新增一個 user.py
,準備建立會員資料的 Model:
class User(db.Model):
__tablename__ = "users"
id = mapped_column(Integer, primary_key=True)
username = mapped_column(String(50), nullable=False)
password = mapped_column(String(100), nullable=False)
created_at = mapped_column(DateTime, server_default=func.now())
updated_at = mapped_column(
DateTime, server_default=func.now(), server_onupdate=func.now()
)
def __repr__(self):
return f"{self.username}"
這裡沒什麼特別的,不過我發現 created_at
以及 updated_at
這兩個欄位我們之前文章的 Model 也出現過,之後其他 Model 應該也會用上,所以我打算把這兩個欄位抽出來變成一個類別,放在 models/mixins
目錄裡,檔名我就用 datetime.py
:
# 檔案 models/mixins/datetime.py
from sqlalchemy import DateTime
from sqlalchemy.orm import mapped_column
from sqlalchemy.sql import func
class TimeTrackable:
created_at = mapped_column(DateTime, server_default=func.now())
updated_at = mapped_column(
DateTime, server_default=func.now(), server_onupdate=func.now()
)
然後就讓 User
這個 Model 除了繼承應該繼承的之外,再另外繼承 TimeTrackable
類別:
# 檔案 models/user.py
from .mixins.datetime import TimeTrackable
class User(db.Model, TimeTrackable):
__tablename__ = "users"
id = mapped_column(Integer, primary_key=True)
username = mapped_column(String(50), nullable=False)
password = mapped_column(String(100), nullable=False)
def __repr__(self):
return f"{self.username}"
這樣以後要記錄建立時間以及更新時間的 Model 直接繼承 TimeTrackable
類別就行了,不用再重複寫一次。既然有了共用的模組,除了 User
Model 之外,上個章節的 Post
別忘了也一起改寫。最後再修正一下 models/__init__.py
:
# 檔案 models/__init__.py
from .post import Post
from .user import User
然後就能執行 flask db migrate
產生 Migration 檔以及 flask db upgrade
更新資料庫了:
$ flask db migrate -m 'add users table'
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'user'
Generating /private/tmp/simple-flask-blog/migrations/versions/3487c42faba5_add_users_table.py ... done
$ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade c6006c6206d2 -> 3487c42faba5, add users table
跟之前有一些些不一樣,這次我在做 flask db migrate
的時候額外加上了 -m
參數,主要是加上這次 Migration 想要做的事情是什麼,可以看到產生的 Migration 檔名除了原本的亂數外,還 會加上這個訊息,這樣以後看檔案名字就能猜出這個 Migration 是做什麼的。
分支名稱 15-add-user-model
註冊頁面
接下來實作會員註冊功能,首先先新增一個 Blueprint,因為功能跟使用者有關,所以我就在 apps
目錄裡新增一個 user
目錄,裡面同樣放一個 views.py
:
# 檔案 apps/user/views.py
from flask import Blueprint, render_template
user_bp = Blueprint("user", __name__)
@user_bp.route("/sign_up")
def new():
return render_template("users/new.html.jinja")
@user_bp.route("/create", methods=["POST"])
def create():
pass
這裡沒什麼特別的,就跟前面一樣的藍圖而已,這裡新增了一個看起來是 /sign_up
的路徑,另一個 /create
還沒什麼內容,先寫好準備著而已。接著到 app.py
註冊一下藍圖:
# 檔案 app.py
from apps.user.views import user_bp
app = Flask(__name__)
app.register_blueprint(post_bp)
app.register_blueprint(user_bp, url_prefix="/users") # <-- 這裡
app.register_blueprint(error_bp)
跟之前又有一些些不同的地方,這次註冊藍圖的時候多加了一個 url_prefix
參數,加這個參數可以避免跟其他藍圖的路徑衝突。想想看,如果兩個不同的藍圖都剛好設定了相同的路徑的時候該聽誰的?以上面的例子來說,加了 url_prefix
參數之後,在這個 user_bp
這個藍圖裡的所有路徑前面都會被冠上 /users
前綴,例如原本看起來是 /sign_up
,現在就會變成 /users/sign_up
。不只可以避免衝突,在各自的藍圖裡的路徑就能少寫幾個字,感覺好像也不錯,所以我也順手把文章的路徑改一下:
from apps.post.views import post_bp, index as root_view
from apps.user.views import user_bp
from apps.error.handlers import error_bp
app = Flask(__name__)
app.add_url_rule("/", view_func=root_view)
app.register_blueprint(post_bp, url_prefix="/posts")
app.register_blueprint(user_bp, url_prefix="/users")
app.register_blueprint(error_bp)
因為首頁的路徑是 /
,所以我另外使用 .add_url_rule
方法來設定路徑,然後借用文章藍圖裡的 index()
函數來用,在文章藍圖裡的其他路徑也別忘了改。
接著,我調整一下原本導覽列的連結,加上註冊與登入的連結:
<nav class="flex items-center justify-between my-2">
<ul class="flex gap-4">
<li><a href="{{ url_for("root") }}" class="hover:underline">文 章列表</a></li>
<li><a href="{{ url_for("post.new") }}" class="hover:underline">新增文章</a></li>
</ul>
<ul class="flex gap-4">
<li><a href="{{ url_for("user.new") }}" class="hover:underline">註冊</a></li>
<li><a href="#" class="hover:underline">登入</a></li>
<li></li>
</ul>
</nav>
再來是會員註冊表單 templates/users/new.html.jinja
:
{% extends "layout.html.jinja" %} {% block content %}
<h1 class="mb-2 text-2xl">會員註冊</h1>
<form
action="{{ url_for('user.create') }}"
method="POST"
class="flex flex-col gap-4"
onsubmit="return confirm('確認送出?')"
>
<div class="flex flex-col gap-2">
<label for="username">帳號</label>
<input type="text" id="username" name="username" class="..." placeholder="使用者帳號" />
</div>
<div class="flex flex-col gap-2">
<label for="password">密碼</label>
<input type="password" id="password" name="password" class="..." placeholder="請填寫密碼" />
</div>
<div class="flex flex-col gap-2">
<label for="password_confirm">密碼確認</label>
<input type="password" id="password_confirm" name="password_confirm" class="..." placeholder="請再次確認密碼" />
</div>
<div>
<button type="submit" class="p-2 text-lg text-white bg-blue-500 rounded-sm">註冊帳號</button>
</div>
</form>
{% endblock %}
這裡我稍微省略了一些 CSS,主要是為了讓程式碼看起來比較簡潔,不過這個表單的部分跟之前的新增文章表單差不多,只是多了一個密碼確認的欄位而已。再來就是要處理表單送出之後的事了。跟文章的 CRUD 的寫法差不多,只是我們在上個章節稍微偷懶了一下,沒有做表單的驗證,但會員註冊就不能這麼偷懶了,該填的沒有填,或是同一個帳號不能註冊兩欠,這些現在都得檢查,所以寫起來可能會像這樣:
# 檔案 apps/user/views.py
@user_bp.route("/create", methods=["POST"])
def create():
username = request.form.get("username")
password = request.form.get("password")
password_confirm = request.form.get("password_confirm")
if not username or not password:
flash("請填寫帳號及密碼")
return redirect(url_for("user.new"))
if password != password_confirm:
flash("密碼確認有誤")
return redirect(url_for("user.new"))
user_exists = User.query.filter_by(username=username).exists()
if user_exists:
flash("該帳號已註冊")
return redirect(url_for("user.new"))
# ....
# 未完,待續
隨著欄位越來越多,這樣的寫法會變得越來越難維護,所以我想借用另一個在 Flask 也常用到的套件 WTForms
來幫助我們搞定處理表單的驗證...
分支名稱 16-user-signup-form
表單驗證
WTForms
是一個 Python 專門用來處理表單的套件,可以幫我們搞定處理表單的驗證之類的事,讓程式碼看起來更加簡潔。在 Flask 有人幫忙寫了擴充套件 Flask-WTF
,在 Flask 裡使用 WTForms
更方便,先來安裝這個擴充套件:
$ poetry add flask-wtf
... 略 ...
Package operations: 2 installs, 0 updates, 0 removals
- Installing wtforms (3.1.2)
- Installing flask-wtf (1.2.1)
Writing lock file
雖然這個 WTF 的名字有點怪,但待會用起來的時候的手感還是很不錯的。接著我要來建立一個專門處理會員的表單類別,因為跟會員系統有關,我就把它放在 apps/user
目錄裡,檔名就取為 forms.py
,內容如下:
# 檔案 apps/user/forms.py
from flask_wtf import Form
from wtforms import StringField, PasswordField
from wtforms.validators import InputRequired, Length, EqualTo
STYLES = "text-xl w-full md:w-1/2 px-3 py-2 border border-gray-900 rounded-sm"
class UserRegisterForm(Form):
username = StringField(
"帳號",
validators=[InputRequired(), Length(min=4)],
render_kw={"placeholder": "使用者帳號", "class": STYLES},
)
password = PasswordField(
"密碼",
validators=[InputRequired()],
render_kw={"placeholder": "請填寫登入密碼", "class": STYLES},
)
password_confirm = PasswordField(
"密碼確認",
validators=[InputRequired(), EqualTo("password", message="密碼確認不符")],
render_kw={"placeholder": "再次確認密碼", "class": STYLES},
)
乍看之下好像有點多東西,不過這裡的 STYLES
樣式以及 render_kw
參數可以先不用管它,這是因為我想待會讓它長出來的樣子有 CSS 樣式以及順便帶入一些屬性,這樣待會就不用在 HTML 裡面寫太多的樣式。
validators
則是用來定義這個欄位的驗證規則,這裡我用了 InputRequired
來確保這個欄位一定要填寫,Length
則是用來檢查帳號是至少有 4 個字,EqualTo
則是用來驗證是不是跟 password
欄位一樣的輸入結果。接著我們要來修改一下會員註冊的表單,讓它使用這個表單類別:
# 檔案 apps/user/views.py
from .forms import UserRegisterForm
@user_bp.route("/sign_up")
def new():
form = UserRegisterForm() # <-- 使用表單
return render_template("users/new.html.jinja", form=form)
這樣就可以準備在 HTML 樣版裡使用它了。使用的方法也挺簡單,回到 templates/users/new.html.jinja
樣版,原本的表單欄位這樣寫:
<div class="flex flex-col gap-2">
<label for="username">帳號</label>
<input
type="text"
id="username"
name="username"
class="w-full p-2 text-xl border border-gray-900 rounded-sm md:w-1/2"
placeholder="使用者帳號"
/>
</div>
使用表單元件後,可以改成這樣寫:
<div class="flex flex-col gap-2">{{ form.username.label }} {{ form.username }}</div>
有覺得比較簡單一點嗎?使用表單元件好像可以省一些手寫 HTML 標籤的時間,但其實 HTML 標籤也沒幾個字,所以這並不是重點,重點在於待會表單送出後的驗證。然後又因為這樣的東西可能會重複使用,所以我要把它抽出來變成一個...函數,通常我會稱這樣的東西叫做「小幫手(Helper)」,在 Jinja 稱這東西叫做「巨集(Macro)」。因為這個小幫手是用來幫助表單元件的,所以我就把它放在 templates/helpers
目錄裡,檔名取為 form_field.html.jinja
,內容如下:
{% macro render_field(field) %}
<div class="flex flex-col gap-2">
{{ field.label(class="font-bold") }} {{ field(**kwargs)|safe }} {% if field.errors %}
<ul>
{% for error in field.errors %}
<li class="px-1 font-bold text-red-500">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
這個不是什麼厲害的東西,其實這段也是從 WTForms
的官方文件上面借來的 ,這裡使用 Jinja 的 macro
語法建立一個名為 render_field()
的巨集,各位要把巨集當函數看也可以,裡面就是寫一些打算重複使用的 HTML 標籤,也就是每當執行這個巨集,就可以幫我產生這一大塊的 <div>...</div>
。回到 templates/users/new.html.jinja
樣版來試用看看:
{% extends "layout.html.jinja" %} {% from "helpers/form_field.html.jinja" import render_field %} {% block content %}
<h1 class="mb-2 text-2xl">會員註冊</h1>
<form
action="{{ url_for('user.create') }}"
method="POST"
class="flex flex-col gap-4"
onsubmit="return confirm('確認送出?')"
>
{{ render_field(form.username) }} {{ render_field(form.password) }} {{ render_field(form.password_confirm) }}
<div>
<button type="submit" class="p-2 text-lg text-white bg-blue-500 rounded-sm">註冊帳號</button>
</div>
</form>
{% endblock %}
這裡看到的 {% from ... import ... %}
並不是 Python 的語法,雖然它看起來有點像,這同樣也是 Jinja 提供的,用途是引入其他檔案裡的巨集,接下來就可以在這個樣版裡使用它了。引入剛才寫的 render_field()
巨集,然後把表單元件傳給它,巨集就能幫我們產生相對應的 HTML 標籤。產生 HTML 標籤不是重點,重點是接下來送出表單之後的處理。回到 apps/user/views.py
檔案裡,我們要來處理一下送出來的表單:
# 檔案 apps/user/views.py
@user_bp.route("/create", methods=["POST"])
def create():
form = UserRegisterForm(request.form)
if form.validate():
user = User(username=form.username.data, password=form.password.data)
db.session.add(user)
db.session.commit()
flash("註冊成功!")
return redirect(url_for("root"))
return render_template("users/new.html.jinja", form=form)
把抓到的表單資料傳給 UserRegisterForm
表單類別,然後呼叫 .validate()
方法來驗證表單資料,驗證成功就把資料存進資料庫,這跟之前新增文章的做法差不多。沒有比較就沒有傷害,這應該比之前一個一個寫 if
判斷要簡單的多。
雖然使用內建的驗證器可以搞定大部分的驗證,像是會員帳號不能重複這件事就不是內建的驗證器能檢查的出來的,得自己另外寫個驗證器,這在 WTForms
還好寫的:
# 檔案 apps/user/forms.py
from wtforms.validators import InputRequired, Length, EqualTo, ValidationError
from models.user import User
# ... 略 ...
def unique_username(form, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError("該帳號已被申請")
class UserRegisterForm(Form):
username = StringField(
"帳號",
validators=[InputRequired(), Length(min=4), unique_username],
render_kw={"placeholder": "使用者帳號", "class": STYLES},
)
# ... 略 ...
這裡我寫了一個 unique_username()
函數,內容也很單純,就是去檢查資料表裡是否已經有相同的 username
存在,如果重複就會拋出 ValidationError
例外。接著把這個函數跟其他驗證器一樣掛在 validators
參數裡,這樣重複的帳號就會被擋下來了。
分支名稱 17-form-validation
密碼看的見!
雖然現在可以註冊會員了,但有另一個有點嚴重的問題,你只要打開 users
資料表看一眼就會發現,所有會員的密碼都是直接存在資料表裡,一看就知道每個人的密碼是什麼。我不確定各位有沒有意識到這有什麼問題,早期我甚至遇過有開發者跟我說這樣很方便啊,要幫使用者查密碼只要進資料庫就查的到。
或許有人會認為,就算被看到密碼又怎麼樣,最多不就是這個網站的密碼被別人知道而已。大家可能不知道一般人其實沒辦法記憶這麼多組密碼組合,大部份的人的密碼可能只是用簡單的生日或數字組合,或是用同一組帳號密碼在不同的網站上註冊,如果這個網站的帳號密碼組合被看到,在其他網站上的帳號也可能會因此被盜用。不管是開發者監守自盜還是被駭客入侵而讓會員資料外流,只要發生都可能造成嚴重的問題。
所以,我們得想個辦法對密碼做一些處置,讓密碼不要這麼容易被看懂,或至少增加被識破的難度。密碼學這時候就能派上用場了,不過我們不是密碼學家,沒能力也不太需要自己實作密碼學演算法,只需要使用現成的加密或雜湊函數就好。在前面章節曾經介紹過什麼是雜湊,它是一種單向計算函數,可以把一段文字轉換成另一段固定長度的雜湊值,雜湊值基本上是不可逆的,就算要逆也得要花非常多的時間。
這裡需要特別提一下,「加密(Encrypt)」和「雜湊(Hash)」雖然看起來好像都是會產生亂數 字串,但這兩個是不同的概念。只要有適當的金鑰或方法,加密過的資料是可以被解密的,但雜湊就是單向運算,是不可逆的。在這裡我們要用的是雜湊函數,對一般人來說看起來已經夠亂,所有些人可能會認為這是幫密碼「加密」,但就以專有名詞上來說還是不太一樣。
Python 有個叫做 Werkzeug
的套件,這個字是德文「工具」的意思,主要的功能是用來實作 WSGI 應用程式,事實上整個 Flask 框架就是建立在 Werkzeug
這個套件之上。Werkzeug
套件裡有滿好用的密碼學模組,包括很多常用的加密或雜湊函數。也有善心人士做了 Flask 的擴充套件 Flask-Bcrypt
,讓我們可以更方便在 Flask 裡使用 Werkzeug
套件裡的密碼學模組,所以先來安裝一下:
$ poetry add flask-bcrypt
Using version ^1.0.1 for flask-bcrypt
... 略 ...
- Installing bcrypt (4.1.3)
- Installing flask-bcrypt (1.0.1)
Writing lock file
在開始使用之前,因為待會使用 Flask-Bcrypt
套件的時候會用到 app.py
裡的 app
物件,現在它放在 app.py
裡不太好拿出來用,所以我想把它拿出來另外找地方擺,其他程式如果需要用的時候再匯入就好,我就把它擺在 apps
目錄裡的 __init__.py
裡:
# 檔案 apps/__init__.py
import os
from dotenv import load_dotenv
from pathlib import Path
from flask import Flask
load_dotenv()
ROOT_PATH = Path().parent.absolute()
TEMPLATE_FOLDER = ROOT_PATH / "templates"
DB_PATH = ROOT_PATH / "db" / "blog.sqlite"
app = Flask(__name__, template_folder=TEMPLATE_FOLDER)
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_PATH}"
app.secret_key = os.getenv("APP_SECRET_KEY")
如果在你的 apps
目錄裡沒有 __init__.py
檔案,手動新增一個即可。因為位置改了,所以像是 template_folder
以及 SQLite 資料庫檔案的路徑也要改一下,這裡我用 pathlib
來處理路徑,看起來比較清楚一點,接著回到根目錄的 app.py
:
# 檔案 app.py
# ... 略 ...
from apps import app # <-- 匯入 apps 資料夾裡的 app 物件
app.add_url_rule("/", view_func=root_view, endpoint="root")
app.register_blueprint(post_bp, url_prefix="/posts")
app.register_blueprint(user_bp, url_prefix="/users")
app.register_blueprint(error_bp)
db.init_app(app)
Migrate(app, db)
if __name__ == "__main__":
app.run(port=9527, debug=True)
這樣這個檔案看起來乾淨一點了。都整理完之後,回到原本要寫入會員資料的 create()
函數:
# 檔案 app/user/views.py
from flask_bcrypt import Bcrypt
from apps import app
user_bp = Blueprint("user", __name__)
bcrypt = Bcrypt(app)
# ... 略 ...
@user_bp.route("/create", methods=["POST"])
def create():
form = UserRegisterForm(request.form)
if form.validate():
hashed_password = bcrypt.generate_password_hash(form.password.data)
user = User(username=form.username.data, password=hashed_password)
db.session.add(user)
db.session.commit()
flash("註冊成功!")
return redirect(url_for("root"))
return render_template("users/new.html.jinja", form=form)