55 lines
2.0 KiB
Python
55 lines
2.0 KiB
Python
"""push notifications about new mail (Web Push, RFC 8030/8291/8292)
|
|||
|
|
|
||
|
|
``push_subscriptions`` holds one row per browser (or installed app) that asked
|
||
|
|
to be told about new mail: the push service URL it gave, and the two keys the
|
||
|
|
message is encrypted to. ``session_sid`` is the sign-in it was made under — once
|
||
|
|
that session is gone (signed out, revoked, expired) nothing is sent there any
|
||
|
|
more, so a signed-out device never shows a subject line again.
|
||
|
|
|
||
|
|
The server signs every push with its own key pair (VAPID). It is generated on
|
||
|
|
first use and kept in ``app_settings``; the private half is vault-encrypted
|
||
|
|
with MASTER_KEY like every other server secret.
|
||
|
|
|
||
|
|
Revision ID: 0022
|
||
|
|
Revises: 0021
|
||
|
|
Create Date: 2026-10-08
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from collections.abc import Sequence
|
||
|
|
|
||
|
|
from alembic import op
|
||
|
|
|
||
|
|
revision: str = "0022"
|
||
|
|
down_revision: str | None = "0021"
|
||
|
|
branch_labels: str | Sequence[str] | None = None
|
||
|
|
depends_on: str | Sequence[str] | None = None
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
op.execute(
|
||
|
|
"""
|
||
|
|
CREATE TABLE push_subscriptions (
|
||
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
|
|
endpoint text NOT NULL UNIQUE,
|
||
|
|
p256dh text NOT NULL,
|
||
|
|
auth text NOT NULL,
|
||
|
|
session_sid text NOT NULL,
|
||
|
|
user_agent text NOT NULL DEFAULT '',
|
||
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||
|
|
last_used_at timestamptz
|
||
|
|
)
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
op.execute("CREATE INDEX ix_push_subscriptions_user ON push_subscriptions (user_id)")
|
||
|
|
op.execute("ALTER TABLE app_settings ADD COLUMN vapid_public text NOT NULL DEFAULT ''")
|
||
|
|
op.execute("ALTER TABLE app_settings ADD COLUMN vapid_private_ct bytea")
|
||
|
|
|
||
|
|
|
||
|
|
def downgrade() -> None:
|
||
|
|
op.execute("ALTER TABLE app_settings DROP COLUMN IF EXISTS vapid_private_ct")
|
||
|
|
op.execute("ALTER TABLE app_settings DROP COLUMN IF EXISTS vapid_public")
|
||
|
|
op.execute("DROP TABLE IF EXISTS push_subscriptions")
|