supython 0.1.0__py3-none-any.whl
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/__init__.py +24 -0
- supython/admin/__init__.py +3 -0
- supython/admin/api/__init__.py +24 -0
- supython/admin/api/auth.py +118 -0
- supython/admin/api/auth_templates.py +67 -0
- supython/admin/api/auth_users.py +225 -0
- supython/admin/api/db.py +174 -0
- supython/admin/api/functions.py +92 -0
- supython/admin/api/jobs.py +192 -0
- supython/admin/api/ops.py +224 -0
- supython/admin/api/realtime.py +281 -0
- supython/admin/api/service_auth.py +49 -0
- supython/admin/api/service_auth_templates.py +83 -0
- supython/admin/api/service_auth_users.py +346 -0
- supython/admin/api/service_db.py +214 -0
- supython/admin/api/service_functions.py +287 -0
- supython/admin/api/service_jobs.py +282 -0
- supython/admin/api/service_ops.py +213 -0
- supython/admin/api/service_realtime.py +30 -0
- supython/admin/api/service_storage.py +220 -0
- supython/admin/api/storage.py +117 -0
- supython/admin/api/system.py +37 -0
- supython/admin/audit.py +29 -0
- supython/admin/deps.py +22 -0
- supython/admin/errors.py +16 -0
- supython/admin/schemas.py +310 -0
- supython/admin/session.py +52 -0
- supython/admin/spa.py +38 -0
- supython/admin/static/assets/Alert-dluGVkos.js +49 -0
- supython/admin/static/assets/Audit-Njung3HI.js +2 -0
- supython/admin/static/assets/Backups-DzPlFgrm.js +2 -0
- supython/admin/static/assets/Buckets-ByacGkU1.js +2 -0
- supython/admin/static/assets/Channels-BoIuTtam.js +353 -0
- supython/admin/static/assets/ChevronRight-CtQH1EQ1.js +2 -0
- supython/admin/static/assets/CodeViewer-Bqy7-wvH.js +2 -0
- supython/admin/static/assets/Crons-B67vc39F.js +2 -0
- supython/admin/static/assets/DashboardView-CUTFVL6k.js +2 -0
- supython/admin/static/assets/DataTable-COAAWEft.js +747 -0
- supython/admin/static/assets/DescriptionsItem-P8JUDaBs.js +75 -0
- supython/admin/static/assets/DrawerContent-TpYTFgF1.js +139 -0
- supython/admin/static/assets/Empty-cr2r7e2u.js +25 -0
- supython/admin/static/assets/EmptyState-DeDck-OL.js +2 -0
- supython/admin/static/assets/Grid-hFkp9F4P.js +2 -0
- supython/admin/static/assets/Input-DppYTq9C.js +259 -0
- supython/admin/static/assets/Invoke-DW3Nveeh.js +2 -0
- supython/admin/static/assets/JsonField-DibyJgun.js +2 -0
- supython/admin/static/assets/LoginView-BjLyE3Ds.css +1 -0
- supython/admin/static/assets/LoginView-CoOjECT_.js +111 -0
- supython/admin/static/assets/Logs-D9WYrnIT.js +2 -0
- supython/admin/static/assets/Logs-DS1XPa0h.css +1 -0
- supython/admin/static/assets/Migrations-DOSC2ddQ.js +2 -0
- supython/admin/static/assets/ObjectBrowser-_5w8vOX8.js +2 -0
- supython/admin/static/assets/Queue-CywZs6vI.js +2 -0
- supython/admin/static/assets/RefreshTokens-Ccjr53jg.js +2 -0
- supython/admin/static/assets/RlsEditor-BSlH9vSc.js +2 -0
- supython/admin/static/assets/Routes-BiLXE49D.js +2 -0
- supython/admin/static/assets/Routes-C-ianIGD.css +1 -0
- supython/admin/static/assets/SchemaBrowser-DKy2_KQi.css +1 -0
- supython/admin/static/assets/SchemaBrowser-XFvFbtDB.js +2 -0
- supython/admin/static/assets/Select-DIzZyRZb.js +434 -0
- supython/admin/static/assets/Space-n5-XcguU.js +400 -0
- supython/admin/static/assets/SqlEditor-b8pTsILY.js +3 -0
- supython/admin/static/assets/SqlWorkspace-BUS7IntH.js +104 -0
- supython/admin/static/assets/TableData-CQIagLKn.js +2 -0
- supython/admin/static/assets/Tag-D1fOKpTH.js +72 -0
- supython/admin/static/assets/Templates-BS-ugkdq.js +2 -0
- supython/admin/static/assets/Thing-CEAniuMg.js +107 -0
- supython/admin/static/assets/Users-wzwajhlh.js +2 -0
- supython/admin/static/assets/_plugin-vue_export-helper-DGA9ry_j.js +1 -0
- supython/admin/static/assets/dist-VXIJLCYq.js +13 -0
- supython/admin/static/assets/format-length-CGCY1rMh.js +2 -0
- supython/admin/static/assets/get-Ca6unauB.js +2 -0
- supython/admin/static/assets/index-CeE6v959.js +951 -0
- supython/admin/static/assets/pinia-COXwfrOX.js +2 -0
- supython/admin/static/assets/resources-Bt6thQCD.js +44 -0
- supython/admin/static/assets/use-locale-mtgM0a3a.js +2 -0
- supython/admin/static/assets/use-merged-state-BvhkaHNX.js +2 -0
- supython/admin/static/assets/useConfirm-tMjvBFXR.js +2 -0
- supython/admin/static/assets/useResource-C_rJCY8C.js +2 -0
- supython/admin/static/assets/useTable-CnZc5zhi.js +363 -0
- supython/admin/static/assets/useTable-Dg0XlRlq.css +1 -0
- supython/admin/static/assets/useToast-DsZKx0IX.js +2 -0
- supython/admin/static/assets/utils-sbXoq7Ir.js +2 -0
- supython/admin/static/favicon.svg +1 -0
- supython/admin/static/icons.svg +24 -0
- supython/admin/static/index.html +24 -0
- supython/app.py +162 -0
- supython/auth/__init__.py +3 -0
- supython/auth/_email_job.py +11 -0
- supython/auth/providers/__init__.py +34 -0
- supython/auth/providers/github.py +22 -0
- supython/auth/providers/google.py +19 -0
- supython/auth/providers/oauth.py +56 -0
- supython/auth/providers/registry.py +16 -0
- supython/auth/ratelimit.py +39 -0
- supython/auth/router.py +282 -0
- supython/auth/schemas.py +79 -0
- supython/auth/service.py +587 -0
- supython/backups/__init__.py +24 -0
- supython/backups/_backup_job.py +170 -0
- supython/backups/schemas.py +18 -0
- supython/backups/service.py +217 -0
- supython/body_size.py +184 -0
- supython/cli.py +1663 -0
- supython/client/__init__.py +67 -0
- supython/client/_auth.py +249 -0
- supython/client/_client.py +145 -0
- supython/client/_config.py +92 -0
- supython/client/_functions.py +69 -0
- supython/client/_storage.py +255 -0
- supython/client/py.typed +0 -0
- supython/db.py +151 -0
- supython/db_admin.py +8 -0
- supython/extensions.py +36 -0
- supython/functions/__init__.py +19 -0
- supython/functions/context.py +262 -0
- supython/functions/loader.py +307 -0
- supython/functions/router.py +228 -0
- supython/functions/schemas.py +50 -0
- supython/gen/__init__.py +5 -0
- supython/gen/_introspect.py +137 -0
- supython/gen/types_py.py +270 -0
- supython/gen/types_ts.py +365 -0
- supython/health.py +229 -0
- supython/hooks.py +117 -0
- supython/jobs/__init__.py +31 -0
- supython/jobs/backends.py +97 -0
- supython/jobs/context.py +58 -0
- supython/jobs/cron.py +152 -0
- supython/jobs/cron_inproc.py +119 -0
- supython/jobs/decorators.py +76 -0
- supython/jobs/registry.py +79 -0
- supython/jobs/router.py +136 -0
- supython/jobs/schemas.py +92 -0
- supython/jobs/service.py +311 -0
- supython/jobs/worker.py +219 -0
- supython/jwks.py +257 -0
- supython/keyset.py +279 -0
- supython/logging_config.py +291 -0
- supython/mail.py +33 -0
- supython/mailer.py +65 -0
- supython/migrate.py +81 -0
- supython/migrations/0001_extensions_and_roles.sql +46 -0
- supython/migrations/0002_auth_schema.sql +66 -0
- supython/migrations/0003_demo_todos.sql +42 -0
- supython/migrations/0004_auth_v0_2.sql +47 -0
- supython/migrations/0005_storage_schema.sql +117 -0
- supython/migrations/0006_realtime_schema.sql +206 -0
- supython/migrations/0007_jobs_schema.sql +254 -0
- supython/migrations/0008_jobs_last_error.sql +56 -0
- supython/migrations/0009_auth_rate_limits.sql +33 -0
- supython/migrations/0010_worker_heartbeat.sql +14 -0
- supython/migrations/0011_admin_schema.sql +45 -0
- supython/migrations/0012_auth_banned_until.sql +10 -0
- supython/migrations/0013_email_templates.sql +19 -0
- supython/migrations/0014_realtime_payload_warning.sql +96 -0
- supython/migrations/0015_backups_schema.sql +14 -0
- supython/passwords.py +15 -0
- supython/realtime/__init__.py +6 -0
- supython/realtime/broker.py +814 -0
- supython/realtime/protocol.py +234 -0
- supython/realtime/router.py +184 -0
- supython/realtime/schemas.py +207 -0
- supython/realtime/service.py +261 -0
- supython/realtime/topics.py +175 -0
- supython/realtime/websocket.py +586 -0
- supython/scaffold/__init__.py +5 -0
- supython/scaffold/init_project.py +144 -0
- supython/scaffold/templates/Caddyfile.tmpl +4 -0
- supython/scaffold/templates/README.md.tmpl +22 -0
- supython/scaffold/templates/apps_hooks.py.tmpl +11 -0
- supython/scaffold/templates/apps_jobs.py.tmpl +8 -0
- supython/scaffold/templates/asgi.py.tmpl +14 -0
- supython/scaffold/templates/docker-compose.prod.yml.tmpl +84 -0
- supython/scaffold/templates/docker-compose.yml.tmpl +45 -0
- supython/scaffold/templates/docker_postgres_Dockerfile.tmpl +9 -0
- supython/scaffold/templates/docker_postgres_postgresql.conf.tmpl +3 -0
- supython/scaffold/templates/env.example.tmpl +168 -0
- supython/scaffold/templates/functions_README.md.tmpl +21 -0
- supython/scaffold/templates/gitignore.tmpl +14 -0
- supython/scaffold/templates/manage.py.tmpl +11 -0
- supython/scaffold/templates/migrations/.gitkeep +0 -0
- supython/scaffold/templates/package_init.py.tmpl +1 -0
- supython/scaffold/templates/settings.py.tmpl +31 -0
- supython/secretset.py +347 -0
- supython/security_headers.py +78 -0
- supython/settings.py +244 -0
- supython/settings_module.py +117 -0
- supython/storage/__init__.py +5 -0
- supython/storage/backends.py +392 -0
- supython/storage/router.py +341 -0
- supython/storage/schemas.py +50 -0
- supython/storage/service.py +445 -0
- supython/storage/signing.py +119 -0
- supython/tokens.py +85 -0
- supython-0.1.0.dist-info/METADATA +756 -0
- supython-0.1.0.dist-info/RECORD +200 -0
- supython-0.1.0.dist-info/WHEEL +4 -0
- supython-0.1.0.dist-info/entry_points.txt +2 -0
- supython-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.filter-bar[data-v-6ba6e82b]{flex-wrap:wrap;align-items:flex-end;gap:8px;margin-bottom:12px;display:flex}.filter-bar__search[data-v-6ba6e82b]{border:1px solid var(--n-border-color,#ffffff1f);min-width:200px;color:inherit;box-sizing:border-box;background:#ffffff0d;border-radius:4px;outline:none;flex:220px;height:34px;padding:6px 10px;font-size:13px}.filter-bar__field[data-v-6ba6e82b]{flex-direction:column;gap:2px;display:flex}.filter-bar__label[data-v-6ba6e82b]{color:var(--n-text-color-3,#ffffff8c);font-size:11px}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Cn as e}from"./Space-n5-XcguU.js";var t=[],n=new WeakMap;function r(){t.forEach(e=>e(...n.get(e))),t=[]}function i(e,...i){n.set(e,i),!t.includes(e)&&t.push(e)===1&&requestAnimationFrame(r)}function a(){return e()!==null}var o=typeof window<`u`;export{o as n,i as r,a as t};
|
|
2
|
+
//# sourceMappingURL=utils-sbXoq7Ir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
3
|
+
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
4
|
+
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
5
|
+
</symbol>
|
|
6
|
+
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
7
|
+
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
8
|
+
</symbol>
|
|
9
|
+
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
10
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
11
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
12
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
13
|
+
</symbol>
|
|
14
|
+
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
15
|
+
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
16
|
+
</symbol>
|
|
17
|
+
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
18
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
19
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
20
|
+
</symbol>
|
|
21
|
+
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
22
|
+
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
23
|
+
</symbol>
|
|
24
|
+
</svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>supython admin</title>
|
|
7
|
+
<script type="module" crossorigin src="/admin/assets/index-CeE6v959.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/Space-n5-XcguU.js">
|
|
9
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/use-locale-mtgM0a3a.js">
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/get-Ca6unauB.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/use-merged-state-BvhkaHNX.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/Input-DppYTq9C.js">
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/Empty-cr2r7e2u.js">
|
|
14
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/format-length-CGCY1rMh.js">
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/Tag-D1fOKpTH.js">
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/utils-sbXoq7Ir.js">
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/Select-DIzZyRZb.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/ChevronRight-CtQH1EQ1.js">
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/admin/assets/pinia-COXwfrOX.js">
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<div id="app"></div>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
supython/app.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import AsyncIterator, Callable, Coroutine
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
|
|
11
|
+
from . import __version__, db, jwks
|
|
12
|
+
from .admin import router as admin_api_router
|
|
13
|
+
from .admin import spa as admin_spa
|
|
14
|
+
from .auth.router import router as auth_router
|
|
15
|
+
from .extensions import load_extensions
|
|
16
|
+
from .settings_module import UserSettings, load_user_settings
|
|
17
|
+
from .functions.loader import get_registry as get_function_registry
|
|
18
|
+
from .functions.router import router as functions_router
|
|
19
|
+
from .health import router as health_router
|
|
20
|
+
from .hooks import on as hook_on
|
|
21
|
+
from .logging_config import (
|
|
22
|
+
RequestIdMiddleware,
|
|
23
|
+
RequestLoggingMiddleware,
|
|
24
|
+
configure_logging,
|
|
25
|
+
)
|
|
26
|
+
from .body_size import BodySizeLimitMiddleware
|
|
27
|
+
from .security_headers import SecurityHeadersMiddleware
|
|
28
|
+
from .realtime import get_broker
|
|
29
|
+
from .realtime.router import router as realtime_router
|
|
30
|
+
from .settings import get_settings
|
|
31
|
+
from .storage.router import router as storage_router
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@asynccontextmanager
|
|
37
|
+
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
|
|
38
|
+
async with db.lifespan(app):
|
|
39
|
+
registry = get_function_registry()
|
|
40
|
+
try:
|
|
41
|
+
registry.discover()
|
|
42
|
+
except Exception:
|
|
43
|
+
logger.warning("functions: discover() failed at startup", exc_info=True)
|
|
44
|
+
|
|
45
|
+
settings = get_settings()
|
|
46
|
+
jwks_doc = jwks.write_current_jwks(settings.jwt_jwks_path)
|
|
47
|
+
kids = ", ".join(key["kid"] for key in jwks_doc["keys"])
|
|
48
|
+
logger.info("jwt: wrote JWKS path=%s kids=[%s]", settings.jwt_jwks_path, kids)
|
|
49
|
+
|
|
50
|
+
broker = get_broker() if settings.realtime_enabled else None
|
|
51
|
+
if broker is not None:
|
|
52
|
+
try:
|
|
53
|
+
await broker.start()
|
|
54
|
+
except Exception:
|
|
55
|
+
logger.exception("realtime: broker failed to start; continuing without realtime")
|
|
56
|
+
broker = None
|
|
57
|
+
app.state.broker = broker
|
|
58
|
+
|
|
59
|
+
worker = None
|
|
60
|
+
worker_task = None
|
|
61
|
+
if settings.jobs_enabled and settings.jobs_cron_backend == "pg_cron":
|
|
62
|
+
try:
|
|
63
|
+
from .jobs.cron import sync_pg_cron
|
|
64
|
+
|
|
65
|
+
async with db.as_service_role() as conn:
|
|
66
|
+
await sync_pg_cron(conn)
|
|
67
|
+
except Exception:
|
|
68
|
+
logger.warning("jobs: pg_cron sync failed at startup", exc_info=True)
|
|
69
|
+
|
|
70
|
+
if settings.jobs_enabled and settings.jobs_dev_inprocess:
|
|
71
|
+
from .jobs.worker import Worker
|
|
72
|
+
|
|
73
|
+
worker = Worker(settings)
|
|
74
|
+
try:
|
|
75
|
+
worker_task = asyncio.create_task(worker.start())
|
|
76
|
+
except Exception:
|
|
77
|
+
logger.warning("jobs: in-process worker failed to start", exc_info=True)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
yield
|
|
81
|
+
finally:
|
|
82
|
+
if worker is not None:
|
|
83
|
+
try:
|
|
84
|
+
await worker.stop()
|
|
85
|
+
except Exception:
|
|
86
|
+
logger.exception("jobs: worker failed to stop cleanly")
|
|
87
|
+
if worker_task is not None:
|
|
88
|
+
worker_task.cancel()
|
|
89
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
90
|
+
await worker_task
|
|
91
|
+
if broker is not None:
|
|
92
|
+
try:
|
|
93
|
+
await broker.stop()
|
|
94
|
+
except Exception:
|
|
95
|
+
logger.exception("realtime: broker failed to stop cleanly")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_app() -> FastAPI:
|
|
99
|
+
settings = get_settings()
|
|
100
|
+
configure_logging(settings.log_level, json_format=settings.log_json)
|
|
101
|
+
|
|
102
|
+
user = (
|
|
103
|
+
load_user_settings(settings.settings_module)
|
|
104
|
+
if settings.settings_module
|
|
105
|
+
else UserSettings()
|
|
106
|
+
)
|
|
107
|
+
load_extensions([*settings.extensions, *user.extensions])
|
|
108
|
+
|
|
109
|
+
app = FastAPI(
|
|
110
|
+
title="supython",
|
|
111
|
+
description="Lightweight Postgres-first BaaS for Python.",
|
|
112
|
+
version=__version__,
|
|
113
|
+
lifespan=_lifespan,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
117
|
+
app.add_middleware(RequestIdMiddleware)
|
|
118
|
+
|
|
119
|
+
app.add_middleware(
|
|
120
|
+
CORSMiddleware,
|
|
121
|
+
allow_origins=[o.strip() for o in settings.cors_origins.split(",") if o.strip()],
|
|
122
|
+
allow_credentials=True,
|
|
123
|
+
allow_methods=["*"],
|
|
124
|
+
allow_headers=["*"],
|
|
125
|
+
)
|
|
126
|
+
app.add_middleware(BodySizeLimitMiddleware)
|
|
127
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
128
|
+
|
|
129
|
+
for mw in user.extra_middleware:
|
|
130
|
+
app.add_middleware(mw)
|
|
131
|
+
|
|
132
|
+
app.include_router(auth_router)
|
|
133
|
+
app.include_router(storage_router)
|
|
134
|
+
app.include_router(functions_router)
|
|
135
|
+
if settings.realtime_enabled:
|
|
136
|
+
app.include_router(realtime_router)
|
|
137
|
+
if settings.jobs_enabled:
|
|
138
|
+
from .jobs.router import router as jobs_router
|
|
139
|
+
|
|
140
|
+
app.include_router(jobs_router)
|
|
141
|
+
|
|
142
|
+
app.include_router(health_router)
|
|
143
|
+
app.include_router(admin_api_router)
|
|
144
|
+
admin_spa.mount(app)
|
|
145
|
+
|
|
146
|
+
for rt in user.extra_routers:
|
|
147
|
+
app.include_router(rt)
|
|
148
|
+
|
|
149
|
+
def _make_hook_decorator(event: str) -> Callable[..., Any]:
|
|
150
|
+
def decorator(fn: Callable[..., Coroutine[Any, Any, None]] | None = None) -> Any:
|
|
151
|
+
return hook_on(event, fn)
|
|
152
|
+
|
|
153
|
+
return decorator
|
|
154
|
+
|
|
155
|
+
app.on_signup = _make_hook_decorator("signup")
|
|
156
|
+
app.on_login = _make_hook_decorator("login")
|
|
157
|
+
app.on_logout = _make_hook_decorator("logout")
|
|
158
|
+
|
|
159
|
+
return app
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
app = create_app()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from ..jobs.decorators import job
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@job("send_auth_email", max_attempts=5, backoff="exponential", backoff_base_s=10.0)
|
|
5
|
+
async def send_auth_email(ctx, payload):
|
|
6
|
+
await ctx.send_email(
|
|
7
|
+
to=payload["to"],
|
|
8
|
+
subject=payload["subject"],
|
|
9
|
+
text=payload["text"],
|
|
10
|
+
html=payload.get("html"),
|
|
11
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""OAuth provider abstractions."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ProviderProfile:
|
|
10
|
+
"""Normalised identity returned by any OAuth provider."""
|
|
11
|
+
|
|
12
|
+
provider_user_id: str
|
|
13
|
+
email: str
|
|
14
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Provider(ABC):
|
|
18
|
+
"""Abstract base that each concrete OAuth provider must implement."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def authorize_url(
|
|
24
|
+
self, state: str, redirect_uri: str, code_verifier: str | None = None
|
|
25
|
+
) -> str:
|
|
26
|
+
"""Return the full authorization URL to redirect the user to."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def exchange(
|
|
31
|
+
self, code: str, redirect_uri: str, code_verifier: str | None = None
|
|
32
|
+
) -> ProviderProfile:
|
|
33
|
+
"""Exchange an authorization code for a normalised ProviderProfile."""
|
|
34
|
+
...
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""GitHub OAuth2 provider."""
|
|
2
|
+
|
|
3
|
+
from . import ProviderProfile
|
|
4
|
+
from .oauth import OAuthProvider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GitHubProvider(OAuthProvider):
|
|
8
|
+
name = "github"
|
|
9
|
+
AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
|
|
10
|
+
# GitHub requires Accept: application/json to return JSON (not form-encoded).
|
|
11
|
+
TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
12
|
+
USERINFO_URL = "https://api.github.com/user"
|
|
13
|
+
DEFAULT_SCOPE = "read:user user:email"
|
|
14
|
+
|
|
15
|
+
def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
|
|
16
|
+
# GitHub users may have a null public email; the service layer will
|
|
17
|
+
# reject the profile if the email is empty.
|
|
18
|
+
return ProviderProfile(
|
|
19
|
+
provider_user_id=str(data["id"]),
|
|
20
|
+
email=data.get("email") or "",
|
|
21
|
+
raw=data,
|
|
22
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Google OAuth2 provider."""
|
|
2
|
+
|
|
3
|
+
from . import ProviderProfile
|
|
4
|
+
from .oauth import OAuthProvider
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GoogleProvider(OAuthProvider):
|
|
8
|
+
name = "google"
|
|
9
|
+
AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
10
|
+
TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
11
|
+
USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
|
|
12
|
+
DEFAULT_SCOPE = "openid email profile"
|
|
13
|
+
|
|
14
|
+
def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
|
|
15
|
+
return ProviderProfile(
|
|
16
|
+
provider_user_id=data["sub"],
|
|
17
|
+
email=data.get("email", ""),
|
|
18
|
+
raw=data,
|
|
19
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Shared authlib-based OAuth2 provider base."""
|
|
2
|
+
|
|
3
|
+
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
|
4
|
+
|
|
5
|
+
from . import Provider, ProviderProfile
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OAuthProvider(Provider):
|
|
9
|
+
"""Concrete base for standard Authorization Code + PKCE providers."""
|
|
10
|
+
|
|
11
|
+
AUTHORIZE_URL: str
|
|
12
|
+
TOKEN_URL: str
|
|
13
|
+
USERINFO_URL: str
|
|
14
|
+
DEFAULT_SCOPE: str = "openid email profile"
|
|
15
|
+
|
|
16
|
+
def __init__(self, client_id: str, client_secret: str) -> None:
|
|
17
|
+
self._client_id = client_id
|
|
18
|
+
self._client_secret = client_secret
|
|
19
|
+
|
|
20
|
+
async def authorize_url(
|
|
21
|
+
self, state: str, redirect_uri: str, code_verifier: str | None = None
|
|
22
|
+
) -> str:
|
|
23
|
+
async with AsyncOAuth2Client(
|
|
24
|
+
client_id=self._client_id,
|
|
25
|
+
client_secret=self._client_secret,
|
|
26
|
+
scope=self.DEFAULT_SCOPE,
|
|
27
|
+
code_challenge_method="S256",
|
|
28
|
+
) as client:
|
|
29
|
+
url, _ = client.create_authorization_url(
|
|
30
|
+
self.AUTHORIZE_URL,
|
|
31
|
+
state=state,
|
|
32
|
+
redirect_uri=redirect_uri,
|
|
33
|
+
code_verifier=code_verifier,
|
|
34
|
+
)
|
|
35
|
+
return url
|
|
36
|
+
|
|
37
|
+
async def exchange(
|
|
38
|
+
self, code: str, redirect_uri: str, code_verifier: str | None = None
|
|
39
|
+
) -> ProviderProfile:
|
|
40
|
+
async with AsyncOAuth2Client(
|
|
41
|
+
client_id=self._client_id,
|
|
42
|
+
client_secret=self._client_secret,
|
|
43
|
+
) as client:
|
|
44
|
+
await client.fetch_token(
|
|
45
|
+
self.TOKEN_URL,
|
|
46
|
+
code=code,
|
|
47
|
+
redirect_uri=redirect_uri,
|
|
48
|
+
code_verifier=code_verifier,
|
|
49
|
+
)
|
|
50
|
+
resp = await client.get(self.USERINFO_URL)
|
|
51
|
+
resp.raise_for_status()
|
|
52
|
+
data: dict = resp.json()
|
|
53
|
+
return self._profile_from_userinfo(data)
|
|
54
|
+
|
|
55
|
+
def _profile_from_userinfo(self, data: dict) -> ProviderProfile:
|
|
56
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Provider registry: maps provider name → configured Provider instance."""
|
|
2
|
+
|
|
3
|
+
from ...settings import get_settings
|
|
4
|
+
from . import Provider
|
|
5
|
+
from .github import GitHubProvider
|
|
6
|
+
from .google import GoogleProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_provider(name: str) -> Provider:
|
|
10
|
+
"""Return a configured provider by name, or raise KeyError if unavailable."""
|
|
11
|
+
s = get_settings()
|
|
12
|
+
if name == "google" and s.google_client_id and s.google_client_secret:
|
|
13
|
+
return GoogleProvider(s.google_client_id, s.google_client_secret)
|
|
14
|
+
if name == "github" and s.github_client_id and s.github_client_secret:
|
|
15
|
+
return GitHubProvider(s.github_client_id, s.github_client_secret)
|
|
16
|
+
raise KeyError(f"Unknown or unconfigured OAuth provider: {name!r}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Postgres-backed fixed-window rate limiter shared by auth endpoints."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
import asyncpg
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class RateLimit:
|
|
14
|
+
limit: int
|
|
15
|
+
window_seconds: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _window_start(window_seconds: int, *, now: datetime | None = None) -> datetime:
|
|
19
|
+
now = now or datetime.now(UTC)
|
|
20
|
+
epoch = int(now.timestamp())
|
|
21
|
+
return datetime.fromtimestamp(epoch - (epoch % window_seconds), tz=UTC)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def hit(
|
|
25
|
+
conn: asyncpg.Connection, *, bucket: str, rule: RateLimit
|
|
26
|
+
) -> tuple[bool, int]:
|
|
27
|
+
"""Increment the counter for ``bucket``. Returns ``(blocked, count)``."""
|
|
28
|
+
count = await conn.fetchval(
|
|
29
|
+
"""
|
|
30
|
+
insert into auth.rate_limit_buckets as b (bucket, window_start, count)
|
|
31
|
+
values ($1, $2, 1)
|
|
32
|
+
on conflict (bucket, window_start) do update
|
|
33
|
+
set count = b.count + 1
|
|
34
|
+
returning b.count
|
|
35
|
+
""",
|
|
36
|
+
bucket,
|
|
37
|
+
_window_start(rule.window_seconds),
|
|
38
|
+
)
|
|
39
|
+
return count > rule.limit, count
|