stkit 17.2.6__tar.gz → 17.3.0__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.
- {stkit-17.2.6 → stkit-17.3.0}/CHANGELOG.md +20 -0
- {stkit-17.2.6 → stkit-17.3.0}/PKG-INFO +2 -2
- {stkit-17.2.6 → stkit-17.3.0}/pyproject.toml +2 -2
- {stkit-17.2.6 → stkit-17.3.0}/stk/__init__.py +1 -1
- {stkit-17.2.6 → stkit-17.3.0}/stk/app.py +19 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/doctor.py +13 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/files/__init__.py +13 -1
- stkit-17.3.0/stk/migrations/versions/stk_0007_confirm_existing_users.py +28 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/settings.py +11 -2
- stkit-17.3.0/stk/templates/security/confirm_email.html +30 -0
- stkit-17.3.0/stk/templates/security/forgot_password.html +44 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/login_user.html +5 -0
- stkit-17.3.0/stk/templates/security/reset_password.html +45 -0
- stkit-17.3.0/stk/templates/security/send_confirmation.html +44 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/user/models.py +7 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_cli.py +10 -0
- stkit-17.3.0/tests/test_confirm_migration.py +28 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_files.py +13 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_security_compatibility.py +102 -0
- stkit-17.2.6/stk/templates/security/confirm_email.html +0 -44
- stkit-17.2.6/stk/templates/security/forgot_password.html +0 -63
- stkit-17.2.6/stk/templates/security/reset_password.html +0 -53
- stkit-17.2.6/stk/templates/security/send_confirmation.html +0 -48
- {stkit-17.2.6 → stkit-17.3.0}/.gitignore +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/LICENSE +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/README.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/SECURITY.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/.env-sample +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/.gitignore +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/README.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/app.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/migrations/364232ae77d5_add_note.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/note/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/note/models.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/note/templates/note/index.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/note/views.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/settings.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/minimal/templates/layout.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/pyproject.toml +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/tests/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/tests/test_app.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/examples/minimal/uv.lock +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/__main__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/agent_login.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/checks.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/agent.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/base.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/copy_rules.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/database.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/develop.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/main.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/reports.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/smoke.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/users.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/cli/watch.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/extensions.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/files/models.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/files/store.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/files/views.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/mcp/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/mcp/models.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/mcp/server.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/mcp/views.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/env.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/script.py.mako +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/versions/stk_0001_initial_schema.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/versions/stk_0002_jobs.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/versions/stk_0003_files.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/versions/stk_0004_notifications.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/versions/stk_0005_mcp_tokens.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/migrations/versions/stk_0006_stk_mcp_token.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/notification/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/notification/models.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/notification/views.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/portal/views.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/public/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/public/models.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/public/views.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/CLAUDE.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/Dockerfile +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/README.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/__app__/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/__app__/app.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/__app__/settings.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/claude/skills/stk-api/SKILL.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/claude/skills/stk-blueprint/SKILL.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/claude/skills/stk-deploy/SKILL.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/claude/skills/stk-lessons/SKILL.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/claude/skills/stk-migrate/SKILL.md +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/docker-compose.yml +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/dockerignore +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/env-sample +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/github/workflows/checks.yml +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/gitignore +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/nginx/nginx.conf +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/nginx/stk.conf +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/postgres/init-db.sh +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/pyproject.toml +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/tests/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/app/tests/test_app.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/generator.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/scaffold/templates.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/security.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/shell.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/VERSIONS.txt +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/css/app.css +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/css/layout.css +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/css/vuetify.min.css +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.animals.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.arrows-1.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.arrows-2.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.badges.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.brand-1.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.brand-2.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.buildings.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.charts.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.communication.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.computers.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.core.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.currencies.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.database.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.design-1.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.design-2.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.development.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.devices-1.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.devices-2.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.document.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.e-commerce.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.electrical.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.extensions.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.food.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.games.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.gender.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.gestures.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.health.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.laundry.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.letters.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.logic.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.map.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.math.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.media.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.mood.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.nature.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.numbers.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.photography.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.shapes.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.sport.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.symbols.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.system-1.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.system-2.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.system-3.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.text.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.vehicles.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.version-control.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.weather.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/fonts/tabler-icons.zodiac.woff2 +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/icons/tabler-icons.min.css +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/img/auth-bg.webp +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/img/favicon.ico +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/img/stk.svg +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/axios.min.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/components/NotificationDropdown.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/components/ThemeSwitcher.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/components/TransitionExpand.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/components/VerticalNavGroup.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/components/VerticalNavLink.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/components/VerticalNavSectionTitle.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/components/index.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/config.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/vue.min.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/js/vuetify.min.js +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/static/robots.txt +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/tasks/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/tasks/models.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/401.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/404.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/500.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/_brand.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/auth_layout.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/cms/activities.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/cms/jobs.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/cms/roles.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/cms/users.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/dashboard.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/index.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/layout.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/login-layout.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/mcp/tokens.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/_macros.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/_messages.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/change_password.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/change_notice.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/change_notice.txt +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/confirmation_instructions.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/confirmation_instructions.txt +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/login_instructions.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/login_instructions.txt +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/reset_instructions.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/reset_instructions.txt +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/reset_notice.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/reset_notice.txt +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/welcome.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/email/welcome.txt +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/mf_recovery.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/mf_recovery_codes.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/register_user.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/send_login.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/two_factor_select.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/two_factor_setup.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/two_factor_verify_code.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/wan_register.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/wan_signin.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/templates/security/wan_verify.html +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/user/__init__.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/user/forms.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/user/views.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/utils/email.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/utils/netguard.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/utils/ratelimit.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/stk/websocket.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_agent_operability.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_app_index.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_branding.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_builder_ux.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_composition.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_copy_rules.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_csrf_exempt.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_error_handler.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_jobs.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_mcp.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_mcp_migration.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_migration_drift.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_netguard.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_notifications.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_production_security.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_revision_imports.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_route_collisions.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_scaffolder.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_shell.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_smoke_pages.py +0 -0
- {stkit-17.2.6 → stkit-17.3.0}/tests/test_static_versioning.py +0 -0
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v17.3.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
Run `uv run stk db upgrade`: `stk_0007` marks every existing account confirmed. Requires quart-security 2.1.0.
|
|
6
|
+
|
|
7
|
+
- Password reset by email at `/reset`, on by default once `MAIL_SERVER` and `STK_PUBLIC_URL` are both set (`SECURITY_RECOVERABLE` overrides). Links work once, for one hour, and die if the email or password changes. A reset signs out every session, revokes MCP tokens and clears a lockout (Authentication).
|
|
8
|
+
- Public sign-up now requires email confirmation (`SECURITY_CONFIRMABLE`, default on). An unconfirmed account fails login exactly like a wrong password, which closes the last way to find accounts through sign-up. With `SECURITY_REGISTERABLE=true`, startup fails without `MAIL_SERVER`; set `SECURITY_CONFIRMABLE=false` to keep unconfirmed sign-up (Authentication).
|
|
9
|
+
- Users created by code, the CLI, admins or OAuth start confirmed: `User()` sets `confirmed_at` unless given one.
|
|
10
|
+
- A locked account now fails login with the same message as a wrong password.
|
|
11
|
+
- Auth mail links come from `STK_PUBLIC_URL` (as `SECURITY_PUBLIC_URL`), never the Host header. The 17.2.7 sign-up notice now comes from quart-security.
|
|
12
|
+
- The reset, resend and confirm pages use the current auth layout.
|
|
13
|
+
|
|
14
|
+
## v17.2.7 (2026-10-31)
|
|
15
|
+
|
|
16
|
+
No migration required. Requires quart-security 2.0.6.
|
|
17
|
+
|
|
18
|
+
- Registration no longer reveals which emails have an account: an existing email gets the same answer as a new one and nothing is created. With `MAIL_SERVER` set, the owner is emailed in the background that someone tried; its link comes from `STK_PUBLIC_URL`, never the request's Host header (Authentication).
|
|
19
|
+
- After login, users land back on the page that sent them to `/login`, including through 2FA and passkeys (Authentication).
|
|
20
|
+
- `stk.files` refuses to start when `STK_UPLOAD_MAX_BYTES` does not fit under Quart's `MAX_CONTENT_LENGTH`. Before, any limit above about 16 MiB was silently capped with a generic 413 (Configuration).
|
|
21
|
+
- `stk doctor` warns when `STK_PUBLIC_URL` is unset outside development, since any Host header is then accepted.
|
|
22
|
+
|
|
3
23
|
## v17.2.6 (2026-10-31)
|
|
4
24
|
|
|
5
25
|
No migration required.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: stkit
|
|
3
|
-
Version: 17.
|
|
3
|
+
Version: 17.3.0
|
|
4
4
|
Summary: An async Quart framework optimized for AI-assisted development workflows
|
|
5
5
|
Project-URL: Documentation, https://docs.stkit.dev
|
|
6
6
|
Project-URL: Source, https://github.com/level09/stk
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pillow>=13.0.0
|
|
|
28
28
|
Requires-Dist: psycopg2-binary>=2.9.10
|
|
29
29
|
Requires-Dist: python-dotenv>=1.2.2
|
|
30
30
|
Requires-Dist: qrcode>=8.2
|
|
31
|
-
Requires-Dist: quart-security<3,>=2.0
|
|
31
|
+
Requires-Dist: quart-security<3,>=2.1.0
|
|
32
32
|
Requires-Dist: quart-session>=3.0.0
|
|
33
33
|
Requires-Dist: quart>=0.20.0
|
|
34
34
|
Requires-Dist: rich>=14.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stkit"
|
|
3
|
-
version = "17.
|
|
3
|
+
version = "17.3.0"
|
|
4
4
|
description = "An async Quart framework optimized for AI-assisted development workflows"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Level09", email = "hello@level09.com"}
|
|
@@ -30,7 +30,7 @@ dependencies = [
|
|
|
30
30
|
"python-dotenv>=1.2.2",
|
|
31
31
|
"qrcode>=8.2",
|
|
32
32
|
"quart>=0.20.0",
|
|
33
|
-
"quart-security>=2.0
|
|
33
|
+
"quart-security>=2.1.0,<3",
|
|
34
34
|
"quart-session>=3.0.0",
|
|
35
35
|
"rich>=14.0.0",
|
|
36
36
|
"sqlalchemy[asyncio]>=2.0.41",
|
|
@@ -173,16 +173,35 @@ def register_extensions(app):
|
|
|
173
173
|
app.logger.warning("Timed out while disposing SQLAlchemy engine")
|
|
174
174
|
|
|
175
175
|
from stk.security import register_security_controls
|
|
176
|
+
from stk.utils.email import send_email
|
|
176
177
|
from stk.utils.ratelimit import register_rate_limits
|
|
177
178
|
|
|
178
179
|
register_rate_limits(app)
|
|
179
180
|
register_security_controls(app)
|
|
180
181
|
|
|
182
|
+
config = app.config
|
|
183
|
+
# Links in auth email come from the public URL, never the Host header.
|
|
184
|
+
config.setdefault("SECURITY_PUBLIC_URL", config.get("STK_PUBLIC_URL"))
|
|
185
|
+
if config.get("SECURITY_RECOVERABLE") is None:
|
|
186
|
+
config["SECURITY_RECOVERABLE"] = bool(
|
|
187
|
+
config.get("MAIL_SERVER") and config["SECURITY_PUBLIC_URL"]
|
|
188
|
+
)
|
|
189
|
+
if (
|
|
190
|
+
config.get("SECURITY_REGISTERABLE")
|
|
191
|
+
and config.get("SECURITY_CONFIRMABLE", True)
|
|
192
|
+
and not config.get("MAIL_SERVER")
|
|
193
|
+
):
|
|
194
|
+
raise RuntimeError(
|
|
195
|
+
"SECURITY_REGISTERABLE needs MAIL_SERVER to send confirmation links;"
|
|
196
|
+
" set it, or set SECURITY_CONFIRMABLE=false to allow unconfirmed sign-up"
|
|
197
|
+
)
|
|
198
|
+
|
|
181
199
|
Security(
|
|
182
200
|
app,
|
|
183
201
|
user_datastore,
|
|
184
202
|
register_form=ExtendedRegisterForm,
|
|
185
203
|
change_password_form=OAuthAwareChangePasswordForm,
|
|
204
|
+
send_mail=send_email,
|
|
186
205
|
)
|
|
187
206
|
# The library resets its session before loading the user. Share that session
|
|
188
207
|
# so signals and application routes participate in the same transaction.
|
|
@@ -193,6 +193,18 @@ def _check_agent_login(config):
|
|
|
193
193
|
)
|
|
194
194
|
|
|
195
195
|
|
|
196
|
+
def _check_public_url(config):
|
|
197
|
+
env = config.get("STK_ENV", "production")
|
|
198
|
+
if env != "development" and not config.get("STK_PUBLIC_URL"):
|
|
199
|
+
return _finding(
|
|
200
|
+
"public url",
|
|
201
|
+
WARN,
|
|
202
|
+
f"STK_PUBLIC_URL unset with STK_ENV={env}: any Host header is accepted",
|
|
203
|
+
"set STK_PUBLIC_URL to the origin users visit, e.g. https://app.example.com",
|
|
204
|
+
)
|
|
205
|
+
return _finding("public url", OK, config.get("STK_PUBLIC_URL") or "development")
|
|
206
|
+
|
|
207
|
+
|
|
196
208
|
def _check_browser():
|
|
197
209
|
try:
|
|
198
210
|
from playwright.sync_api import sync_playwright
|
|
@@ -244,6 +256,7 @@ def build_doctor_report():
|
|
|
244
256
|
_check_migrations(app),
|
|
245
257
|
_check_admin(),
|
|
246
258
|
_check_agent_login(app.config),
|
|
259
|
+
_check_public_url(app.config),
|
|
247
260
|
]
|
|
248
261
|
findings += [_check_assets(), _check_browser(), _check_repl()]
|
|
249
262
|
if any(item["status"] == FAIL for item in findings):
|
|
@@ -8,10 +8,22 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def init_app(app):
|
|
11
|
-
from stk.files.views import SNIFFED, bp, can_read
|
|
11
|
+
from stk.files.views import MULTIPART_SLACK, SNIFFED, bp, can_read
|
|
12
12
|
|
|
13
13
|
app.config.setdefault("STK_UPLOAD_DIR", str(Path(app.instance_path) / "files"))
|
|
14
14
|
app.config.setdefault("STK_UPLOAD_MAX_BYTES", 10 * 1024 * 1024)
|
|
15
|
+
# Quart caps every request body at MAX_CONTENT_LENGTH before a view runs, so
|
|
16
|
+
# a larger upload limit would never take effect.
|
|
17
|
+
limit, body_max = (
|
|
18
|
+
app.config["STK_UPLOAD_MAX_BYTES"],
|
|
19
|
+
app.config["MAX_CONTENT_LENGTH"],
|
|
20
|
+
)
|
|
21
|
+
if body_max is not None and limit + MULTIPART_SLACK > body_max:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"STK_UPLOAD_MAX_BYTES ({limit}) plus {MULTIPART_SLACK} bytes of"
|
|
24
|
+
f" multipart framing exceeds MAX_CONTENT_LENGTH ({body_max}): raise"
|
|
25
|
+
" MAX_CONTENT_LENGTH or lower STK_UPLOAD_MAX_BYTES"
|
|
26
|
+
)
|
|
15
27
|
app.config.setdefault(
|
|
16
28
|
"STK_UPLOAD_TYPES",
|
|
17
29
|
("image/png", "image/jpeg", "image/gif", "image/webp", "application/pdf"),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Mark every existing account confirmed.
|
|
2
|
+
|
|
3
|
+
stk 17.3 turns on email confirmation, and an account with no `confirmed_at` can
|
|
4
|
+
no longer sign in. No account before this revision ever had to confirm, so all
|
|
5
|
+
of them count as confirmed; only sign-ups from now on wait for the email.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sqlalchemy as sa
|
|
9
|
+
from alembic import op
|
|
10
|
+
|
|
11
|
+
revision = "stk_0007"
|
|
12
|
+
down_revision = "stk_0006"
|
|
13
|
+
branch_labels = None
|
|
14
|
+
depends_on = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def upgrade() -> None:
|
|
18
|
+
user = sa.table("user", sa.column("confirmed_at", sa.DateTime()))
|
|
19
|
+
op.execute(
|
|
20
|
+
user.update()
|
|
21
|
+
.where(user.c.confirmed_at.is_(None))
|
|
22
|
+
.values(confirmed_at=sa.func.current_timestamp())
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
# Which accounts were unconfirmed before is not recorded; leave them all.
|
|
28
|
+
pass
|
|
@@ -36,8 +36,10 @@ class Config:
|
|
|
36
36
|
SECURITY_REGISTERABLE = (
|
|
37
37
|
os.environ.get("SECURITY_REGISTERABLE", "False").lower() == "true"
|
|
38
38
|
)
|
|
39
|
-
|
|
40
|
-
SECURITY_CONFIRMABLE =
|
|
39
|
+
# Self-registered accounts sign in only after confirming their email.
|
|
40
|
+
SECURITY_CONFIRMABLE = (
|
|
41
|
+
os.environ.get("SECURITY_CONFIRMABLE", "True").lower() == "true"
|
|
42
|
+
)
|
|
41
43
|
SECURITY_CHANGEABLE = True
|
|
42
44
|
SECURITY_TRACKABLE = True
|
|
43
45
|
SECURITY_PASSWORD_HASH = "argon2"
|
|
@@ -106,6 +108,12 @@ class Config:
|
|
|
106
108
|
MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
|
|
107
109
|
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
|
|
108
110
|
SECURITY_EMAIL_SENDER = os.environ.get("SECURITY_EMAIL_SENDER", "info@domain.com")
|
|
111
|
+
# Password reset by email. Unset: on once MAIL_SERVER and STK_PUBLIC_URL are.
|
|
112
|
+
SECURITY_RECOVERABLE = (
|
|
113
|
+
os.environ["SECURITY_RECOVERABLE"].lower() == "true"
|
|
114
|
+
if "SECURITY_RECOVERABLE" in os.environ
|
|
115
|
+
else None
|
|
116
|
+
)
|
|
109
117
|
SECURITY_SEND_PASSWORD_CHANGE_EMAIL = False
|
|
110
118
|
|
|
111
119
|
# Google OAuth Settings
|
|
@@ -142,5 +150,6 @@ class TestingConfig(Config):
|
|
|
142
150
|
DISABLE_MULTIPLE_SESSIONS = False
|
|
143
151
|
STK_ENABLE_AGENT_LOGIN = False
|
|
144
152
|
MAIL_SERVER = None
|
|
153
|
+
SECURITY_RECOVERABLE = False
|
|
145
154
|
GOOGLE_AUTH_ENABLED = False
|
|
146
155
|
GITHUB_AUTH_ENABLED = False
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{% extends 'login-layout.html' %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<v-main class="d-flex flex-column align-center justify-center" style="min-height: 100vh; background: rgb(var(--v-theme-background));">
|
|
5
|
+
<v-card class="mx-auto pa-8 w-100" rounded="lg" style="max-width: 424px;">
|
|
6
|
+
<div class="text-center mb-6">
|
|
7
|
+
<div class="mb-4">{% include "_brand.html" %}</div>
|
|
8
|
+
<div class="text-headline-small font-weight-bold">Confirm your email</div>
|
|
9
|
+
<div class="text-body-medium text-medium-emphasis">Confirm only if you created this account. If you did not, close this page.</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<form action="{{ url_for_security('confirm_email', token=confirm_email_token) }}" method="POST">
|
|
13
|
+
{{ confirm_email_form.hidden_tag() }}
|
|
14
|
+
{% include "security/_messages.html" %}
|
|
15
|
+
<v-btn type="submit" color="primary" size="large" block class="mb-4">Confirm my email</v-btn>
|
|
16
|
+
</form>
|
|
17
|
+
</v-card>
|
|
18
|
+
</v-main>
|
|
19
|
+
{% endblock %}
|
|
20
|
+
|
|
21
|
+
{% block js %}
|
|
22
|
+
<script>
|
|
23
|
+
const { createApp } = Vue;
|
|
24
|
+
const { createVuetify } = Vuetify;
|
|
25
|
+
const vuetify = createVuetify(vuetifyConfig);
|
|
26
|
+
|
|
27
|
+
const app = createApp({ delimiters: delimiters });
|
|
28
|
+
app.use(vuetify).mount('#app');
|
|
29
|
+
</script>
|
|
30
|
+
{% endblock %}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% extends 'login-layout.html' %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<v-main class="d-flex flex-column align-center justify-center" style="min-height: 100vh; background: rgb(var(--v-theme-background));">
|
|
5
|
+
<v-card class="mx-auto pa-8 w-100" rounded="lg" style="max-width: 424px;">
|
|
6
|
+
<div class="text-center mb-6">
|
|
7
|
+
<div class="mb-4">{% include "_brand.html" %}</div>
|
|
8
|
+
<div class="text-headline-small font-weight-bold">Reset password</div>
|
|
9
|
+
<div class="text-body-medium text-medium-emphasis">We will email you a link to choose a new password</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<form action="{{ url_for_security('forgot_password') }}" method="POST">
|
|
13
|
+
{{ forgot_password_form.hidden_tag() }}
|
|
14
|
+
<v-text-field name="email" type="email" label="Email" autocomplete="email" required class="mb-4"></v-text-field>
|
|
15
|
+
|
|
16
|
+
{% include "security/_messages.html" %}
|
|
17
|
+
|
|
18
|
+
{% for field, errors in forgot_password_form.errors.items() %}
|
|
19
|
+
<v-alert type="error" variant="tonal" density="compact" class="mb-4">{{ errors[0] }}</v-alert>
|
|
20
|
+
{% endfor %}
|
|
21
|
+
|
|
22
|
+
<v-btn type="submit" color="primary" size="large" block class="mb-4">Send reset link</v-btn>
|
|
23
|
+
</form>
|
|
24
|
+
</v-card>
|
|
25
|
+
|
|
26
|
+
<div class="mt-6 w-100" style="max-width: 424px;">
|
|
27
|
+
<v-btn href="{{ url_for_security('login') }}" variant="text" color="primary">
|
|
28
|
+
<i class="ti ti-chevron-left mr-1"></i>
|
|
29
|
+
Back to log in
|
|
30
|
+
</v-btn>
|
|
31
|
+
</div>
|
|
32
|
+
</v-main>
|
|
33
|
+
{% endblock %}
|
|
34
|
+
|
|
35
|
+
{% block js %}
|
|
36
|
+
<script>
|
|
37
|
+
const { createApp } = Vue;
|
|
38
|
+
const { createVuetify } = Vuetify;
|
|
39
|
+
const vuetify = createVuetify(vuetifyConfig);
|
|
40
|
+
|
|
41
|
+
const app = createApp({ delimiters: delimiters });
|
|
42
|
+
app.use(vuetify).mount('#app');
|
|
43
|
+
</script>
|
|
44
|
+
{% endblock %}
|
|
@@ -157,6 +157,11 @@
|
|
|
157
157
|
<span class="text-medium-emphasis">New to {{ config.APP_NAME }}? </span>
|
|
158
158
|
<a href="{{ url_for_security('register') }}" class="text-decoration-none">Sign up</a>
|
|
159
159
|
</div>
|
|
160
|
+
{% if security.confirmable %}
|
|
161
|
+
<div class="text-center mt-2">
|
|
162
|
+
<a href="{{ url_for_security('send_confirmation') }}" class="text-decoration-none text-body-small">Resend confirmation link</a>
|
|
163
|
+
</div>
|
|
164
|
+
{% endif %}
|
|
160
165
|
{% endif %}
|
|
161
166
|
</v-card-text>
|
|
162
167
|
</v-card>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{% extends 'login-layout.html' %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<v-main class="d-flex flex-column align-center justify-center" style="min-height: 100vh; background: rgb(var(--v-theme-background));">
|
|
5
|
+
<v-card class="mx-auto pa-8 w-100" rounded="lg" style="max-width: 424px;">
|
|
6
|
+
<div class="text-center mb-6">
|
|
7
|
+
<div class="mb-4">{% include "_brand.html" %}</div>
|
|
8
|
+
<div class="text-headline-small font-weight-bold">Choose a new password</div>
|
|
9
|
+
<div class="text-body-medium text-medium-emphasis">Every signed-in session will be signed out</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST">
|
|
13
|
+
{{ reset_password_form.hidden_tag() }}
|
|
14
|
+
<v-text-field name="password" type="password" label="New password" autocomplete="new-password" required class="mb-2"></v-text-field>
|
|
15
|
+
<v-text-field name="password_confirm" type="password" label="Confirm new password" autocomplete="new-password" required class="mb-4"></v-text-field>
|
|
16
|
+
|
|
17
|
+
{% include "security/_messages.html" %}
|
|
18
|
+
|
|
19
|
+
{% for field, errors in reset_password_form.errors.items() %}
|
|
20
|
+
<v-alert type="error" variant="tonal" density="compact" class="mb-4">{{ errors[0] }}</v-alert>
|
|
21
|
+
{% endfor %}
|
|
22
|
+
|
|
23
|
+
<v-btn type="submit" color="primary" size="large" block class="mb-4">Reset password</v-btn>
|
|
24
|
+
</form>
|
|
25
|
+
</v-card>
|
|
26
|
+
|
|
27
|
+
<div class="mt-6 w-100" style="max-width: 424px;">
|
|
28
|
+
<v-btn href="{{ url_for_security('login') }}" variant="text" color="primary">
|
|
29
|
+
<i class="ti ti-chevron-left mr-1"></i>
|
|
30
|
+
Back to log in
|
|
31
|
+
</v-btn>
|
|
32
|
+
</div>
|
|
33
|
+
</v-main>
|
|
34
|
+
{% endblock %}
|
|
35
|
+
|
|
36
|
+
{% block js %}
|
|
37
|
+
<script>
|
|
38
|
+
const { createApp } = Vue;
|
|
39
|
+
const { createVuetify } = Vuetify;
|
|
40
|
+
const vuetify = createVuetify(vuetifyConfig);
|
|
41
|
+
|
|
42
|
+
const app = createApp({ delimiters: delimiters });
|
|
43
|
+
app.use(vuetify).mount('#app');
|
|
44
|
+
</script>
|
|
45
|
+
{% endblock %}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% extends 'login-layout.html' %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<v-main class="d-flex flex-column align-center justify-center" style="min-height: 100vh; background: rgb(var(--v-theme-background));">
|
|
5
|
+
<v-card class="mx-auto pa-8 w-100" rounded="lg" style="max-width: 424px;">
|
|
6
|
+
<div class="text-center mb-6">
|
|
7
|
+
<div class="mb-4">{% include "_brand.html" %}</div>
|
|
8
|
+
<div class="text-headline-small font-weight-bold">Resend confirmation</div>
|
|
9
|
+
<div class="text-body-medium text-medium-emphasis">We will email you a new link to confirm your account</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<form action="{{ url_for_security('send_confirmation') }}" method="POST">
|
|
13
|
+
{{ send_confirmation_form.hidden_tag() }}
|
|
14
|
+
<v-text-field name="email" type="email" label="Email" autocomplete="email" required class="mb-4"></v-text-field>
|
|
15
|
+
|
|
16
|
+
{% include "security/_messages.html" %}
|
|
17
|
+
|
|
18
|
+
{% for field, errors in send_confirmation_form.errors.items() %}
|
|
19
|
+
<v-alert type="error" variant="tonal" density="compact" class="mb-4">{{ errors[0] }}</v-alert>
|
|
20
|
+
{% endfor %}
|
|
21
|
+
|
|
22
|
+
<v-btn type="submit" color="primary" size="large" block class="mb-4">Send link</v-btn>
|
|
23
|
+
</form>
|
|
24
|
+
</v-card>
|
|
25
|
+
|
|
26
|
+
<div class="mt-6 w-100" style="max-width: 424px;">
|
|
27
|
+
<v-btn href="{{ url_for_security('login') }}" variant="text" color="primary">
|
|
28
|
+
<i class="ti ti-chevron-left mr-1"></i>
|
|
29
|
+
Back to log in
|
|
30
|
+
</v-btn>
|
|
31
|
+
</div>
|
|
32
|
+
</v-main>
|
|
33
|
+
{% endblock %}
|
|
34
|
+
|
|
35
|
+
{% block js %}
|
|
36
|
+
<script>
|
|
37
|
+
const { createApp } = Vue;
|
|
38
|
+
const { createVuetify } = Vuetify;
|
|
39
|
+
const vuetify = createVuetify(vuetifyConfig);
|
|
40
|
+
|
|
41
|
+
const app = createApp({ delimiters: delimiters });
|
|
42
|
+
app.use(vuetify).mount('#app');
|
|
43
|
+
</script>
|
|
44
|
+
{% endblock %}
|
|
@@ -69,6 +69,13 @@ class User(Base, UserMixin):
|
|
|
69
69
|
password_set = Column(Boolean, default=True, nullable=False)
|
|
70
70
|
active = Column(Boolean, default=False, nullable=True)
|
|
71
71
|
|
|
72
|
+
def __init__(self, **kwargs):
|
|
73
|
+
# Accounts made by code, the CLI, admins or OAuth start confirmed. Only
|
|
74
|
+
# self-registration sets None afterwards, until the emailed link is
|
|
75
|
+
# opened; a column default would refill that None at insert.
|
|
76
|
+
kwargs.setdefault("confirmed_at", datetime.now())
|
|
77
|
+
super().__init__(**kwargs)
|
|
78
|
+
|
|
72
79
|
@validates("email")
|
|
73
80
|
def _normalize_email(self, key, value):
|
|
74
81
|
# Login lowercases what it is given; store the same form so one address
|
|
@@ -55,6 +55,16 @@ if __name__ == "__main__":
|
|
|
55
55
|
unittest.main()
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
class PublicUrlDoctorTest(unittest.TestCase):
|
|
59
|
+
def test_warns_only_outside_development_without_a_public_url(self):
|
|
60
|
+
from stk.cli.doctor import _check_public_url
|
|
61
|
+
|
|
62
|
+
self.assertEqual(_check_public_url({})["status"], "warn")
|
|
63
|
+
self.assertEqual(_check_public_url({"STK_ENV": "development"})["status"], "ok")
|
|
64
|
+
set_url = {"STK_PUBLIC_URL": "https://app.example.com"}
|
|
65
|
+
self.assertEqual(_check_public_url(set_url)["status"], "ok")
|
|
66
|
+
|
|
67
|
+
|
|
58
68
|
class DowngradeArgumentTest(unittest.TestCase):
|
|
59
69
|
def test_relative_revision_is_accepted(self):
|
|
60
70
|
with patch("stk.cli.database.run_alembic") as run:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""stk_0007 confirms every account that existed before email confirmation."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from alembic import command
|
|
9
|
+
|
|
10
|
+
from stk.migrations import build_alembic_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfirmExistingUsersTest(unittest.TestCase):
|
|
14
|
+
def test_existing_accounts_count_as_confirmed(self):
|
|
15
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
16
|
+
path = Path(tmp) / "db.sqlite"
|
|
17
|
+
config = build_alembic_config(url=f"sqlite:///{path}", quiet=True)
|
|
18
|
+
command.upgrade(config, "stk_0006")
|
|
19
|
+
with sqlite3.connect(path) as db:
|
|
20
|
+
db.execute(
|
|
21
|
+
"INSERT INTO user (email, password, password_set, active,"
|
|
22
|
+
" fs_uniquifier, created_at, failed_login_count) VALUES"
|
|
23
|
+
" ('a@example.com', 'h', 1, 1, 'u', '2026-01-01', 0)"
|
|
24
|
+
)
|
|
25
|
+
command.upgrade(config, "heads")
|
|
26
|
+
with sqlite3.connect(path) as db:
|
|
27
|
+
rows = db.execute("select confirmed_at from user").fetchall()
|
|
28
|
+
self.assertIsNotNone(rows[0][0])
|
|
@@ -82,6 +82,19 @@ class FilesTests(unittest.IsolatedAsyncioTestCase):
|
|
|
82
82
|
def stored(self):
|
|
83
83
|
return sorted(p.name for p in Path(self.dir.name).rglob("*") if p.is_file())
|
|
84
84
|
|
|
85
|
+
def test_upload_limit_above_the_body_limit_refuses_to_start(self):
|
|
86
|
+
class Config(TestingConfig):
|
|
87
|
+
STK_UPLOAD_DIR = self.dir.name
|
|
88
|
+
STK_UPLOAD_MAX_BYTES = 20 * 1024 * 1024
|
|
89
|
+
|
|
90
|
+
with self.assertRaisesRegex(ValueError, "MAX_CONTENT_LENGTH"):
|
|
91
|
+
Stk(__name__, config=Config, extensions=[stk.files])
|
|
92
|
+
|
|
93
|
+
class Raised(Config):
|
|
94
|
+
MAX_CONTENT_LENGTH = 21 * 1024 * 1024
|
|
95
|
+
|
|
96
|
+
Stk(__name__, config=Raised, extensions=[stk.files])
|
|
97
|
+
|
|
85
98
|
async def test_same_bytes_are_stored_once(self):
|
|
86
99
|
alice, bob = await self.client(), await self.client("bob@example.com")
|
|
87
100
|
data = png()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Exercise stk's authentication integration with quart-security 2."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import re
|
|
3
5
|
import unittest
|
|
4
6
|
|
|
5
7
|
import pyotp
|
|
@@ -146,5 +148,105 @@ class SecurityCompatibilityTests(unittest.IsolatedAsyncioTestCase):
|
|
|
146
148
|
)
|
|
147
149
|
self.assertNotEqual((await second.get("/dashboard/")).status_code, 200)
|
|
148
150
|
|
|
151
|
+
def mailbox(self):
|
|
152
|
+
"""Turn on sign-up and reset with mail; collect what would be sent."""
|
|
153
|
+
self.app.config.update(
|
|
154
|
+
SECURITY_REGISTERABLE=True,
|
|
155
|
+
SECURITY_RECOVERABLE=True,
|
|
156
|
+
MAIL_SERVER="smtp.example.com",
|
|
157
|
+
SECURITY_PUBLIC_URL="http://localhost",
|
|
158
|
+
)
|
|
159
|
+
sent = []
|
|
160
|
+
|
|
161
|
+
async def send_mail(subject, recipient, body):
|
|
162
|
+
sent.append((subject, recipient, body))
|
|
163
|
+
|
|
164
|
+
self.app.extensions["security"].send_mail = send_mail
|
|
165
|
+
return sent
|
|
166
|
+
|
|
167
|
+
async def post(self, client, path, **form):
|
|
168
|
+
await client.get("/login")
|
|
169
|
+
async with client.session_transaction() as cookie:
|
|
170
|
+
form["csrf_token"] = cookie["_csrf_token"]
|
|
171
|
+
response = await client.post(path, form=form)
|
|
172
|
+
# Auth mail and account lookups run after the response; wait for them.
|
|
173
|
+
while self.app.background_tasks:
|
|
174
|
+
await asyncio.gather(*list(self.app.background_tasks))
|
|
175
|
+
return response
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def link(message):
|
|
179
|
+
return re.search(r"http://localhost(/\S+)", message[2]).group(1)
|
|
180
|
+
|
|
181
|
+
async def test_sign_up_waits_for_the_email_and_hides_existing_accounts(self):
|
|
182
|
+
sent = self.mailbox()
|
|
183
|
+
client = self.app.test_client()
|
|
184
|
+
password = "AnotherPassword123!"
|
|
185
|
+
for email in ("new@example.com", "USER@example.com"):
|
|
186
|
+
response = await self.post(
|
|
187
|
+
client,
|
|
188
|
+
"/register",
|
|
189
|
+
email=email,
|
|
190
|
+
password=password,
|
|
191
|
+
password_confirm=password,
|
|
192
|
+
)
|
|
193
|
+
self.assertEqual(response.status_code, 302)
|
|
194
|
+
self.assertEqual([m[1] for m in sent], ["new@example.com", "user@example.com"])
|
|
195
|
+
|
|
196
|
+
login = {"email": "new@example.com", "password": password}
|
|
197
|
+
self.assertNotIn("_user_id", await self.login_as(login))
|
|
198
|
+
# Opening the link confirms nothing; the button on that page does.
|
|
199
|
+
self.assertEqual((await client.get(self.link(sent[0]))).status_code, 200)
|
|
200
|
+
self.assertNotIn("_user_id", await self.login_as(login))
|
|
201
|
+
await self.post(client, self.link(sent[0]))
|
|
202
|
+
self.assertIn("_user_id", await self.login_as(login))
|
|
203
|
+
|
|
204
|
+
# A forged Host header never reaches the links in the mail.
|
|
205
|
+
await self.post(
|
|
206
|
+
client,
|
|
207
|
+
"/register",
|
|
208
|
+
email="user@example.com",
|
|
209
|
+
password=password,
|
|
210
|
+
password_confirm=password,
|
|
211
|
+
)
|
|
212
|
+
self.assertNotIn("evil.example", sent[-1][2])
|
|
213
|
+
|
|
214
|
+
async def login_as(self, form):
|
|
215
|
+
client = self.app.test_client()
|
|
216
|
+
await self.post(client, "/login", **form)
|
|
217
|
+
async with client.session_transaction() as cookie:
|
|
218
|
+
return dict(cookie)
|
|
219
|
+
|
|
220
|
+
async def test_password_reset_replaces_the_password_and_logs_activity(self):
|
|
221
|
+
sent = self.mailbox()
|
|
222
|
+
signed_in = self.app.test_client()
|
|
223
|
+
await self.login(signed_in)
|
|
224
|
+
client = self.app.test_client()
|
|
225
|
+
page = await client.get("/reset")
|
|
226
|
+
self.assertEqual(page.status_code, 200)
|
|
227
|
+
await self.post(client, "/reset", email="user@example.com")
|
|
228
|
+
path = self.link(sent[0])
|
|
229
|
+
self.assertEqual((await client.get(path)).status_code, 200)
|
|
230
|
+
new = "ReplacementPass123!"
|
|
231
|
+
response = await self.post(client, path, password=new, password_confirm=new)
|
|
232
|
+
self.assertEqual(response.status_code, 302)
|
|
233
|
+
self.assertIn(
|
|
234
|
+
"_user_id",
|
|
235
|
+
await self.login_as({"email": "user@example.com", "password": new}),
|
|
236
|
+
)
|
|
237
|
+
self.assertNotEqual((await signed_in.get("/dashboard/")).status_code, 200)
|
|
238
|
+
|
|
239
|
+
def test_sign_up_without_mail_refuses_to_start(self):
|
|
240
|
+
class Config(SecurityConfig):
|
|
241
|
+
SECURITY_REGISTERABLE = True
|
|
242
|
+
|
|
243
|
+
with self.assertRaisesRegex(RuntimeError, "MAIL_SERVER"):
|
|
244
|
+
Stk(__name__, config=Config)
|
|
245
|
+
|
|
246
|
+
class Unconfirmed(Config):
|
|
247
|
+
SECURITY_CONFIRMABLE = False
|
|
248
|
+
|
|
249
|
+
Stk(__name__, config=Unconfirmed)
|
|
250
|
+
|
|
149
251
|
def test_cookie_setting_preserves_local_http_support(self):
|
|
150
252
|
self.assertFalse(self.app.config["SESSION_COOKIE_SECURE"])
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
{% extends 'layout.html' %}
|
|
2
|
-
|
|
3
|
-
{% block content %}
|
|
4
|
-
<v-app id="app" class="justify-center align-center min-vh-100 d-flex">
|
|
5
|
-
<v-container class="d-flex align-center justify-center">
|
|
6
|
-
<div class="main">
|
|
7
|
-
<h2>Thank for your registration</h2>
|
|
8
|
-
<p>
|
|
9
|
-
Please confirm your Email to continue.
|
|
10
|
-
</p>
|
|
11
|
-
</div>
|
|
12
|
-
</v-container>
|
|
13
|
-
<v-snackbar absolute button v-model="snackBar">
|
|
14
|
-
${snackMessage}
|
|
15
|
-
<v-btn text color="primary" @click.native="value = false">Close</v-btn>
|
|
16
|
-
</v-snackbar>
|
|
17
|
-
</v-app>
|
|
18
|
-
{% endblock %}
|
|
19
|
-
|
|
20
|
-
{% block js %}
|
|
21
|
-
<script>
|
|
22
|
-
const {createApp} = Vue;
|
|
23
|
-
const {createVuetify} = Vuetify;
|
|
24
|
-
const vuetify = createVuetify(config.vuetifyConfig);
|
|
25
|
-
|
|
26
|
-
const app = createApp({
|
|
27
|
-
data() {
|
|
28
|
-
return {
|
|
29
|
-
snackBar: false,
|
|
30
|
-
snackMessage: '',
|
|
31
|
-
};
|
|
32
|
-
},
|
|
33
|
-
methods: {
|
|
34
|
-
showSnack(message) {
|
|
35
|
-
this.snackMessage = message;
|
|
36
|
-
this.snackBar = true;
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
delimiters: ['${', '}']
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
app.use(vuetify).mount('#app');
|
|
43
|
-
</script>
|
|
44
|
-
{% endblock %}
|