feat: add secure invitation and reset links

This commit is contained in:
2026-08-05 11:31:38 +08:00
parent 540b8e0932
commit 8ef0c312ef
14 changed files with 418 additions and 75 deletions
+19 -14
View File
@@ -6,17 +6,22 @@ from .extensions import db
def upgrade_schema() -> None:
"""Create new tables and add authentication columns without losing existing data."""
db.create_all()
inspector = inspect(db.engine)
columns = {item["name"] for item in inspector.get_columns("users")}
additions = {
"email": "VARCHAR(254)",
"email_verified_at": "DATETIME",
"is_active_account": "BOOLEAN NOT NULL DEFAULT 0",
"auth_version": "INTEGER NOT NULL DEFAULT 1",
}
for name, sql_type in additions.items():
if name not in columns:
db.session.execute(text(f"ALTER TABLE users ADD COLUMN {name} {sql_type}"))
db.session.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_users_email_unique ON users (email) WHERE email IS NOT NULL"))
db.session.commit()
with db.engine.begin() as connection:
# Gunicorn workers can import the application simultaneously. Acquiring
# SQLite's write lock before inspecting or creating tables serializes the
# first-start migration and prevents duplicate CREATE TABLE attempts.
if db.engine.dialect.name == "sqlite":
connection.exec_driver_sql("BEGIN IMMEDIATE")
db.metadata.create_all(bind=connection)
inspector = inspect(connection)
columns = {item["name"] for item in inspector.get_columns("users")}
additions = {
"email": "VARCHAR(254)",
"email_verified_at": "DATETIME",
"is_active_account": "BOOLEAN NOT NULL DEFAULT 0",
"auth_version": "INTEGER NOT NULL DEFAULT 1",
}
for name, sql_type in additions.items():
if name not in columns:
connection.execute(text(f"ALTER TABLE users ADD COLUMN {name} {sql_type}"))
connection.execute(text("CREATE UNIQUE INDEX IF NOT EXISTS ix_users_email_unique ON users (email) WHERE email IS NOT NULL"))