supython 0.1.14__tar.gz → 0.1.16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {supython-0.1.14 → supython-0.1.16}/CHANGELOG.md +133 -0
- {supython-0.1.14 → supython-0.1.16}/PKG-INFO +26 -9
- {supython-0.1.14 → supython-0.1.16}/README.md +25 -8
- {supython-0.1.14 → supython-0.1.16}/pyproject.toml +1 -1
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/providers/__init__.py +8 -1
- supython-0.1.16/src/supython/auth/providers/github.py +50 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/providers/google.py +4 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/providers/oauth.py +11 -0
- supython-0.1.16/src/supython/auth/router.py +747 -0
- supython-0.1.16/src/supython/auth/schemas.py +177 -0
- supython-0.1.16/src/supython/auth/service.py +1388 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/client/__init__.py +4 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/client/_auth.py +169 -7
- supython-0.1.16/src/supython/migrations/0018_auth_email_confirmation.sql +25 -0
- supython-0.1.16/src/supython/migrations/0019_hash_refresh_tokens.sql +19 -0
- supython-0.1.16/src/supython/migrations/0020_email_change_and_metadata.sql +34 -0
- supython-0.1.16/src/supython/netutil.py +64 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/env.example.tmpl +21 -1
- {supython-0.1.14 → supython-0.1.16}/src/supython/settings.py +25 -0
- supython-0.1.14/src/supython/auth/providers/github.py +0 -22
- supython-0.1.14/src/supython/auth/router.py +0 -322
- supython-0.1.14/src/supython/auth/schemas.py +0 -92
- supython-0.1.14/src/supython/auth/service.py +0 -753
- {supython-0.1.14 → supython-0.1.16}/.gitignore +0 -0
- {supython-0.1.14 → supython-0.1.16}/LICENSE +0 -0
- {supython-0.1.14 → supython-0.1.16}/SECURITY.md +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/auth.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/auth_templates.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/auth_users.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/db.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/functions.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/jobs.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/ops.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/realtime.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_auth.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_auth_templates.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_auth_users.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_db.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_functions.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_jobs.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_ops.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_realtime.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/service_storage.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/storage.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/api/system.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/audit.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/deps.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/errors.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/schemas.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/session.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/spa.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Alert-dluGVkos.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Alert-dluGVkos.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Audit-Njung3HI.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Audit-Njung3HI.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Backups-DzPlFgrm.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Backups-DzPlFgrm.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Buckets-ByacGkU1.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Buckets-ByacGkU1.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Channels-BoIuTtam.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Crons-B67vc39F.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Crons-B67vc39F.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/DataTable-COAAWEft.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Empty-cr2r7e2u.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Empty-cr2r7e2u.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Grid-hFkp9F4P.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Grid-hFkp9F4P.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Input-DppYTq9C.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Input-DppYTq9C.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/JsonField-DibyJgun.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/JsonField-DibyJgun.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/LoginView-BjLyE3Ds.css +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/LoginView-CoOjECT_.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/LoginView-CoOjECT_.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Logs-D9WYrnIT.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Logs-D9WYrnIT.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Logs-DS1XPa0h.css +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Queue-CywZs6vI.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Queue-CywZs6vI.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Routes-BiLXE49D.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Routes-BiLXE49D.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Routes-C-ianIGD.css +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Select-DIzZyRZb.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Select-DIzZyRZb.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Space-n5-XcguU.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Space-n5-XcguU.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/TableData-CQIagLKn.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/TableData-CQIagLKn.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Tag-D1fOKpTH.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Tag-D1fOKpTH.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Templates-BS-ugkdq.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Templates-BS-ugkdq.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Thing-CEAniuMg.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Thing-CEAniuMg.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Users-wzwajhlh.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/Users-wzwajhlh.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/dist-VXIJLCYq.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/dist-VXIJLCYq.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/format-length-CGCY1rMh.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/format-length-CGCY1rMh.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/get-Ca6unauB.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/get-Ca6unauB.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/index-CeE6v959.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/index-CeE6v959.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/pinia-COXwfrOX.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/pinia-COXwfrOX.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/resources-Bt6thQCD.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/resources-Bt6thQCD.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useResource-C_rJCY8C.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useResource-C_rJCY8C.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useTable-CnZc5zhi.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useTable-CnZc5zhi.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useTable-Dg0XlRlq.css +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useToast-DsZKx0IX.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/useToast-DsZKx0IX.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/utils-sbXoq7Ir.js +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/assets/utils-sbXoq7Ir.js.map +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/favicon.svg +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/icons.svg +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/admin/static/index.html +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/app.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/_email_job.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/claims.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/deps.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/providers/registry.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/auth/ratelimit.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/backups/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/backups/_backup_job.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/backups/schemas.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/backups/service.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/body_size.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/cli.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/client/_client.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/client/_config.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/client/_functions.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/client/_storage.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/client/py.typed +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/db.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/db_admin.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/extensions.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/functions/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/functions/context.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/functions/loader.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/functions/router.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/functions/schemas.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/gen/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/gen/_introspect.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/gen/types_py.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/gen/types_ts.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/health.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/hooks.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/backends.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/context.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/cron.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/cron_inproc.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/decorators.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/registry.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/router.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/schemas.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/service.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jobs/worker.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/jwks.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/keyset.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/logging_config.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/mail.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/mailer.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrate.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0001_extensions_and_roles.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0002_auth_schema.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0004_auth_v0_2.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0005_storage_schema.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0006_realtime_schema.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0007_jobs_schema.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0008_jobs_last_error.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0009_auth_rate_limits.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0010_worker_heartbeat.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0011_admin_schema.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0012_auth_banned_until.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0013_email_templates.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0014_realtime_payload_warning.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0015_backups_schema.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0016_magic_link_redirect.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/migrations/0017_auth_activated_at.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/passwords.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/realtime/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/realtime/broker.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/realtime/protocol.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/realtime/router.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/realtime/schemas.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/realtime/service.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/realtime/topics.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/realtime/websocket.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/init_project.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/Caddyfile.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/README.md.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/apps_hooks_welcome.py.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/apps_jobs_example.py.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/asgi.py.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/docker-compose.prod.yml.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/docker-compose.yml.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/functions_README.md.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/gitignore.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/manage.py.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/package_hooks_init.py.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/package_init.py.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/package_jobs_init.py.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/pyproject.toml.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/settings.py.tmpl +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/static/0001_create_todos.sql +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/scaffold/templates/static/functions_hello.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/secretset.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/security_headers.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/settings_module.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/storage/__init__.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/storage/backends.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/storage/router.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/storage/schemas.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/storage/service.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/storage/signing.py +0 -0
- {supython-0.1.14 → supython-0.1.16}/src/supython/tokens.py +0 -0
|
@@ -30,6 +30,139 @@ Each entry links the relevant `PROJECT.md` section and decision-log row
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## [0.1.16] — 2026-07-22
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- Emailed-link interstitials (`GET /auth/v1/{confirm,magiclink,email_change}/verify`)
|
|
37
|
+
are usable in a browser again. The global CSP's `form-action 'none'`
|
|
38
|
+
blocked the form's self-POST that consumes the one-time token, so
|
|
39
|
+
clicking Confirm silently did nothing (#13). These responses now carry
|
|
40
|
+
their own scoped `Content-Security-Policy`
|
|
41
|
+
(`form-action 'self'; style-src 'unsafe-inline'`, still no `script-src`),
|
|
42
|
+
which the security-headers middleware yields to.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- Restyled the verify interstitials and the invalid-link page: a
|
|
46
|
+
dark-mode-aware card with inline SVG status icons, replacing the
|
|
47
|
+
previous bare unstyled markup. Still fully self-contained (system
|
|
48
|
+
fonts, no external requests) to stay within the scoped CSP.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [0.1.15] — 2026-07-21
|
|
53
|
+
|
|
54
|
+
Auth hardening from the GHSA security review (three stacked PRs, #10–#12).
|
|
55
|
+
|
|
56
|
+
### Breaking
|
|
57
|
+
- Emailed verify links (`GET /auth/v1/{magiclink,confirm}/verify`) are now
|
|
58
|
+
side-effect-free HTML interstitials; the token is consumed by the form
|
|
59
|
+
**POST** to the same path. Mail scanners that prefetch the GET no longer
|
|
60
|
+
burn the token or (in redirect mode) receive the session. Any client
|
|
61
|
+
calling the verify endpoint directly must switch GET → POST (the bundled
|
|
62
|
+
SDK already does).
|
|
63
|
+
- OAuth `redirect_uri` must now match `OAUTH_REDIRECT_ALLOWLIST`
|
|
64
|
+
(comma-separated origins). The allowlist is empty by default, which
|
|
65
|
+
**fails closed**: OAuth sign-in returns `invalid_redirect` (400) until
|
|
66
|
+
the setting lists your app origins. Previously any `redirect_uri`
|
|
67
|
+
accepted by the provider was 302'd to with the token pair in the URL
|
|
68
|
+
fragment — a token-stealing open redirect on laxly configured
|
|
69
|
+
providers.
|
|
70
|
+
- Refresh tokens are stored as sha256 hashes (migration `0019` converts
|
|
71
|
+
existing rows in place; issued raw tokens keep working). Anything that
|
|
72
|
+
read raw tokens out of `auth.refresh_tokens` directly — including the
|
|
73
|
+
admin refresh-token list — now sees digests.
|
|
74
|
+
- `client.auth.sign_up(...)` now resolves to a `SignUpResponse`
|
|
75
|
+
(`{user, session, confirmation_sent_at}`) instead of a bare
|
|
76
|
+
`TokenResponse`; `session` is `None` when the server requires email
|
|
77
|
+
confirmation. Read tokens via `result.data.session.access_token`.
|
|
78
|
+
- OAuth sign-ins whose provider cannot vouch for the email address are
|
|
79
|
+
refused with `provider_email_unverified` (403). In particular, GitHub
|
|
80
|
+
accounts with no *verified* email can no longer sign in or link.
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
- Email change with dual confirmation. `PUT /auth/v1/user` with `email`
|
|
84
|
+
starts it; a confirmation link is sent to **both** the current and the
|
|
85
|
+
new address, and the change applies only once both are verified
|
|
86
|
+
(`POST /auth/v1/email_change/verify`, 202 until the second side). The
|
|
87
|
+
current-inbox requirement means a stolen access token alone cannot
|
|
88
|
+
re-point the account email. New `EMAIL_CHANGE_TOKEN_TTL` (1 h) and two
|
|
89
|
+
operator-editable templates (migration `0020`).
|
|
90
|
+
- User/app metadata. `raw_user_meta_data` is now wired through: set at
|
|
91
|
+
signup via `data`, merged via `PUT /auth/v1/user` `data`, and returned
|
|
92
|
+
as `user_metadata` on the user object — user-controlled, display-only,
|
|
93
|
+
**never** an authorization input. New server-controlled
|
|
94
|
+
`raw_app_meta_data` (migration `0020`) records the auth provider(s) and
|
|
95
|
+
is surfaced as `app_metadata`. SDK: `auth.update_user(email=, data=)`,
|
|
96
|
+
`auth.verify_email_change(token)`.
|
|
97
|
+
- `TRUSTED_PROXIES` — proxy-aware client-IP resolution for rate limiting
|
|
98
|
+
and audit logging. When the TCP peer is a listed proxy, the IP is taken
|
|
99
|
+
from the rightmost untrusted `X-Forwarded-For` hop; empty (default)
|
|
100
|
+
ignores the header so a spoofed value can never shift a rate-limit
|
|
101
|
+
bucket.
|
|
102
|
+
- Signup email-confirmation flow (§9.2). With
|
|
103
|
+
`AUTH_REQUIRE_EMAIL_CONFIRMATION=true`, `POST /auth/v1/signup` returns
|
|
104
|
+
202 without tokens and emails a confirmation link;
|
|
105
|
+
`GET /auth/v1/confirm/verify` redeems it (optionally 302-redirecting to
|
|
106
|
+
an allowlisted `redirect_url` with the token pair in the fragment) and
|
|
107
|
+
`POST /auth/v1/confirm/resend` re-sends it. Sign-in and refresh are
|
|
108
|
+
refused with `email_not_confirmed` (403) until the address is proven.
|
|
109
|
+
New settings: `AUTH_REQUIRE_EMAIL_CONFIRMATION` (default off),
|
|
110
|
+
`SIGNUP_CONFIRM_TOKEN_TTL` (24 h),
|
|
111
|
+
`AUTH_RATE_LIMIT_CONFIRM_PER_WINDOW` (5). New `signup_confirm`
|
|
112
|
+
one-time-token type and operator-editable email template
|
|
113
|
+
(migration `0018`). Client SDK grows `auth.verify_signup(token)` and
|
|
114
|
+
`auth.resend_confirmation(email)`.
|
|
115
|
+
- Scoped signout. `POST /auth/v1/logout` accepts
|
|
116
|
+
`{refresh_token?, scope: local|global|others}` (default `local`, wire-
|
|
117
|
+
compatible with the old body). `local` also revokes the token's rotated
|
|
118
|
+
descendants; `global` revokes every session and can be driven by the
|
|
119
|
+
bearer access token alone; `others` keeps only the presented session.
|
|
120
|
+
Global/others sign-outs write a `sign_out` audit event. Client SDK:
|
|
121
|
+
`auth.sign_out(scope=...)`.
|
|
122
|
+
- `PUT /auth/v1/user` — authenticated password change. Requires
|
|
123
|
+
`current_password` when one is set (a stolen access token alone cannot
|
|
124
|
+
take over the credential); passwordless (OAuth-only/invite) accounts
|
|
125
|
+
may set a first password with just their bearer. Revokes every refresh
|
|
126
|
+
token and returns a fresh pair. Client SDK:
|
|
127
|
+
`auth.update_password(new, current)`.
|
|
128
|
+
|
|
129
|
+
### Changed
|
|
130
|
+
- `auth.users.email_confirmed_at` is now an honest "inbox ownership
|
|
131
|
+
proven" flag: stamped by signup confirmation, magic-link / OTP /
|
|
132
|
+
recovery verification, and provider-verified OAuth sign-ins — never by
|
|
133
|
+
signup itself (previously it was set unconditionally at signup). Rows
|
|
134
|
+
created before `0018` keep their old unproven stamp; see the migration
|
|
135
|
+
header for the strict-mode cleanup query.
|
|
136
|
+
|
|
137
|
+
### Fixed
|
|
138
|
+
- One-time-token consumption (recover / magic link / OTP / signup
|
|
139
|
+
confirm) is now a single atomic `update … where used_at is null`,
|
|
140
|
+
closing the race where two concurrent verifies of the same token could
|
|
141
|
+
both succeed.
|
|
142
|
+
|
|
143
|
+
### Security
|
|
144
|
+
- Emailed verify links are no longer consumable by GET prefetch (see
|
|
145
|
+
Breaking) — closes the link-scanner token-burn / session-leak window.
|
|
146
|
+
- `PUT /auth/v1/user` email change requires confirmation from the current
|
|
147
|
+
inbox as well as the new one, so a stolen access token cannot silently
|
|
148
|
+
hijack the account's email.
|
|
149
|
+
- A successful password reset (`/auth/v1/recover/verify`) now revokes
|
|
150
|
+
every existing refresh token and every other pending recover token, so
|
|
151
|
+
a suspected-stolen session does not survive the reset. Password change
|
|
152
|
+
via `PUT /auth/v1/user` does the same.
|
|
153
|
+
- Refresh tokens are hashed at rest (see Breaking) and OAuth redirect
|
|
154
|
+
targets are origin-allowlisted (see Breaking).
|
|
155
|
+
- Account pre-hijack defence (pre-hijack pair, review 2026-07-21):
|
|
156
|
+
OAuth account creation and link-by-email now require a
|
|
157
|
+
provider-verified email (Google: OIDC `email_verified`; GitHub:
|
|
158
|
+
verified entry from `/user/emails`), and linking into an unproven-email
|
|
159
|
+
account that has a password is refused with `email_conflict` (403) and
|
|
160
|
+
an `oauth_link_refused` audit event. Previously an attacker could
|
|
161
|
+
pre-register a victim's email with a password and silently gain a
|
|
162
|
+
backdoor into the account the victim later created via OAuth.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
33
166
|
## [0.1.14] — 2026-07-17
|
|
34
167
|
|
|
35
168
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supython
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.16
|
|
4
4
|
Summary: A lightweight Postgres-first BaaS framework for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tkeby/supython
|
|
6
6
|
Project-URL: Repository, https://github.com/Tkeby/supython
|
|
@@ -407,20 +407,29 @@ supython dev
|
|
|
407
407
|
|
|
408
408
|
| Endpoint | Method | Purpose |
|
|
409
409
|
|---|---|---|
|
|
410
|
-
| `/auth/v1/signup` | POST | Create account
|
|
410
|
+
| `/auth/v1/signup` | POST | Create account; returns a token pair, or `202` + confirmation email when `AUTH_REQUIRE_EMAIL_CONFIRMATION` is on |
|
|
411
|
+
| `/auth/v1/confirm/verify` | GET / POST | GET renders a confirm page; POST redeems the signup-confirmation token |
|
|
412
|
+
| `/auth/v1/confirm/resend` | POST | Re-send the signup confirmation email |
|
|
411
413
|
| `/auth/v1/token` | POST | Password login |
|
|
412
414
|
| `/auth/v1/refresh` | POST | Rotate refresh token (reuse detection built in) |
|
|
413
|
-
| `/auth/v1/logout` | POST |
|
|
414
|
-
| `/auth/v1/user` | GET | Return the caller's user (JWT required) |
|
|
415
|
+
| `/auth/v1/logout` | POST | Sign out — `scope`: `local` (default) / `global` / `others` |
|
|
416
|
+
| `/auth/v1/user` | GET | Return the caller's user, incl. `user_metadata` / `app_metadata` (JWT required) |
|
|
417
|
+
| `/auth/v1/user` | PUT | Change password, OR start an email change / merge `data` into `user_metadata` |
|
|
418
|
+
| `/auth/v1/email_change/verify` | GET / POST | Confirm one side of a dual-confirmation email change |
|
|
415
419
|
| `/auth/v1/recover` | POST | Request password-reset email |
|
|
416
|
-
| `/auth/v1/recover/verify` | POST | Verify reset token, set new password |
|
|
420
|
+
| `/auth/v1/recover/verify` | POST | Verify reset token, set new password (revokes all sessions) |
|
|
417
421
|
| `/auth/v1/magiclink` | POST | Request magic-link email |
|
|
418
|
-
| `/auth/v1/magiclink/verify` | GET |
|
|
422
|
+
| `/auth/v1/magiclink/verify` | GET / POST | GET renders a confirm page; POST redeems the magic-link token |
|
|
419
423
|
| `/auth/v1/otp` | POST | Request email OTP |
|
|
420
424
|
| `/auth/v1/otp/verify` | POST | Verify OTP code |
|
|
421
|
-
| `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider) |
|
|
425
|
+
| `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider; `redirect_uri` must be allowlisted) |
|
|
422
426
|
| `/auth/v1/callback/{provider}` | GET | Handle OAuth callback, redirect with tokens |
|
|
423
427
|
|
|
428
|
+
Emailed verify links (signup confirm, magic link, email change) are
|
|
429
|
+
**GET landing pages** that consume nothing — the token is redeemed by the
|
|
430
|
+
form **POST** they submit, so mail-scanner GET prefetch can't burn a token
|
|
431
|
+
or leak a session.
|
|
432
|
+
|
|
424
433
|
**Email backend** — set `EMAIL_BACKEND=console` (default, logs to stdout) or
|
|
425
434
|
`EMAIL_BACKEND=smtp` and configure `SMTP_HOST / SMTP_PORT / SMTP_USERNAME /
|
|
426
435
|
SMTP_PASSWORD`.
|
|
@@ -428,8 +437,10 @@ SMTP_PASSWORD`.
|
|
|
428
437
|
**OAuth** — add `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (and/or GitHub
|
|
429
438
|
equivalents) to `.env`. Providers without credentials are silently disabled.
|
|
430
439
|
|
|
431
|
-
**Logout semantics (stateless JWT)** — `/auth/v1/logout` revokes
|
|
432
|
-
|
|
440
|
+
**Logout semantics (stateless JWT)** — `/auth/v1/logout` revokes **refresh**
|
|
441
|
+
tokens (`scope=local` for this session and its rotated descendants, `global`
|
|
442
|
+
for all, `others` for all but this one); the access token remains valid until
|
|
443
|
+
its `exp` claim. This
|
|
433
444
|
is the standard stateless-JWT posture (same as Supabase Auth, Auth0, Cognito):
|
|
434
445
|
both supython and PostgREST verify access tokens from the JWKS with **no
|
|
435
446
|
per-request DB lookup**, so there is no place to consult a server-side
|
|
@@ -530,6 +541,12 @@ need any claim beyond the user UUID.
|
|
|
530
541
|
| `AUTH_RATE_LIMIT_RECOVER_PER_WINDOW` | `3` | `/auth/v1/recover` attempts per IP/window |
|
|
531
542
|
| `AUTH_RATE_LIMIT_OTP_PER_WINDOW` | `5` | `/auth/v1/otp` attempts per IP/window |
|
|
532
543
|
| `AUTH_RATE_LIMIT_MAGICLINK_PER_WINDOW` | `5` | `/auth/v1/magiclink` attempts per IP/window |
|
|
544
|
+
| `AUTH_RATE_LIMIT_CONFIRM_PER_WINDOW` | `5` | `/auth/v1/confirm/*` attempts per IP/window |
|
|
545
|
+
| `AUTH_REQUIRE_EMAIL_CONFIRMATION` | `false` | Require new signups to confirm their email (signup returns 202 without tokens; sign-in refused with `email_not_confirmed` until `/auth/v1/confirm/verify`) |
|
|
546
|
+
| `OAUTH_REDIRECT_ALLOWLIST` | *(empty)* | Comma-separated origins an OAuth `redirect_uri` may target. Empty fails closed — OAuth sign-in is refused until configured |
|
|
547
|
+
| `TRUSTED_PROXIES` | *(empty)* | Comma-separated IPs/CIDRs of reverse proxies. When the TCP peer is trusted, rate limiting and audit logs use the rightmost untrusted `X-Forwarded-For` hop; empty ignores the header entirely |
|
|
548
|
+
| `EMAIL_CHANGE_TOKEN_TTL` | `3600` | Email-change confirmation token lifetime in seconds (both inboxes must confirm) |
|
|
549
|
+
| `SIGNUP_CONFIRM_TOKEN_TTL` | `86400` | Signup confirmation token lifetime in seconds (24 h) |
|
|
533
550
|
|
|
534
551
|
**`AUTHENTICATOR_PASSWORD`** — the password used for the `authenticator` Postgres
|
|
535
552
|
role that PostgREST connects as. Defaults to `authenticator` (matches the
|
|
@@ -348,20 +348,29 @@ supython dev
|
|
|
348
348
|
|
|
349
349
|
| Endpoint | Method | Purpose |
|
|
350
350
|
|---|---|---|
|
|
351
|
-
| `/auth/v1/signup` | POST | Create account
|
|
351
|
+
| `/auth/v1/signup` | POST | Create account; returns a token pair, or `202` + confirmation email when `AUTH_REQUIRE_EMAIL_CONFIRMATION` is on |
|
|
352
|
+
| `/auth/v1/confirm/verify` | GET / POST | GET renders a confirm page; POST redeems the signup-confirmation token |
|
|
353
|
+
| `/auth/v1/confirm/resend` | POST | Re-send the signup confirmation email |
|
|
352
354
|
| `/auth/v1/token` | POST | Password login |
|
|
353
355
|
| `/auth/v1/refresh` | POST | Rotate refresh token (reuse detection built in) |
|
|
354
|
-
| `/auth/v1/logout` | POST |
|
|
355
|
-
| `/auth/v1/user` | GET | Return the caller's user (JWT required) |
|
|
356
|
+
| `/auth/v1/logout` | POST | Sign out — `scope`: `local` (default) / `global` / `others` |
|
|
357
|
+
| `/auth/v1/user` | GET | Return the caller's user, incl. `user_metadata` / `app_metadata` (JWT required) |
|
|
358
|
+
| `/auth/v1/user` | PUT | Change password, OR start an email change / merge `data` into `user_metadata` |
|
|
359
|
+
| `/auth/v1/email_change/verify` | GET / POST | Confirm one side of a dual-confirmation email change |
|
|
356
360
|
| `/auth/v1/recover` | POST | Request password-reset email |
|
|
357
|
-
| `/auth/v1/recover/verify` | POST | Verify reset token, set new password |
|
|
361
|
+
| `/auth/v1/recover/verify` | POST | Verify reset token, set new password (revokes all sessions) |
|
|
358
362
|
| `/auth/v1/magiclink` | POST | Request magic-link email |
|
|
359
|
-
| `/auth/v1/magiclink/verify` | GET |
|
|
363
|
+
| `/auth/v1/magiclink/verify` | GET / POST | GET renders a confirm page; POST redeems the magic-link token |
|
|
360
364
|
| `/auth/v1/otp` | POST | Request email OTP |
|
|
361
365
|
| `/auth/v1/otp/verify` | POST | Verify OTP code |
|
|
362
|
-
| `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider) |
|
|
366
|
+
| `/auth/v1/authorize/{provider}` | GET | Start OAuth flow (redirect to provider; `redirect_uri` must be allowlisted) |
|
|
363
367
|
| `/auth/v1/callback/{provider}` | GET | Handle OAuth callback, redirect with tokens |
|
|
364
368
|
|
|
369
|
+
Emailed verify links (signup confirm, magic link, email change) are
|
|
370
|
+
**GET landing pages** that consume nothing — the token is redeemed by the
|
|
371
|
+
form **POST** they submit, so mail-scanner GET prefetch can't burn a token
|
|
372
|
+
or leak a session.
|
|
373
|
+
|
|
365
374
|
**Email backend** — set `EMAIL_BACKEND=console` (default, logs to stdout) or
|
|
366
375
|
`EMAIL_BACKEND=smtp` and configure `SMTP_HOST / SMTP_PORT / SMTP_USERNAME /
|
|
367
376
|
SMTP_PASSWORD`.
|
|
@@ -369,8 +378,10 @@ SMTP_PASSWORD`.
|
|
|
369
378
|
**OAuth** — add `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` (and/or GitHub
|
|
370
379
|
equivalents) to `.env`. Providers without credentials are silently disabled.
|
|
371
380
|
|
|
372
|
-
**Logout semantics (stateless JWT)** — `/auth/v1/logout` revokes
|
|
373
|
-
|
|
381
|
+
**Logout semantics (stateless JWT)** — `/auth/v1/logout` revokes **refresh**
|
|
382
|
+
tokens (`scope=local` for this session and its rotated descendants, `global`
|
|
383
|
+
for all, `others` for all but this one); the access token remains valid until
|
|
384
|
+
its `exp` claim. This
|
|
374
385
|
is the standard stateless-JWT posture (same as Supabase Auth, Auth0, Cognito):
|
|
375
386
|
both supython and PostgREST verify access tokens from the JWKS with **no
|
|
376
387
|
per-request DB lookup**, so there is no place to consult a server-side
|
|
@@ -471,6 +482,12 @@ need any claim beyond the user UUID.
|
|
|
471
482
|
| `AUTH_RATE_LIMIT_RECOVER_PER_WINDOW` | `3` | `/auth/v1/recover` attempts per IP/window |
|
|
472
483
|
| `AUTH_RATE_LIMIT_OTP_PER_WINDOW` | `5` | `/auth/v1/otp` attempts per IP/window |
|
|
473
484
|
| `AUTH_RATE_LIMIT_MAGICLINK_PER_WINDOW` | `5` | `/auth/v1/magiclink` attempts per IP/window |
|
|
485
|
+
| `AUTH_RATE_LIMIT_CONFIRM_PER_WINDOW` | `5` | `/auth/v1/confirm/*` attempts per IP/window |
|
|
486
|
+
| `AUTH_REQUIRE_EMAIL_CONFIRMATION` | `false` | Require new signups to confirm their email (signup returns 202 without tokens; sign-in refused with `email_not_confirmed` until `/auth/v1/confirm/verify`) |
|
|
487
|
+
| `OAUTH_REDIRECT_ALLOWLIST` | *(empty)* | Comma-separated origins an OAuth `redirect_uri` may target. Empty fails closed — OAuth sign-in is refused until configured |
|
|
488
|
+
| `TRUSTED_PROXIES` | *(empty)* | Comma-separated IPs/CIDRs of reverse proxies. When the TCP peer is trusted, rate limiting and audit logs use the rightmost untrusted `X-Forwarded-For` hop; empty ignores the header entirely |
|
|
489
|
+
| `EMAIL_CHANGE_TOKEN_TTL` | `3600` | Email-change confirmation token lifetime in seconds (both inboxes must confirm) |
|
|
490
|
+
| `SIGNUP_CONFIRM_TOKEN_TTL` | `86400` | Signup confirmation token lifetime in seconds (24 h) |
|
|
474
491
|
|
|
475
492
|
**`AUTHENTICATOR_PASSWORD`** — the password used for the `authenticator` Postgres
|
|
476
493
|
role that PostgREST connects as. Defaults to `authenticator` (matches the
|
|
@@ -7,10 +7,17 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
9
9
|
class ProviderProfile:
|
|
10
|
-
"""Normalised identity returned by any OAuth provider.
|
|
10
|
+
"""Normalised identity returned by any OAuth provider.
|
|
11
|
+
|
|
12
|
+
``email_verified`` must be True only when the provider itself vouches for
|
|
13
|
+
ownership of ``email``. It defaults to False so a provider that never sets
|
|
14
|
+
it fails closed: the auth service refuses to create or link an account by
|
|
15
|
+
email on an unverified profile (pre-hijack defence).
|
|
16
|
+
"""
|
|
11
17
|
|
|
12
18
|
provider_user_id: str
|
|
13
19
|
email: str
|
|
20
|
+
email_verified: bool = False
|
|
14
21
|
raw: dict[str, Any] = field(default_factory=dict)
|
|
15
22
|
|
|
16
23
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""GitHub OAuth2 provider."""
|
|
2
|
+
|
|
3
|
+
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
|
4
|
+
|
|
5
|
+
from . import ProviderProfile
|
|
6
|
+
from .oauth import OAuthProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitHubProvider(OAuthProvider):
|
|
10
|
+
name = "github"
|
|
11
|
+
AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
|
|
12
|
+
# GitHub requires Accept: application/json to return JSON (not form-encoded).
|
|
13
|
+
TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
14
|
+
USERINFO_URL = "https://api.github.com/user"
|
|
15
|
+
EMAILS_URL = "https://api.github.com/user/emails"
|
|
16
|
+
DEFAULT_SCOPE = "read:user user:email"
|
|
17
|
+
|
|
18
|
+
async def _build_profile(
|
|
19
|
+
self, client: AsyncOAuth2Client, data: dict
|
|
20
|
+
) -> ProviderProfile:
|
|
21
|
+
# The public profile email on /user is optional and carries no
|
|
22
|
+
# verified flag, so it must not be trusted for account linking.
|
|
23
|
+
# /user/emails (covered by the user:email scope) lists every address
|
|
24
|
+
# with primary/verified bits; only a verified one may be used.
|
|
25
|
+
email, verified = "", False
|
|
26
|
+
resp = await client.get(self.EMAILS_URL)
|
|
27
|
+
if resp.status_code == 200:
|
|
28
|
+
entries = [e for e in resp.json() if e.get("verified")]
|
|
29
|
+
primary = [e for e in entries if e.get("primary")]
|
|
30
|
+
chosen = primary[0] if primary else (entries[0] if entries else None)
|
|
31
|
+
if chosen:
|
|
32
|
+
email, verified = chosen["email"], True
|
|
33
|
+
# Fall back to the public profile email for display only — the
|
|
34
|
+
# service layer refuses email matching when verified is False.
|
|
35
|
+
if not email:
|
|
36
|
+
email = data.get("email") or ""
|
|
37
|
+
return ProviderProfile(
|
|
38
|
+
provider_user_id=str(data["id"]),
|
|
39
|
+
email=email,
|
|
40
|
+
email_verified=verified,
|
|
41
|
+
raw=data,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
|
|
45
|
+
return ProviderProfile(
|
|
46
|
+
provider_user_id=str(data["id"]),
|
|
47
|
+
email=data.get("email") or "",
|
|
48
|
+
email_verified=False,
|
|
49
|
+
raw=data,
|
|
50
|
+
)
|
|
@@ -12,8 +12,12 @@ class GoogleProvider(OAuthProvider):
|
|
|
12
12
|
DEFAULT_SCOPE = "openid email profile"
|
|
13
13
|
|
|
14
14
|
def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
|
|
15
|
+
# OIDC userinfo carries `email_verified`; Google may serve it as a
|
|
16
|
+
# bool or the string "true" depending on endpoint version.
|
|
17
|
+
verified = data.get("email_verified")
|
|
15
18
|
return ProviderProfile(
|
|
16
19
|
provider_user_id=data["sub"],
|
|
17
20
|
email=data.get("email", ""),
|
|
21
|
+
email_verified=verified is True or verified == "true",
|
|
18
22
|
raw=data,
|
|
19
23
|
)
|
|
@@ -50,6 +50,17 @@ class OAuthProvider(Provider):
|
|
|
50
50
|
resp = await client.get(self.USERINFO_URL)
|
|
51
51
|
resp.raise_for_status()
|
|
52
52
|
data: dict = resp.json()
|
|
53
|
+
return await self._build_profile(client, data)
|
|
54
|
+
|
|
55
|
+
async def _build_profile(
|
|
56
|
+
self, client: AsyncOAuth2Client, data: dict
|
|
57
|
+
) -> ProviderProfile:
|
|
58
|
+
"""Turn the userinfo payload into a profile.
|
|
59
|
+
|
|
60
|
+
Runs while the authenticated ``client`` is still open so providers that
|
|
61
|
+
need extra API calls (e.g. GitHub's /user/emails) can override this
|
|
62
|
+
instead of re-implementing ``exchange``.
|
|
63
|
+
"""
|
|
53
64
|
return self._profile_from_userinfo(data)
|
|
54
65
|
|
|
55
66
|
def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
|