Single sign-on wrote a random string nobody would ever know. That reads as "has a password" to everything that asks — so Settings demanded a current password before it would let those accounts set their first, and the only way through was to click "forgot password" for a password they never had. The same trap was waiting for anybody who only ever signs in with a code. Null says the true thing. Signing in refuses an account with no password the way it refuses a wrong one, because which accounts have one is not a question that endpoint answers. Setting a first password asks for no current one; changing an existing password still does. `/auth/me` reports whether there is one at all and nothing about it, because Settings has to choose between "Set a password" and "Change password" and cannot tell from the outside. The random strings already written are left alone. They are unguessable, so nothing can sign in with them, and clearing them would mean deciding from outside which accounts were meant to have one. Identity is the email address throughout, so the three ways in are three ways into the same account: single sign-on, a code, or a password — and a person may acquire or drop the third at any point without losing the other two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
30 lines
1 KiB
Python
30 lines
1 KiB
Python
"""An account may have no password.
|
|
|
|
Single sign-on wrote a random string nobody would ever know, which reads as
|
|
"has a password" to everything that asks — so Settings demanded a current
|
|
password before it would let those accounts set their first, and the only way
|
|
through was to click "forgot password" for one they never had.
|
|
|
|
Null says the true thing. The random strings already written are left alone:
|
|
they are unguessable, so nothing can sign in with them, and clearing them would
|
|
mean deciding from outside which accounts were meant to have one.
|
|
|
|
Revision ID: h1b2c3d4e5f6
|
|
Revises: g9a1b2c3d4e5
|
|
"""
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision = "h1b2c3d4e5f6"
|
|
down_revision = "g9a1b2c3d4e5"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade():
|
|
op.alter_column("users", "hashed_password", existing_type=sa.String(), nullable=True)
|
|
|
|
|
|
def downgrade():
|
|
op.execute("UPDATE users SET hashed_password = '' WHERE hashed_password IS NULL")
|
|
op.alter_column("users", "hashed_password", existing_type=sa.String(), nullable=False)
|