Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a9ce478
feat(models): add verification_tokens table to track tokens sent out …
ethnjs Jul 27, 2026
cf1f8d1
feat(auth): add verification token helpers: create and consume
ethnjs Jul 27, 2026
df328dc
refactor(auth): extract helpers from routes/auth to their correspondi…
ethnjs Jul 27, 2026
449c290
refactor(auth): removed core/email_verification helper module since t…
ethnjs Jul 27, 2026
3390aec
feat(auth): add email change, password change, and password reset sch…
ethnjs Jul 27, 2026
4988375
feat(auth): add account setup schemas for users whose account was ope…
ethnjs Jul 27, 2026
51c64da
feat(auth): add shared auth email html template for sign up verificat…
ethnjs Jul 27, 2026
9c2f99b
feat(auth): add new auth routes for email change, password change/res…
ethnjs Jul 27, 2026
5492550
feat(tests): mocked email service so that resend isn't called on ever…
ethnjs Jul 27, 2026
12d0bdf
feat(models): add sessions table to track user sessions and to revoke…
ethnjs Jul 27, 2026
a200026
feat(auth): remove JWT for sessions; add session helpers like revoke …
ethnjs Jul 27, 2026
28d9e81
feat(auth): wire session-based auth into login/register/logout/accoun…
ethnjs Jul 27, 2026
2421714
feat(user): replace User.is_active boolean with status field
ethnjs Jul 27, 2026
6355464
feat(user): revoke sessions when admin locks a user account
ethnjs Jul 27, 2026
3a47558
feat(user): add self-service account deactivate and delete routes
ethnjs Jul 27, 2026
1b3b876
fix(tests): ensure tests aren't sending actual emails
ethnjs Jul 27, 2026
cd83abf
feat(user): add session-facing settings endpoints
ethnjs Jul 27, 2026
0698bd2
chore: remove unused python-jose dependency
ethnjs Jul 27, 2026
a3634af
tests: new tests for new auth routes
ethnjs Jul 28, 2026
4b65558
feat(auth): add account settings frontend API client methods
ethnjs Jul 28, 2026
710b545
feat(settings): add settings layout and account page
ethnjs Jul 28, 2026
0941221
refactor(settings): move email change into its own modal with resend …
ethnjs Jul 28, 2026
e0fdcd3
fix(auth): redirect and clear cookie on invalid session token
ethnjs Jul 28, 2026
8cdced8
fix(users): remove email field from PATCH /users/me/ to close verific…
ethnjs Jul 28, 2026
3839a05
feat(nav): add settings link to user avatar dropdown
ethnjs Jul 28, 2026
6bd9389
fix(auth): scope forced logout to protected pages to stop redirect loop
ethnjs Jul 28, 2026
c241bb2
fix(users): remove email from UserUpdate to match backend PATCH /user…
ethnjs Jul 28, 2026
20cd780
refactor(icons): reorganize sections and update settings icon
ethnjs Jul 28, 2026
f243a22
feat(settings): add security page with password change form
ethnjs Jul 28, 2026
dc1a2c2
feat(auth): add password reset and email-change confirmation pages, c…
ethnjs Jul 28, 2026
7160ff7
fix(auth): add vertical padding to auth layout container
ethnjs Jul 28, 2026
00a61eb
fix(auth): force full reload on sign-in so auth context refetches user
ethnjs Jul 28, 2026
7173825
feat(settings): add session list and log-out-everywhere to security page
ethnjs Jul 28, 2026
cc446bb
style(settings): card-style sections and sticky nav rail
ethnjs Jul 28, 2026
9471410
fix(settings): hide password checklist until new password field has i…
ethnjs Jul 28, 2026
40fdec4
fix(settings): make nav bar fixed-position to stop scroll drift and c…
ethnjs Jul 28, 2026
5d94356
feat(settings): add unverified-email banner with resend action
ethnjs Jul 28, 2026
1bfc361
feat(auth): add pending email-change endpoint and response shape
ethnjs Jul 28, 2026
3e5a8de
feat(settings): wire up pending email change banner and modal to shar…
ethnjs Jul 28, 2026
f7607e7
fix(profile): point account settings edit link to /settings/account
ethnjs Jul 28, 2026
1c10c2c
feat(auth): shrink register and account-setup to email/password only
ethnjs Jul 28, 2026
345ab8c
feat(users): add is_onboarding_complete signal for onboarding gating
ethnjs Jul 28, 2026
02345d6
feat(auth): add email-change revert flow and session revocation on pa…
ethnjs Jul 28, 2026
71c57be
feat(api): sync frontend client with trimmed register/setup and email…
ethnjs Jul 28, 2026
713019c
feat(onboarding): add /onboarding route with redirect guard for incom…
ethnjs Jul 29, 2026
dc4562a
fix(users): drop ambiguous Union response_model on GET /users/me
ethnjs Jul 29, 2026
8406792
feat(auth): shrink sign-up to email/password and redirect into /onboa…
ethnjs Jul 29, 2026
78be9f5
feat(auth): redesign sign-in/sign-up chrome with cross-links and even…
ethnjs Jul 29, 2026
7999545
feat(home): redesign hero with interactive three.js grid warp and flo…
ethnjs Jul 29, 2026
500fb74
feat(auth): add locked Input state, account-setup page, and shared cr…
ethnjs Jul 29, 2026
3b80a25
feat(auth): add revert-email-change page for compromised-account reco…
ethnjs Jul 29, 2026
eeb80d0
fix(auth): make forgot-password success feedback visible via Banner
ethnjs Jul 29, 2026
291fa0d
feat(settings): add danger-zone deactivate/delete account UI
ethnjs Jul 29, 2026
257037d
fix(settings): collapse settings nav into a hamburger drawer on mobile
ethnjs Jul 30, 2026
4cffe13
fix(settings): uneven padding in settings section
ethnjs Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""add verification_tokens table

Revision ID: 013b9d385bd2
Revises: b92215c62c84
Create Date: 2026-07-27 06:28:19.504543

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = '013b9d385bd2'
down_revision: Union[str, None] = 'b92215c62c84'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table('verification_tokens',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('token_hash', sa.String(length=255), nullable=False),
sa.Column('purpose', sa.String(length=32), nullable=False),
sa.Column('new_email', sa.String(length=255), nullable=True),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('used_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_verification_tokens_id'), 'verification_tokens', ['id'], unique=False)
op.create_index(op.f('ix_verification_tokens_token_hash'), 'verification_tokens', ['token_hash'], unique=True)


def downgrade() -> None:
op.drop_index(op.f('ix_verification_tokens_token_hash'), table_name='verification_tokens')
op.drop_index(op.f('ix_verification_tokens_id'), table_name='verification_tokens')
op.drop_table('verification_tokens')
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""make first_name and last_name nullable again

Revision ID: 3cb43e5b0a3f
Revises: d10cb65643a2
Create Date: 2026-07-28 16:04:44.299156

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '3cb43e5b0a3f'
down_revision: Union[str, None] = 'd10cb65643a2'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.alter_column('users', 'first_name',
existing_type=sa.VARCHAR(length=100),
nullable=True)
op.alter_column('users', 'last_name',
existing_type=sa.VARCHAR(length=100),
nullable=True)


def downgrade() -> None:
op.alter_column('users', 'last_name',
existing_type=sa.VARCHAR(length=100),
nullable=False)
op.alter_column('users', 'first_name',
existing_type=sa.VARCHAR(length=100),
nullable=False)
42 changes: 42 additions & 0 deletions backend/alembic/versions/8c1053c617c1_add_sessions_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""add sessions table

Revision ID: 8c1053c617c1
Revises: 013b9d385bd2
Create Date: 2026-07-27 10:00:13.901096

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = '8c1053c617c1'
down_revision: Union[str, None] = '013b9d385bd2'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table('sessions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('token_hash', sa.String(length=64), nullable=False),
sa.Column('user_agent', sa.String(length=255), nullable=True),
sa.Column('ip_address', sa.String(length=45), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('last_active_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_sessions_id'), 'sessions', ['id'], unique=False)
op.create_index(op.f('ix_sessions_token_hash'), 'sessions', ['token_hash'], unique=True)


def downgrade() -> None:
op.drop_index(op.f('ix_sessions_token_hash'), table_name='sessions')
op.drop_index(op.f('ix_sessions_id'), table_name='sessions')
op.drop_table('sessions')
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""replace user is_active with status

Revision ID: d10cb65643a2
Revises: 8c1053c617c1
Create Date: 2026-07-27 10:51:09.714544

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'd10cb65643a2'
down_revision: Union[str, None] = '8c1053c617c1'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.add_column('users', sa.Column('status', sa.String(length=32), nullable=True))

conn = op.get_bind()

# is_active=false with a password set has no producing code path today —
# its correct status is ambiguous (deactivated? locked?), so refuse to
# guess and halt instead.
ambiguous = conn.execute(sa.text(
"SELECT id FROM users WHERE is_active = false AND hashed_password IS NOT NULL"
)).fetchall()
if ambiguous:
raise RuntimeError(
f"Cannot backfill status: {len(ambiguous)} user(s) have is_active=false "
f"with a password set (ids: {[r[0] for r in ambiguous]}). No existing code "
"path produces this combination, so the correct status can't be inferred "
"automatically — resolve manually before re-running this migration."
)

conn.execute(sa.text("UPDATE users SET status = 'active' WHERE is_active = true"))
conn.execute(sa.text("UPDATE users SET status = 'invited' WHERE is_active = false"))

op.alter_column('users', 'status', nullable=False)
op.drop_column('users', 'is_active')


def downgrade() -> None:
op.add_column('users', sa.Column('is_active', sa.Boolean(), nullable=True))

conn = op.get_bind()
conn.execute(sa.text("UPDATE users SET is_active = true WHERE status = 'active'"))
conn.execute(sa.text("UPDATE users SET is_active = false WHERE status != 'active'"))

op.alter_column('users', 'is_active', nullable=False)
op.drop_column('users', 'status')
Loading
Loading