From 46f44f6171813179ebed1bcfc32b6d2ec10fdcbb Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 23 Feb 2026 09:55:27 +0000 Subject: [PATCH] OAuth SSO infrastructure + account app support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OAuthCode model + migration for authorization code flow - OAuth client blueprint (auto-registered for non-federation apps) - Per-app first-party session cookies (fixes Safari ITP) - /oauth/authorize endpoint support in URL helpers - account_url() helper + Jinja global - Templates: federation_url('/auth/...') → account_url('/...') - Widget registry: account page links use account_url Co-Authored-By: Claude Opus 4.6 --- .../p6n4k0l2m3_add_oauth_codes_table.py | 37 +++++ browser/templates/_types/auth/_nav.html | 2 +- .../_types/auth/_newsletter_toggle.html | 2 +- .../_types/auth/_newsletters_panel.html | 2 +- .../templates/_types/auth/header/_header.html | 2 +- browser/templates/_types/root/_full_user.html | 2 +- .../_types/root/mobile/_full_user.html | 2 +- infrastructure/factory.py | 16 ++- infrastructure/jinja_setup.py | 3 +- infrastructure/oauth.py | 130 ++++++++++++++++++ infrastructure/urls.py | 35 +++-- models/__init__.py | 1 + models/oauth_code.py | 25 ++++ services/widget_registry.py | 4 +- 14 files changed, 236 insertions(+), 27 deletions(-) create mode 100644 alembic/versions/p6n4k0l2m3_add_oauth_codes_table.py create mode 100644 infrastructure/oauth.py create mode 100644 models/oauth_code.py diff --git a/alembic/versions/p6n4k0l2m3_add_oauth_codes_table.py b/alembic/versions/p6n4k0l2m3_add_oauth_codes_table.py new file mode 100644 index 0000000..d74a687 --- /dev/null +++ b/alembic/versions/p6n4k0l2m3_add_oauth_codes_table.py @@ -0,0 +1,37 @@ +"""Add oauth_codes table + +Revision ID: p6n4k0l2m3 +Revises: o5m3j9k1l2 +Create Date: 2026-02-23 +""" +from alembic import op +import sqlalchemy as sa + +revision = "p6n4k0l2m3" +down_revision = "o5m3j9k1l2" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "oauth_codes", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("code", sa.String(128), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("client_id", sa.String(64), nullable=False), + sa.Column("redirect_uri", sa.String(512), nullable=False), + 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), server_default=sa.func.now(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), + ) + op.create_index("ix_oauth_code_code", "oauth_codes", ["code"], unique=True) + op.create_index("ix_oauth_code_user", "oauth_codes", ["user_id"]) + + +def downgrade() -> None: + op.drop_index("ix_oauth_code_user", table_name="oauth_codes") + op.drop_index("ix_oauth_code_code", table_name="oauth_codes") + op.drop_table("oauth_codes") diff --git a/browser/templates/_types/auth/_nav.html b/browser/templates/_types/auth/_nav.html index 9d8042a..93499a8 100644 --- a/browser/templates/_types/auth/_nav.html +++ b/browser/templates/_types/auth/_nav.html @@ -1,5 +1,5 @@ {% import 'macros/links.html' as links %} -{% call links.link(federation_url('/auth/newsletters/'), hx_select_search, select_colours, True, aclass=styles.nav_button) %} +{% call links.link(account_url('/newsletters/'), hx_select_search, select_colours, True, aclass=styles.nav_button) %} newsletters {% endcall %} {% for link in account_nav_links %} diff --git a/browser/templates/_types/auth/_newsletter_toggle.html b/browser/templates/_types/auth/_newsletter_toggle.html index af3a4e1..8bb3f69 100644 --- a/browser/templates/_types/auth/_newsletter_toggle.html +++ b/browser/templates/_types/auth/_newsletter_toggle.html @@ -1,6 +1,6 @@