supython 0.1.12__tar.gz → 0.1.13__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.12 → supython-0.1.13}/CHANGELOG.md +28 -0
- {supython-0.1.12 → supython-0.1.13}/PKG-INFO +1 -1
- {supython-0.1.12 → supython-0.1.13}/pyproject.toml +1 -1
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/router.py +48 -8
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/schemas.py +13 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/service.py +88 -10
- supython-0.1.13/src/supython/migrations/0016_magic_link_redirect.sql +15 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/settings.py +13 -0
- {supython-0.1.12 → supython-0.1.13}/.gitignore +0 -0
- {supython-0.1.12 → supython-0.1.13}/LICENSE +0 -0
- {supython-0.1.12 → supython-0.1.13}/README.md +0 -0
- {supython-0.1.12 → supython-0.1.13}/SECURITY.md +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/auth.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/auth_templates.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/auth_users.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/db.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/functions.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/jobs.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/ops.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/realtime.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_auth.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_auth_templates.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_auth_users.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_db.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_functions.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_jobs.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_ops.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_realtime.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/service_storage.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/storage.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/api/system.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/audit.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/deps.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/errors.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/schemas.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/session.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/spa.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Alert-dluGVkos.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Alert-dluGVkos.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Audit-Njung3HI.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Audit-Njung3HI.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Backups-DzPlFgrm.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Backups-DzPlFgrm.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Buckets-ByacGkU1.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Buckets-ByacGkU1.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Channels-BoIuTtam.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Crons-B67vc39F.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Crons-B67vc39F.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DataTable-COAAWEft.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Empty-cr2r7e2u.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Empty-cr2r7e2u.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Grid-hFkp9F4P.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Grid-hFkp9F4P.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Input-DppYTq9C.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Input-DppYTq9C.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Invoke-DW3Nveeh.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/JsonField-DibyJgun.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/JsonField-DibyJgun.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/LoginView-BjLyE3Ds.css +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/LoginView-CoOjECT_.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/LoginView-CoOjECT_.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Logs-D9WYrnIT.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Logs-D9WYrnIT.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Logs-DS1XPa0h.css +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Queue-CywZs6vI.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Queue-CywZs6vI.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Routes-BiLXE49D.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Routes-BiLXE49D.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Routes-C-ianIGD.css +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Select-DIzZyRZb.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Select-DIzZyRZb.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Space-n5-XcguU.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Space-n5-XcguU.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/TableData-CQIagLKn.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/TableData-CQIagLKn.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Tag-D1fOKpTH.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Tag-D1fOKpTH.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Templates-BS-ugkdq.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Templates-BS-ugkdq.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Thing-CEAniuMg.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Thing-CEAniuMg.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Users-wzwajhlh.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Users-wzwajhlh.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/dist-VXIJLCYq.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/dist-VXIJLCYq.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/format-length-CGCY1rMh.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/format-length-CGCY1rMh.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/get-Ca6unauB.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/get-Ca6unauB.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/index-CeE6v959.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/index-CeE6v959.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/pinia-COXwfrOX.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/pinia-COXwfrOX.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/resources-Bt6thQCD.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/resources-Bt6thQCD.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/use-locale-mtgM0a3a.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/use-merged-state-BvhkaHNX.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useConfirm-tMjvBFXR.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useResource-C_rJCY8C.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useResource-C_rJCY8C.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useTable-CnZc5zhi.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useTable-CnZc5zhi.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useTable-Dg0XlRlq.css +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useToast-DsZKx0IX.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/useToast-DsZKx0IX.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/utils-sbXoq7Ir.js +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/utils-sbXoq7Ir.js.map +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/favicon.svg +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/icons.svg +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/index.html +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/app.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/_email_job.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/claims.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/deps.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/github.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/google.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/oauth.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/providers/registry.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/auth/ratelimit.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/backups/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/backups/_backup_job.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/backups/schemas.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/backups/service.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/body_size.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/cli.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/client/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/client/_auth.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/client/_client.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/client/_config.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/client/_functions.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/client/_storage.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/client/py.typed +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/db.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/db_admin.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/extensions.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/functions/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/functions/context.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/functions/loader.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/functions/router.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/functions/schemas.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/gen/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/gen/_introspect.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/gen/types_py.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/gen/types_ts.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/health.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/hooks.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/backends.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/context.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/cron.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/cron_inproc.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/decorators.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/registry.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/router.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/schemas.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/service.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jobs/worker.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/jwks.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/keyset.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/logging_config.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/mail.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/mailer.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrate.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0001_extensions_and_roles.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0002_auth_schema.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0004_auth_v0_2.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0005_storage_schema.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0006_realtime_schema.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0007_jobs_schema.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0008_jobs_last_error.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0009_auth_rate_limits.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0010_worker_heartbeat.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0011_admin_schema.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0012_auth_banned_until.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0013_email_templates.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0014_realtime_payload_warning.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/migrations/0015_backups_schema.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/passwords.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/broker.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/protocol.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/router.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/schemas.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/service.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/topics.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/realtime/websocket.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/init_project.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/Caddyfile.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/README.md.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/apps_hooks_welcome.py.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/apps_jobs_example.py.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/asgi.py.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/docker-compose.prod.yml.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/docker-compose.yml.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/env.example.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/functions_README.md.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/gitignore.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/manage.py.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/package_hooks_init.py.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/package_init.py.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/package_jobs_init.py.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/pyproject.toml.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/settings.py.tmpl +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/static/0001_create_todos.sql +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/scaffold/templates/static/functions_hello.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/secretset.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/security_headers.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/settings_module.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/storage/__init__.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/storage/backends.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/storage/router.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/storage/schemas.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/storage/service.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/storage/signing.py +0 -0
- {supython-0.1.12 → supython-0.1.13}/src/supython/tokens.py +0 -0
|
@@ -30,6 +30,33 @@ Each entry links the relevant `PROJECT.md` section and decision-log row
|
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
+
## [0.1.13] — 2026-07-16
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- `POST /auth/v1/magiclink` accepts an optional `redirect_url`: when its origin
|
|
37
|
+
is in the new `MAGIC_LINK_REDIRECT_ALLOWLIST` setting, `GET
|
|
38
|
+
/auth/v1/magiclink/verify` 302-redirects the browser to it with the token
|
|
39
|
+
pair in the fragment (the same shape as the OAuth callback), instead of
|
|
40
|
+
returning JSON. Omitting `redirect_url` keeps the existing JSON response.
|
|
41
|
+
Lets a consumer send a magic link straight to its own SPA route (e.g. an
|
|
42
|
+
invite-accept page) rather than the API's bare verify endpoint.
|
|
43
|
+
- `POST /auth/v1/magiclink` accepts an optional `ttl` (seconds), clamped to
|
|
44
|
+
`[60, MAGIC_LINK_MAX_TTL]` (new setting, default 7 days). Lets a single
|
|
45
|
+
caller mint a longer-lived link (e.g. a multi-day operator invite) without
|
|
46
|
+
moving `MAGIC_LINK_TOKEN_TTL`, which every other magic-link request still
|
|
47
|
+
uses unchanged.
|
|
48
|
+
- Migration `0016_magic_link_redirect.sql`: nullable `auth.one_time_tokens.redirect_url`.
|
|
49
|
+
|
|
50
|
+
### Security
|
|
51
|
+
- `redirect_url` is validated against an explicit origin allowlist before any
|
|
52
|
+
email is sent (empty allowlist ⇒ every redirect is rejected, fail-closed);
|
|
53
|
+
a non-allowlisted origin, embedded credentials (`user:pass@host`), or a
|
|
54
|
+
non-http(s) scheme all reject the request with `400 invalid_redirect`. The
|
|
55
|
+
target is stored server-side on the one-time token, not echoed through the
|
|
56
|
+
emailed link, so an intercepted link can't be repointed at a different origin.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
33
60
|
## [0.1.12] — 2026-07-01
|
|
34
61
|
|
|
35
62
|
### Fixed
|
|
@@ -318,6 +345,7 @@ v0.1–v0.7 plus a v1.1.x admin track; see §19 decision log
|
|
|
318
345
|
---
|
|
319
346
|
|
|
320
347
|
|
|
348
|
+
[0.1.13]: https://github.com/Tkeby/supython/releases/tag/v0.1.13
|
|
321
349
|
[0.1.12]: https://github.com/Tkeby/supython/releases/tag/v0.1.12
|
|
322
350
|
[0.1.11]: https://github.com/Tkeby/supython/releases/tag/v0.1.11
|
|
323
351
|
[0.1.10]: https://github.com/Tkeby/supython/releases/tag/v0.1.10
|
|
@@ -2,8 +2,18 @@ from typing import Annotated
|
|
|
2
2
|
from uuid import UUID
|
|
3
3
|
|
|
4
4
|
import jwt
|
|
5
|
-
from fastapi import
|
|
6
|
-
|
|
5
|
+
from fastapi import (
|
|
6
|
+
APIRouter,
|
|
7
|
+
Depends,
|
|
8
|
+
Header,
|
|
9
|
+
HTTPException,
|
|
10
|
+
Query,
|
|
11
|
+
Request,
|
|
12
|
+
Response,
|
|
13
|
+
status,
|
|
14
|
+
)
|
|
15
|
+
from fastapi.encoders import jsonable_encoder
|
|
16
|
+
from fastapi.responses import JSONResponse, RedirectResponse
|
|
7
17
|
|
|
8
18
|
from .. import db, tokens
|
|
9
19
|
from ..settings import get_settings
|
|
@@ -198,24 +208,54 @@ async def verify_recover(payload: RecoverVerifyRequest, request: Request) -> Tok
|
|
|
198
208
|
|
|
199
209
|
@router.post("/magiclink", status_code=202, dependencies=[Depends(_rl_magiclink)])
|
|
200
210
|
async def request_magic_link(payload: MagicLinkRequest) -> None:
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
try:
|
|
212
|
+
async with db.acquire() as conn:
|
|
213
|
+
await service.request_magic_link(
|
|
214
|
+
conn,
|
|
215
|
+
payload.email,
|
|
216
|
+
redirect_url=payload.redirect_url,
|
|
217
|
+
ttl=payload.ttl,
|
|
218
|
+
)
|
|
219
|
+
except service.AuthError as exc:
|
|
220
|
+
# Currently only invalid_redirect (400) — a bad email is filtered by
|
|
221
|
+
# EmailStr at the schema layer before this handler ever runs.
|
|
222
|
+
raise _auth_error(exc) from exc
|
|
203
223
|
|
|
204
224
|
|
|
205
225
|
@router.get(
|
|
206
226
|
"/magiclink/verify",
|
|
207
|
-
response_model=TokenResponse,
|
|
208
227
|
dependencies=[Depends(_rl_magiclink)],
|
|
209
228
|
)
|
|
210
229
|
async def verify_magic_link(
|
|
211
230
|
token: Annotated[str, Query(description="Raw magic-link token from the email")],
|
|
212
|
-
) ->
|
|
231
|
+
) -> Response:
|
|
232
|
+
"""Verify a magic-link token.
|
|
233
|
+
|
|
234
|
+
Legacy/JSON callers (no ``redirect_url`` at request time) get the same
|
|
235
|
+
``TokenResponse`` body as always. A caller that requested a redirect gets a
|
|
236
|
+
302 to that URL with the token pair in the fragment — the same shape as the
|
|
237
|
+
OAuth callback (`oauth_callback` below) — so a browser landing here from an
|
|
238
|
+
emailed link ends up on the SPA's own page with a session, not looking at
|
|
239
|
+
raw JSON.
|
|
240
|
+
"""
|
|
213
241
|
try:
|
|
214
242
|
async with db.acquire() as conn:
|
|
215
|
-
|
|
243
|
+
user, access, refresh, ttl, redirect_url = await service.verify_magic_link(
|
|
244
|
+
conn, token
|
|
245
|
+
)
|
|
216
246
|
except service.AuthError as exc:
|
|
217
247
|
raise _auth_error(exc) from exc
|
|
218
|
-
|
|
248
|
+
if redirect_url:
|
|
249
|
+
fragment = (
|
|
250
|
+
f"access_token={access}"
|
|
251
|
+
f"&refresh_token={refresh}"
|
|
252
|
+
f"&expires_in={ttl}"
|
|
253
|
+
f"&token_type=bearer"
|
|
254
|
+
)
|
|
255
|
+
return RedirectResponse(f"{redirect_url}#{fragment}", status_code=302)
|
|
256
|
+
return JSONResponse(
|
|
257
|
+
jsonable_encoder(_to_token_response(user, access, refresh, ttl))
|
|
258
|
+
)
|
|
219
259
|
|
|
220
260
|
|
|
221
261
|
@router.post("/otp", status_code=202, dependencies=[Depends(_rl_otp)])
|
|
@@ -17,6 +17,8 @@ MAX_PASSWORD_LEN = 128
|
|
|
17
17
|
# chars in practice). 4096 keeps a generous head-room for any future
|
|
18
18
|
# encoding while still rejecting "1 MB token" abuse.
|
|
19
19
|
MAX_TOKEN_LEN = 4096
|
|
20
|
+
# Redirect URLs are ordinary web URLs; 2048 is the de-facto browser cap.
|
|
21
|
+
MAX_REDIRECT_URL_LEN = 2048
|
|
20
22
|
|
|
21
23
|
EmailField = Annotated[EmailStr, Field(max_length=MAX_EMAIL_LEN)]
|
|
22
24
|
PasswordField = Annotated[
|
|
@@ -68,6 +70,17 @@ class RecoverVerifyRequest(BaseModel):
|
|
|
68
70
|
|
|
69
71
|
class MagicLinkRequest(BaseModel):
|
|
70
72
|
email: EmailField
|
|
73
|
+
# Where /auth/v1/magiclink/verify should send the browser after issuing
|
|
74
|
+
# tokens: a 302 to this URL with the token pair in the fragment (as OAuth
|
|
75
|
+
# does), instead of a JSON response. Must match MAGIC_LINK_REDIRECT_
|
|
76
|
+
# ALLOWLIST or the request is rejected 400. Omit for the JSON flow.
|
|
77
|
+
redirect_url: Annotated[
|
|
78
|
+
str | None, Field(default=None, max_length=MAX_REDIRECT_URL_LEN)
|
|
79
|
+
] = None
|
|
80
|
+
# Per-request token lifetime in seconds, clamped to [60, MAGIC_LINK_MAX_TTL].
|
|
81
|
+
# Omit to use MAGIC_LINK_TOKEN_TTL. Lets a caller mint a longer-lived link
|
|
82
|
+
# (e.g. an operator invite) without changing the global default.
|
|
83
|
+
ttl: Annotated[int | None, Field(default=None, ge=1)] = None
|
|
71
84
|
|
|
72
85
|
|
|
73
86
|
class OtpRequest(BaseModel):
|
|
@@ -6,6 +6,7 @@ import logging
|
|
|
6
6
|
import secrets
|
|
7
7
|
from datetime import UTC, datetime, timedelta
|
|
8
8
|
from typing import Any
|
|
9
|
+
from urllib.parse import urlsplit
|
|
9
10
|
from uuid import UUID
|
|
10
11
|
|
|
11
12
|
import asyncpg
|
|
@@ -82,19 +83,26 @@ async def _store_one_time_token(
|
|
|
82
83
|
user_id: UUID,
|
|
83
84
|
token_type: str,
|
|
84
85
|
ttl_seconds: int,
|
|
86
|
+
*,
|
|
87
|
+
redirect_url: str | None = None,
|
|
85
88
|
) -> str:
|
|
86
|
-
"""Generate, sha256-hash, and store a one-time token. Returns the raw token.
|
|
89
|
+
"""Generate, sha256-hash, and store a one-time token. Returns the raw token.
|
|
90
|
+
|
|
91
|
+
``redirect_url`` is stored only for magic-link tokens (recover/otp pass
|
|
92
|
+
None ⇒ NULL); verify reads it back to decide between a 302 and JSON.
|
|
93
|
+
"""
|
|
87
94
|
raw = secrets.token_urlsafe(32)
|
|
88
95
|
expires_at = datetime.now(UTC) + timedelta(seconds=ttl_seconds)
|
|
89
96
|
await conn.execute(
|
|
90
97
|
"""
|
|
91
|
-
insert into auth.one_time_tokens (user_id, type, token_hash, expires_at)
|
|
92
|
-
values ($1, $2, $3, $4)
|
|
98
|
+
insert into auth.one_time_tokens (user_id, type, token_hash, expires_at, redirect_url)
|
|
99
|
+
values ($1, $2, $3, $4, $5)
|
|
93
100
|
""",
|
|
94
101
|
user_id,
|
|
95
102
|
token_type,
|
|
96
103
|
_sha256(raw),
|
|
97
104
|
expires_at,
|
|
105
|
+
redirect_url,
|
|
98
106
|
)
|
|
99
107
|
return raw
|
|
100
108
|
|
|
@@ -110,7 +118,7 @@ async def _verify_one_time_token(
|
|
|
110
118
|
if email is not None:
|
|
111
119
|
row = await conn.fetchrow(
|
|
112
120
|
"""
|
|
113
|
-
select ott.id as ott_id, u.id, u.email, u.created_at
|
|
121
|
+
select ott.id as ott_id, ott.redirect_url, u.id, u.email, u.created_at
|
|
114
122
|
from auth.one_time_tokens ott
|
|
115
123
|
join auth.users u on u.id = ott.user_id
|
|
116
124
|
where ott.token_hash = $1
|
|
@@ -126,7 +134,7 @@ async def _verify_one_time_token(
|
|
|
126
134
|
else:
|
|
127
135
|
row = await conn.fetchrow(
|
|
128
136
|
"""
|
|
129
|
-
select ott.id as ott_id, u.id, u.email, u.created_at
|
|
137
|
+
select ott.id as ott_id, ott.redirect_url, u.id, u.email, u.created_at
|
|
130
138
|
from auth.one_time_tokens ott
|
|
131
139
|
join auth.users u on u.id = ott.user_id
|
|
132
140
|
where ott.token_hash = $1
|
|
@@ -354,17 +362,85 @@ async def verify_recover(
|
|
|
354
362
|
return user, access, refresh, ttl
|
|
355
363
|
|
|
356
364
|
|
|
365
|
+
def _url_origin(url: str) -> str | None:
|
|
366
|
+
"""Return the ``scheme://host[:port]`` origin of an absolute http(s) URL.
|
|
367
|
+
|
|
368
|
+
Returns None for anything that isn't a well-formed absolute http(s) URL, or
|
|
369
|
+
that carries embedded credentials (``user:pass@host``) — those have no place
|
|
370
|
+
in a redirect target and are a classic obfuscation vector. Origins are
|
|
371
|
+
lower-cased so comparison is case-insensitive on scheme/host.
|
|
372
|
+
"""
|
|
373
|
+
try:
|
|
374
|
+
parts = urlsplit(url)
|
|
375
|
+
except ValueError:
|
|
376
|
+
return None
|
|
377
|
+
if parts.scheme not in ("http", "https") or not parts.netloc or "@" in parts.netloc:
|
|
378
|
+
return None
|
|
379
|
+
return f"{parts.scheme}://{parts.netloc}".lower()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def validate_magic_link_redirect(redirect_url: str, allowlist_csv: str) -> str:
|
|
383
|
+
"""Return ``redirect_url`` when its origin is allowlisted, else raise.
|
|
384
|
+
|
|
385
|
+
``allowlist_csv`` is the comma-separated ``MAGIC_LINK_REDIRECT_ALLOWLIST``
|
|
386
|
+
setting. An empty allowlist rejects every redirect (the feature is off and
|
|
387
|
+
fails closed). Matching is by origin only, so any path/query/fragment under
|
|
388
|
+
an allowlisted origin is accepted. Raises ``AuthError('invalid_redirect')``
|
|
389
|
+
(400) on any miss — the caller turns that into an HTTP 400.
|
|
390
|
+
"""
|
|
391
|
+
origin = _url_origin(redirect_url)
|
|
392
|
+
if origin is None:
|
|
393
|
+
raise AuthError(
|
|
394
|
+
"invalid_redirect", "redirect_url must be an absolute http(s) URL", 400
|
|
395
|
+
)
|
|
396
|
+
allowed = {
|
|
397
|
+
o for entry in allowlist_csv.split(",") if (o := _url_origin(entry.strip()))
|
|
398
|
+
}
|
|
399
|
+
if origin not in allowed:
|
|
400
|
+
raise AuthError(
|
|
401
|
+
"invalid_redirect", "redirect_url origin is not allowlisted", 400
|
|
402
|
+
)
|
|
403
|
+
return redirect_url
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _clamp_magic_link_ttl(ttl: int | None, settings: Any) -> int:
|
|
407
|
+
"""Resolve the effective magic-link TTL in seconds.
|
|
408
|
+
|
|
409
|
+
``None`` ⇒ the ``magic_link_token_ttl`` default. A supplied value is clamped
|
|
410
|
+
to ``[60, magic_link_max_ttl]`` so a caller can neither mint a link that
|
|
411
|
+
expires too fast to click nor one that outlives the configured ceiling.
|
|
412
|
+
"""
|
|
413
|
+
if ttl is None:
|
|
414
|
+
return settings.magic_link_token_ttl
|
|
415
|
+
return max(60, min(ttl, settings.magic_link_max_ttl))
|
|
416
|
+
|
|
417
|
+
|
|
357
418
|
async def request_magic_link(
|
|
358
|
-
conn: asyncpg.Connection,
|
|
419
|
+
conn: asyncpg.Connection,
|
|
420
|
+
email: str,
|
|
421
|
+
*,
|
|
422
|
+
redirect_url: str | None = None,
|
|
423
|
+
ttl: int | None = None,
|
|
359
424
|
) -> None:
|
|
425
|
+
s = get_settings()
|
|
426
|
+
# Validate the redirect before the user lookup: a bad redirect is a client
|
|
427
|
+
# error regardless of whether the email exists, and checking it here keeps
|
|
428
|
+
# the email-enumeration silence below intact (unknown emails still 202).
|
|
429
|
+
if redirect_url is not None:
|
|
430
|
+
validate_magic_link_redirect(redirect_url, s.magic_link_redirect_allowlist)
|
|
360
431
|
row = await conn.fetchrow(
|
|
361
432
|
"select id from auth.users where email = $1", email
|
|
362
433
|
)
|
|
363
434
|
if not row:
|
|
364
435
|
return
|
|
365
|
-
s = get_settings()
|
|
366
436
|
async with conn.transaction():
|
|
367
|
-
raw = await _store_one_time_token(
|
|
437
|
+
raw = await _store_one_time_token(
|
|
438
|
+
conn,
|
|
439
|
+
row["id"],
|
|
440
|
+
"magic_link",
|
|
441
|
+
_clamp_magic_link_ttl(ttl, s),
|
|
442
|
+
redirect_url=redirect_url,
|
|
443
|
+
)
|
|
368
444
|
verify_url = f"{s.site_url}/auth/v1/magiclink/verify?token={raw}"
|
|
369
445
|
await mail.dispatch(
|
|
370
446
|
conn,
|
|
@@ -379,7 +455,7 @@ async def request_magic_link(
|
|
|
379
455
|
|
|
380
456
|
async def verify_magic_link(
|
|
381
457
|
conn: asyncpg.Connection, token: str
|
|
382
|
-
) -> tuple[UserResponse, str, str, int]:
|
|
458
|
+
) -> tuple[UserResponse, str, str, int, str | None]:
|
|
383
459
|
row = await _verify_one_time_token(conn, token, "magic_link")
|
|
384
460
|
user = _row_to_user(row)
|
|
385
461
|
async with conn.transaction():
|
|
@@ -388,7 +464,9 @@ async def verify_magic_link(
|
|
|
388
464
|
row["ott_id"],
|
|
389
465
|
)
|
|
390
466
|
access, refresh, ttl = await _issue_pair(conn, user)
|
|
391
|
-
|
|
467
|
+
# redirect_url is None for legacy/JSON callers; the router 302-redirects
|
|
468
|
+
# (OAuth-style, tokens in the fragment) only when it is set.
|
|
469
|
+
return user, access, refresh, ttl, row["redirect_url"]
|
|
392
470
|
|
|
393
471
|
|
|
394
472
|
async def request_otp(
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- Magic-link redirect target.
|
|
2
|
+
--
|
|
3
|
+
-- When a POST /auth/v1/magiclink request carries a redirect_url (validated
|
|
4
|
+
-- against MAGIC_LINK_REDIRECT_ALLOWLIST at request time), it is stored on the
|
|
5
|
+
-- one-time token here. GET /auth/v1/magiclink/verify then 302-redirects the
|
|
6
|
+
-- browser to that SPA URL with the token pair in the fragment — mirroring the
|
|
7
|
+
-- OAuth callback — instead of returning JSON. Storing the target server-side
|
|
8
|
+
-- (rather than echoing it in the emailed link) means an attacker who intercepts
|
|
9
|
+
-- the link cannot repoint it at a token-stealing origin.
|
|
10
|
+
--
|
|
11
|
+
-- Null ⇒ legacy behaviour: verify returns a JSON TokenResponse. Backfill-safe
|
|
12
|
+
-- (nullable, no default), so existing rows keep working.
|
|
13
|
+
|
|
14
|
+
alter table auth.one_time_tokens
|
|
15
|
+
add column if not exists redirect_url text;
|
|
@@ -82,6 +82,19 @@ class Settings(BaseSettings):
|
|
|
82
82
|
recover_token_ttl: int = 3600
|
|
83
83
|
magic_link_token_ttl: int = 15 * 60
|
|
84
84
|
otp_token_ttl: int = 10 * 60
|
|
85
|
+
|
|
86
|
+
# Ceiling for a per-request magic-link lifetime. A POST /auth/v1/magiclink
|
|
87
|
+
# may pass its own `ttl` (e.g. a multi-day operator invite) without moving
|
|
88
|
+
# the magic_link_token_ttl default that ordinary sign-in links use; the
|
|
89
|
+
# requested value is clamped to [60, magic_link_max_ttl].
|
|
90
|
+
magic_link_max_ttl: int = 60 * 60 * 24 * 7 # 7 days
|
|
91
|
+
|
|
92
|
+
# Comma-separated origins (scheme://host[:port]) a magic-link `redirect_url`
|
|
93
|
+
# may target. Empty ⇒ redirects are disabled and verify keeps returning
|
|
94
|
+
# JSON. A redirect_url whose origin is not listed is rejected 400 at request
|
|
95
|
+
# time, so an attacker can't aim a victim's emailed link at a token-stealing
|
|
96
|
+
# site. Example: MAGIC_LINK_REDIRECT_ALLOWLIST=https://app.example.com,http://localhost:5173
|
|
97
|
+
magic_link_redirect_allowlist: str = ""
|
|
85
98
|
auth_rate_limit_enabled: bool = True
|
|
86
99
|
auth_rate_limit_window_seconds: int = 60
|
|
87
100
|
auth_rate_limit_token_per_window: int = 10
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Backups-DzPlFgrm.js.map
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Buckets-ByacGkU1.js.map
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Channels-BoIuTtam.js.map
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ChevronRight-CtQH1EQ1.js.map
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/CodeViewer-Bqy7-wvH.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DashboardView-CUTFVL6k.js.map
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DataTable-COAAWEft.js.map
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/DrawerContent-TpYTFgF1.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/EmptyState-DeDck-OL.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/JsonField-DibyJgun.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/LoginView-CoOjECT_.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/Migrations-DOSC2ddQ.js.map
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RefreshTokens-Ccjr53jg.js.map
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/RlsEditor-BSlH9vSc.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js.map
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlEditor-b8pTsILY.js.map
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js
RENAMED
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/SqlWorkspace-BUS7IntH.js.map
RENAMED
|
File without changes
|
|
File without changes
|
{supython-0.1.12 → supython-0.1.13}/src/supython/admin/static/assets/TableData-CQIagLKn.js.map
RENAMED
|
File without changes
|