mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
feat(email): require verified approval email
Backfill approved legacy users with verified emails and replace the old unverified-email warning setting with a single approval email policy.
This commit is contained in:
@@ -16,10 +16,14 @@ from backend.migration_steps.account_lockout import apply as apply_account_locko
|
||||
from backend.migration_steps.email_notification_settings import (
|
||||
apply as apply_email_notification_settings,
|
||||
)
|
||||
from backend.migration_steps.email_approval_policy import apply as apply_email_approval_policy
|
||||
from backend.migration_steps.task_thread_id import apply as apply_task_thread_id
|
||||
from backend.migration_steps.user_email_verification import (
|
||||
apply as apply_user_email_verification,
|
||||
)
|
||||
from backend.migration_steps.legacy_user_email_verification import (
|
||||
apply as apply_legacy_user_email_verification,
|
||||
)
|
||||
|
||||
|
||||
def test_pending_migration_is_recorded_and_skipped_on_next_run() -> None:
|
||||
@@ -83,12 +87,16 @@ def test_existing_migrations_are_registered_in_order() -> None:
|
||||
"2026050402_add_task_thread_id",
|
||||
"2026050501_add_email_notification_settings",
|
||||
"2026050601_add_user_email_verification",
|
||||
"2026050602_backfill_legacy_verified_emails",
|
||||
"2026050603_remove_legacy_email_approval_warning",
|
||||
]
|
||||
assert [migration.apply.__module__ for migration in MIGRATIONS] == [
|
||||
"backend.migration_steps.account_lockout",
|
||||
"backend.migration_steps.task_thread_id",
|
||||
"backend.migration_steps.email_notification_settings",
|
||||
"backend.migration_steps.user_email_verification",
|
||||
"backend.migration_steps.legacy_user_email_verification",
|
||||
"backend.migration_steps.email_approval_policy",
|
||||
]
|
||||
|
||||
|
||||
@@ -111,8 +119,9 @@ def test_email_notification_settings_migration_creates_settings_table() -> None:
|
||||
"notify_token_expiring",
|
||||
"notify_check_in_success",
|
||||
"require_admin_approval_for_registration",
|
||||
"warn_unverified_email_before_approval",
|
||||
"require_verified_email_for_approval",
|
||||
} <= columns
|
||||
assert "warn_unverified_email_before_approval" not in columns
|
||||
|
||||
|
||||
def test_user_email_verification_migration_adds_user_fields_and_policy_flags() -> None:
|
||||
@@ -145,10 +154,74 @@ def test_user_email_verification_migration_adds_user_fields_and_policy_flags() -
|
||||
} <= user_columns
|
||||
assert {
|
||||
"require_admin_approval_for_registration",
|
||||
"warn_unverified_email_before_approval",
|
||||
"require_verified_email_for_approval",
|
||||
} <= settings_columns
|
||||
|
||||
|
||||
def test_email_notification_settings_migration_removes_legacy_warning_column() -> None:
|
||||
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||
|
||||
with engine.connect() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE email_notification_settings ("
|
||||
"id INTEGER PRIMARY KEY, "
|
||||
"warn_unverified_email_before_approval BOOLEAN NOT NULL DEFAULT 1"
|
||||
")"
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
apply_email_approval_policy(conn)
|
||||
|
||||
columns = {
|
||||
row[1] for row in conn.execute(text("PRAGMA table_info(email_notification_settings)"))
|
||||
}
|
||||
|
||||
assert "warn_unverified_email_before_approval" not in columns
|
||||
assert "require_verified_email_for_approval" in columns
|
||||
|
||||
|
||||
def test_legacy_email_verification_migration_trusts_approved_existing_emails() -> None:
|
||||
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||
|
||||
with engine.connect() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"CREATE TABLE users ("
|
||||
"id INTEGER PRIMARY KEY, "
|
||||
"email VARCHAR(100), "
|
||||
"is_approved BOOLEAN NOT NULL DEFAULT 0, "
|
||||
"email_verified_at DATETIME"
|
||||
")"
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO users (id, email, is_approved, email_verified_at) VALUES "
|
||||
"(1, 'old@example.com', 1, NULL), "
|
||||
"(2, 'pending@example.com', 0, NULL), "
|
||||
"(3, '', 1, NULL), "
|
||||
"(4, 'verified@example.com', 1, '2026-01-01T00:00:00+00:00')"
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
apply_legacy_user_email_verification(conn)
|
||||
|
||||
rows = {
|
||||
row.id: row.email_verified_at
|
||||
for row in conn.execute(
|
||||
text("SELECT id, email_verified_at FROM users ORDER BY id")
|
||||
).fetchall()
|
||||
}
|
||||
|
||||
assert rows[1] is not None
|
||||
assert rows[2] is None
|
||||
assert rows[3] is None
|
||||
assert rows[4] == "2026-01-01T00:00:00+00:00"
|
||||
|
||||
|
||||
def test_account_lockout_migration_adds_missing_user_fields() -> None:
|
||||
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ def test_email_settings_update_preserves_and_clears_password(monkeypatch) -> Non
|
||||
assert updated.notify_token_expiring is False
|
||||
assert updated.notify_check_in_success is False
|
||||
assert updated.require_admin_approval_for_registration is True
|
||||
assert updated.warn_unverified_email_before_approval is True
|
||||
assert updated.require_verified_email_for_approval is True
|
||||
|
||||
EmailSettingsService.update_settings(
|
||||
session,
|
||||
@@ -87,7 +87,7 @@ def test_email_settings_update_preserves_and_clears_password(monkeypatch) -> Non
|
||||
notify_token_expiring=False,
|
||||
notify_check_in_success=False,
|
||||
require_admin_approval_for_registration=False,
|
||||
warn_unverified_email_before_approval=False,
|
||||
require_verified_email_for_approval=False,
|
||||
clear_smtp_sender_password=True,
|
||||
),
|
||||
)
|
||||
@@ -96,7 +96,7 @@ def test_email_settings_update_preserves_and_clears_password(monkeypatch) -> Non
|
||||
assert cleared.smtp_sender_password in ("", None)
|
||||
assert cleared.has_smtp_sender_password is False
|
||||
assert cleared.require_admin_approval_for_registration is False
|
||||
assert cleared.warn_unverified_email_before_approval is False
|
||||
assert cleared.require_verified_email_for_approval is False
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
@@ -209,6 +209,6 @@ def test_registration_approval_policy_defaults_enabled() -> None:
|
||||
snapshot = EmailSettingsService.get_snapshot(session)
|
||||
|
||||
assert snapshot.require_admin_approval_for_registration is True
|
||||
assert snapshot.warn_unverified_email_before_approval is True
|
||||
assert snapshot.require_verified_email_for_approval is True
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
@@ -84,7 +84,10 @@ def test_frontend_admin_approval_policy_warnings_are_visible() -> None:
|
||||
assert "allow_unverified_email" in admin_users
|
||||
assert "邮箱未验证" in admin_users
|
||||
assert "require_admin_approval_for_registration" in email_settings
|
||||
assert "require_verified_email_for_approval" in email_settings
|
||||
assert "审批前要求验证邮箱" in email_settings
|
||||
assert "关闭管理员审批" in email_settings
|
||||
assert "未验证邮箱审批警告" not in email_settings
|
||||
|
||||
|
||||
def test_frontend_replaces_starter_component() -> None:
|
||||
@@ -110,3 +113,13 @@ def test_dashboard_qr_refresh_uses_dialog() -> None:
|
||||
assert "qrRefreshDialogOpen" in dashboard
|
||||
assert "<Dialog" in dashboard
|
||||
assert "<DialogContent" in dashboard
|
||||
|
||||
|
||||
def test_settings_email_changes_use_verification_flow() -> None:
|
||||
settings = (SRC_ROOT / "views" / "SettingsView.vue").read_text(encoding="utf-8")
|
||||
api = (SRC_ROOT / "api" / "index.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "userApi.setEmail" in settings
|
||||
assert "userApi.verifyEmail" in settings
|
||||
assert "验证码" in settings
|
||||
assert "email?: string" not in api
|
||||
|
||||
@@ -12,7 +12,7 @@ from backend.api import users as users_api
|
||||
from backend.dependencies import get_current_user, get_db
|
||||
from backend.models import Base, User
|
||||
from backend.schemas.email_settings import EmailNotificationSettingsUpdate
|
||||
from backend.schemas.user import UserUpdate
|
||||
from backend.schemas.user import UserUpdate, UserUpdateProfile
|
||||
from backend.services import email_settings_service
|
||||
from backend.services.admin_service import AdminService
|
||||
from backend.services.email_service import EmailService
|
||||
@@ -116,6 +116,24 @@ def test_changing_email_clears_verification_state(monkeypatch) -> None:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_profile_update_rejects_email_changes() -> None:
|
||||
engine, _, session = make_session()
|
||||
user = add_user(session, email="old@example.com")
|
||||
|
||||
with pytest.raises(ValueError, match="邮箱"):
|
||||
UserService.update_user_profile(
|
||||
user.id,
|
||||
UserUpdateProfile(email="new@example.com"),
|
||||
session,
|
||||
)
|
||||
|
||||
refreshed = session.get(User, user.id)
|
||||
assert refreshed is not None
|
||||
assert refreshed.email == "old@example.com"
|
||||
session.close()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_approval_requires_warning_then_allows_override(monkeypatch) -> None:
|
||||
engine, session_factory, session = make_session()
|
||||
monkeypatch.setattr(email_settings_service, "SessionLocal", session_factory)
|
||||
@@ -134,7 +152,7 @@ def test_approval_requires_warning_then_allows_override(monkeypatch) -> None:
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def test_approval_warning_can_be_disabled(monkeypatch) -> None:
|
||||
def test_verified_email_requirement_can_be_disabled(monkeypatch) -> None:
|
||||
engine, session_factory, session = make_session()
|
||||
monkeypatch.setattr(email_settings_service, "SessionLocal", session_factory)
|
||||
EmailSettingsService.update_settings(
|
||||
@@ -147,7 +165,7 @@ def test_approval_warning_can_be_disabled(monkeypatch) -> None:
|
||||
notify_token_expiring=True,
|
||||
notify_check_in_success=True,
|
||||
require_admin_approval_for_registration=True,
|
||||
warn_unverified_email_before_approval=False,
|
||||
require_verified_email_for_approval=False,
|
||||
),
|
||||
)
|
||||
user = add_user(session)
|
||||
@@ -192,7 +210,6 @@ def test_registration_approval_policy_controls_new_user_approval(monkeypatch) ->
|
||||
notify_token_expiring=True,
|
||||
notify_check_in_success=True,
|
||||
require_admin_approval_for_registration=False,
|
||||
warn_unverified_email_before_approval=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user