snackbase 0.12.1__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.
- snackbase/__init__.py +11 -0
- snackbase/__main__.py +10 -0
- snackbase/alembic/README +1 -0
- snackbase/alembic/env.py +123 -0
- snackbase/alembic/script.py.mako +28 -0
- snackbase/alembic/versions/038a7194dd07_enhance_password_reset_template.py +272 -0
- snackbase/alembic/versions/084e50e30ae8_create_collection_rules_table.py +131 -0
- snackbase/alembic/versions/0ea276bf29bc_add_email_sent_to_invitations.py +39 -0
- snackbase/alembic/versions/147572043440_seed_invitation_email_template.py +154 -0
- snackbase/alembic/versions/1799ee5d7311_add_email_tables.py +193 -0
- snackbase/alembic/versions/20260214_create_token_blacklist.py +38 -0
- snackbase/alembic/versions/20260219_add_is_default_to_configurations.py +44 -0
- snackbase/alembic/versions/20260330_create_webhooks_tables.py +201 -0
- snackbase/alembic/versions/20260401_create_jobs_table.py +159 -0
- snackbase/alembic/versions/20260402_create_hooks_table.py +131 -0
- snackbase/alembic/versions/20260403_add_hook_condition_and_executions.py +125 -0
- snackbase/alembic/versions/20260403_create_endpoints_tables.py +225 -0
- snackbase/alembic/versions/20260403_create_workflow_tables.py +256 -0
- snackbase/alembic/versions/20260725_create_codelists_tables.py +329 -0
- snackbase/alembic/versions/20260725_remove_builtin_regions_seed.py +38 -0
- snackbase/alembic/versions/20260803_create_functions_tables.py +266 -0
- snackbase/alembic/versions/20260803_encrypt_webhook_secrets.py +115 -0
- snackbase/alembic/versions/20260809_login_lockout.py +49 -0
- snackbase/alembic/versions/20260812_files_add_files_registry.py +88 -0
- snackbase/alembic/versions/3f032f63cb42_drop_permissions_table.py +58 -0
- snackbase/alembic/versions/60d278c6a3e3_add_timezone_support_to_all_tables.py +90 -0
- snackbase/alembic/versions/7bccaa56da18_create_api_keys_table.py +60 -0
- snackbase/alembic/versions/85e1c8c401fe_update_email_verification_template.py +246 -0
- snackbase/alembic/versions/8cebef609756_add_audit_log_triggers.py +105 -0
- snackbase/alembic/versions/aeef347267b5_initial_migration.py +264 -0
- snackbase/alembic/versions/b6a59f965907_add_configurations_table.py +197 -0
- snackbase/alembic/versions/b8e206c85cf5_add_variables_to_email_logs.py +41 -0
- snackbase/alembic/versions/bb38e4d658cd_create_password_reset_tokens_table.py +52 -0
- snackbase/alembic/versions/c69bf537b237_add_auth_provider_tracking_to_users.py +107 -0
- snackbase/alembic/versions/def3fc1039eb_add_email_verification_tokens_and_user_.py +76 -0
- snackbase/alembic/versions/e4ea5d0adb91_add_oauth_states_table.py +103 -0
- snackbase/alembic/versions/ea6f4f210d59_add_migration_revision_to_collections.py +35 -0
- snackbase/alembic/versions/f793cd9fde21_add_timezone_support_to_users_table.py +122 -0
- snackbase/alembic.ini +141 -0
- snackbase/application/__init__.py +0 -0
- snackbase/application/commands/__init__.py +0 -0
- snackbase/application/queries/__init__.py +0 -0
- snackbase/application/services/migration_query_service.py +295 -0
- snackbase/cli.py +1294 -0
- snackbase/core/__init__.py +23 -0
- snackbase/core/config.py +721 -0
- snackbase/core/configuration/config_registry.py +436 -0
- snackbase/core/context.py +39 -0
- snackbase/core/cron/__init__.py +9 -0
- snackbase/core/cron/parser.py +384 -0
- snackbase/core/cursor.py +49 -0
- snackbase/core/hooks/__init__.py +52 -0
- snackbase/core/hooks/hook_decorator.py +651 -0
- snackbase/core/hooks/hook_events.py +165 -0
- snackbase/core/hooks/hook_registry.py +444 -0
- snackbase/core/logging.py +215 -0
- snackbase/core/macros/engine.py +239 -0
- snackbase/core/macros/expander.py +109 -0
- snackbase/core/rules/__init__.py +48 -0
- snackbase/core/rules/aggregation_parser.py +391 -0
- snackbase/core/rules/ast.py +67 -0
- snackbase/core/rules/exceptions.py +15 -0
- snackbase/core/rules/expression_compiler.py +384 -0
- snackbase/core/rules/filter_compiler.py +222 -0
- snackbase/core/rules/filter_validator.py +245 -0
- snackbase/core/rules/lexer.py +283 -0
- snackbase/core/rules/parser.py +199 -0
- snackbase/core/rules/rule_validator.py +150 -0
- snackbase/core/rules/sql_compiler.py +213 -0
- snackbase/domain/__init__.py +0 -0
- snackbase/domain/entities/__init__.py +50 -0
- snackbase/domain/entities/account.py +52 -0
- snackbase/domain/entities/audit_log.py +95 -0
- snackbase/domain/entities/codelist.py +126 -0
- snackbase/domain/entities/collection.py +40 -0
- snackbase/domain/entities/collection_rule.py +139 -0
- snackbase/domain/entities/email_log.py +55 -0
- snackbase/domain/entities/email_template.py +59 -0
- snackbase/domain/entities/email_verification.py +67 -0
- snackbase/domain/entities/group.py +41 -0
- snackbase/domain/entities/hook_context.py +100 -0
- snackbase/domain/entities/invitation.py +63 -0
- snackbase/domain/entities/password_reset.py +66 -0
- snackbase/domain/entities/role.py +30 -0
- snackbase/domain/entities/user.py +64 -0
- snackbase/domain/services/__init__.py +75 -0
- snackbase/domain/services/account_code_generator.py +247 -0
- snackbase/domain/services/account_service.py +203 -0
- snackbase/domain/services/audit_checksum.py +101 -0
- snackbase/domain/services/audit_log_service.py +709 -0
- snackbase/domain/services/codelist_service.py +902 -0
- snackbase/domain/services/collection_service.py +699 -0
- snackbase/domain/services/collection_validator.py +696 -0
- snackbase/domain/services/dashboard_service.py +550 -0
- snackbase/domain/services/email_verification_service.py +144 -0
- snackbase/domain/services/encryption_migration_service.py +225 -0
- snackbase/domain/services/file_storage_service.py +414 -0
- snackbase/domain/services/password_reset_service.py +238 -0
- snackbase/domain/services/password_validator.py +138 -0
- snackbase/domain/services/pii_masking_service.py +260 -0
- snackbase/domain/services/record_validator.py +400 -0
- snackbase/domain/services/schema_diff.py +125 -0
- snackbase/domain/services/slug_generator.py +152 -0
- snackbase/domain/services/superadmin_service.py +191 -0
- snackbase/domain/services/user_field.py +56 -0
- snackbase/embedded.py +261 -0
- snackbase/infrastructure/__init__.py +30 -0
- snackbase/infrastructure/api/__init__.py +0 -0
- snackbase/infrastructure/api/app.py +1092 -0
- snackbase/infrastructure/api/dependencies.py +370 -0
- snackbase/infrastructure/api/middleware/__init__.py +21 -0
- snackbase/infrastructure/api/middleware/authorization.py +459 -0
- snackbase/infrastructure/api/middleware/client_ip.py +108 -0
- snackbase/infrastructure/api/middleware/context_middleware.py +56 -0
- snackbase/infrastructure/api/middleware/rate_limit_middleware.py +149 -0
- snackbase/infrastructure/api/middleware/rate_limit_storage.py +162 -0
- snackbase/infrastructure/api/middleware/security_headers_middleware.py +95 -0
- snackbase/infrastructure/api/routes/__init__.py +69 -0
- snackbase/infrastructure/api/routes/accounts_router.py +334 -0
- snackbase/infrastructure/api/routes/admin_router.py +919 -0
- snackbase/infrastructure/api/routes/api_keys_router.py +240 -0
- snackbase/infrastructure/api/routes/audit_log_router.py +162 -0
- snackbase/infrastructure/api/routes/auth_router.py +1142 -0
- snackbase/infrastructure/api/routes/backups_router.py +546 -0
- snackbase/infrastructure/api/routes/codelists_router.py +859 -0
- snackbase/infrastructure/api/routes/collection_rules_router.py +202 -0
- snackbase/infrastructure/api/routes/collections_router.py +739 -0
- snackbase/infrastructure/api/routes/custom_endpoint_dispatcher.py +420 -0
- snackbase/infrastructure/api/routes/dashboard_router.py +61 -0
- snackbase/infrastructure/api/routes/email_templates_router.py +471 -0
- snackbase/infrastructure/api/routes/endpoints_router.py +303 -0
- snackbase/infrastructure/api/routes/files_router.py +369 -0
- snackbase/infrastructure/api/routes/function_dispatcher.py +469 -0
- snackbase/infrastructure/api/routes/functions_router.py +711 -0
- snackbase/infrastructure/api/routes/groups_router.py +281 -0
- snackbase/infrastructure/api/routes/hooks_router.py +447 -0
- snackbase/infrastructure/api/routes/invitations_router.py +794 -0
- snackbase/infrastructure/api/routes/jobs_router.py +192 -0
- snackbase/infrastructure/api/routes/macros_router.py +316 -0
- snackbase/infrastructure/api/routes/migrations_router.py +420 -0
- snackbase/infrastructure/api/routes/oauth_router.py +622 -0
- snackbase/infrastructure/api/routes/realtime_router.py +229 -0
- snackbase/infrastructure/api/routes/records_router.py +2247 -0
- snackbase/infrastructure/api/routes/roles_router.py +330 -0
- snackbase/infrastructure/api/routes/saml_router.py +611 -0
- snackbase/infrastructure/api/routes/users_router.py +623 -0
- snackbase/infrastructure/api/routes/webhooks_router.py +284 -0
- snackbase/infrastructure/api/routes/workflows_router.py +576 -0
- snackbase/infrastructure/api/schemas/__init__.py +193 -0
- snackbase/infrastructure/api/schemas/account_schemas.py +87 -0
- snackbase/infrastructure/api/schemas/api_key_schemas.py +55 -0
- snackbase/infrastructure/api/schemas/audit_log_schemas.py +53 -0
- snackbase/infrastructure/api/schemas/auth_schemas.py +206 -0
- snackbase/infrastructure/api/schemas/backup_schemas.py +100 -0
- snackbase/infrastructure/api/schemas/codelist_schemas.py +177 -0
- snackbase/infrastructure/api/schemas/collection_schemas.py +444 -0
- snackbase/infrastructure/api/schemas/dashboard_schemas.py +222 -0
- snackbase/infrastructure/api/schemas/email_schemas.py +147 -0
- snackbase/infrastructure/api/schemas/endpoint_schemas.py +213 -0
- snackbase/infrastructure/api/schemas/file_schemas.py +26 -0
- snackbase/infrastructure/api/schemas/function_schemas.py +178 -0
- snackbase/infrastructure/api/schemas/group_schemas.py +52 -0
- snackbase/infrastructure/api/schemas/hook_schemas.py +160 -0
- snackbase/infrastructure/api/schemas/invitation_schemas.py +89 -0
- snackbase/infrastructure/api/schemas/job_schemas.py +50 -0
- snackbase/infrastructure/api/schemas/macro.py +124 -0
- snackbase/infrastructure/api/schemas/migration_query_schemas.py +121 -0
- snackbase/infrastructure/api/schemas/record_schemas.py +165 -0
- snackbase/infrastructure/api/schemas/role_schemas.py +84 -0
- snackbase/infrastructure/api/schemas/users_schemas.py +166 -0
- snackbase/infrastructure/api/schemas/webhook_schemas.py +133 -0
- snackbase/infrastructure/api/schemas/workflow_schemas.py +267 -0
- snackbase/infrastructure/auth/__init__.py +56 -0
- snackbase/infrastructure/auth/api_key_service.py +172 -0
- snackbase/infrastructure/auth/authenticator.py +537 -0
- snackbase/infrastructure/auth/jwt_service.py +215 -0
- snackbase/infrastructure/auth/login_throttle.py +106 -0
- snackbase/infrastructure/auth/middleware.py +72 -0
- snackbase/infrastructure/auth/password_hasher.py +100 -0
- snackbase/infrastructure/auth/platform_jwks.py +122 -0
- snackbase/infrastructure/auth/token_codec.py +113 -0
- snackbase/infrastructure/auth/token_types.py +62 -0
- snackbase/infrastructure/backup/__init__.py +7 -0
- snackbase/infrastructure/backup/alerting.py +173 -0
- snackbase/infrastructure/backup/archive.py +192 -0
- snackbase/infrastructure/backup/audit.py +106 -0
- snackbase/infrastructure/backup/destinations.py +560 -0
- snackbase/infrastructure/backup/lock.py +182 -0
- snackbase/infrastructure/backup/logical_snapshot.py +234 -0
- snackbase/infrastructure/backup/manifest.py +426 -0
- snackbase/infrastructure/backup/restart.py +52 -0
- snackbase/infrastructure/backup/restore.py +1017 -0
- snackbase/infrastructure/backup/retention.py +86 -0
- snackbase/infrastructure/backup/scheduler.py +235 -0
- snackbase/infrastructure/backup/service.py +518 -0
- snackbase/infrastructure/backup/sqlite_snapshot.py +83 -0
- snackbase/infrastructure/configuration/providers/__init__.py +5 -0
- snackbase/infrastructure/configuration/providers/auth/__init__.py +7 -0
- snackbase/infrastructure/configuration/providers/auth/email_password.py +50 -0
- snackbase/infrastructure/configuration/providers/backup/__init__.py +1 -0
- snackbase/infrastructure/configuration/providers/backup/backup_settings.py +151 -0
- snackbase/infrastructure/configuration/providers/email/aws_ses.py +88 -0
- snackbase/infrastructure/configuration/providers/email/resend.py +77 -0
- snackbase/infrastructure/configuration/providers/email/smtp.py +102 -0
- snackbase/infrastructure/configuration/providers/oauth/__init__.py +24 -0
- snackbase/infrastructure/configuration/providers/oauth/apple.py +221 -0
- snackbase/infrastructure/configuration/providers/oauth/github.py +195 -0
- snackbase/infrastructure/configuration/providers/oauth/google.py +174 -0
- snackbase/infrastructure/configuration/providers/oauth/microsoft.py +184 -0
- snackbase/infrastructure/configuration/providers/oauth/oauth_handler.py +198 -0
- snackbase/infrastructure/configuration/providers/saml/__init__.py +19 -0
- snackbase/infrastructure/configuration/providers/saml/assertion_validator.py +143 -0
- snackbase/infrastructure/configuration/providers/saml/azure_ad.py +236 -0
- snackbase/infrastructure/configuration/providers/saml/generic.py +292 -0
- snackbase/infrastructure/configuration/providers/saml/okta.py +242 -0
- snackbase/infrastructure/configuration/providers/saml/saml_handler.py +189 -0
- snackbase/infrastructure/configuration/providers/storage/__init__.py +10 -0
- snackbase/infrastructure/configuration/providers/storage/local.py +32 -0
- snackbase/infrastructure/configuration/providers/storage/s3.py +67 -0
- snackbase/infrastructure/configuration/providers/system/__init__.py +7 -0
- snackbase/infrastructure/configuration/providers/system/system_config.py +68 -0
- snackbase/infrastructure/endpoints/__init__.py +1 -0
- snackbase/infrastructure/endpoints/endpoint_executor.py +503 -0
- snackbase/infrastructure/functions/__init__.py +14 -0
- snackbase/infrastructure/functions/audit.py +63 -0
- snackbase/infrastructure/functions/bootstrap.py +170 -0
- snackbase/infrastructure/functions/concurrency.py +80 -0
- snackbase/infrastructure/functions/env_builder.py +216 -0
- snackbase/infrastructure/functions/invoke_action.py +162 -0
- snackbase/infrastructure/functions/nested_budget.py +65 -0
- snackbase/infrastructure/functions/pin_parser.py +117 -0
- snackbase/infrastructure/functions/redaction.py +61 -0
- snackbase/infrastructure/functions/retention.py +27 -0
- snackbase/infrastructure/functions/runner.py +539 -0
- snackbase/infrastructure/functions/sandbox.py +349 -0
- snackbase/infrastructure/functions/source_paths.py +56 -0
- snackbase/infrastructure/functions/syntax_preflight.py +46 -0
- snackbase/infrastructure/functions/worker_pool.py +110 -0
- snackbase/infrastructure/hooks/__init__.py +20 -0
- snackbase/infrastructure/hooks/action_executor.py +323 -0
- snackbase/infrastructure/hooks/api_defined_hook.py +328 -0
- snackbase/infrastructure/hooks/builtin_hooks.py +387 -0
- snackbase/infrastructure/hooks/webhook_hook.py +189 -0
- snackbase/infrastructure/persistence/__init__.py +0 -0
- snackbase/infrastructure/persistence/audit_helper.py +182 -0
- snackbase/infrastructure/persistence/database.py +1026 -0
- snackbase/infrastructure/persistence/event_listeners.py +360 -0
- snackbase/infrastructure/persistence/migration_service.py +652 -0
- snackbase/infrastructure/persistence/models/__init__.py +97 -0
- snackbase/infrastructure/persistence/models/account.py +112 -0
- snackbase/infrastructure/persistence/models/api_key.py +113 -0
- snackbase/infrastructure/persistence/models/audit_log.py +252 -0
- snackbase/infrastructure/persistence/models/codelist.py +298 -0
- snackbase/infrastructure/persistence/models/collection.py +67 -0
- snackbase/infrastructure/persistence/models/collection_rule.py +126 -0
- snackbase/infrastructure/persistence/models/configuration.py +240 -0
- snackbase/infrastructure/persistence/models/email_log.py +117 -0
- snackbase/infrastructure/persistence/models/email_template.py +133 -0
- snackbase/infrastructure/persistence/models/email_verification.py +87 -0
- snackbase/infrastructure/persistence/models/endpoint.py +138 -0
- snackbase/infrastructure/persistence/models/endpoint_execution.py +93 -0
- snackbase/infrastructure/persistence/models/file.py +93 -0
- snackbase/infrastructure/persistence/models/function.py +320 -0
- snackbase/infrastructure/persistence/models/group.py +105 -0
- snackbase/infrastructure/persistence/models/hook.py +133 -0
- snackbase/infrastructure/persistence/models/hook_execution.py +93 -0
- snackbase/infrastructure/persistence/models/invitation.py +110 -0
- snackbase/infrastructure/persistence/models/job.py +156 -0
- snackbase/infrastructure/persistence/models/macro.py +81 -0
- snackbase/infrastructure/persistence/models/password_reset.py +87 -0
- snackbase/infrastructure/persistence/models/refresh_token.py +80 -0
- snackbase/infrastructure/persistence/models/role.py +52 -0
- snackbase/infrastructure/persistence/models/token_blacklist.py +49 -0
- snackbase/infrastructure/persistence/models/user.py +232 -0
- snackbase/infrastructure/persistence/models/users_groups.py +38 -0
- snackbase/infrastructure/persistence/models/webhook.py +213 -0
- snackbase/infrastructure/persistence/models/workflow.py +115 -0
- snackbase/infrastructure/persistence/models/workflow_instance.py +105 -0
- snackbase/infrastructure/persistence/models/workflow_step_log.py +112 -0
- snackbase/infrastructure/persistence/record_snapshot.py +29 -0
- snackbase/infrastructure/persistence/repositories/__init__.py +96 -0
- snackbase/infrastructure/persistence/repositories/account_repository.py +324 -0
- snackbase/infrastructure/persistence/repositories/api_key_repository.py +166 -0
- snackbase/infrastructure/persistence/repositories/audit_log_repository.py +360 -0
- snackbase/infrastructure/persistence/repositories/codelist_repository.py +378 -0
- snackbase/infrastructure/persistence/repositories/collection_repository.py +194 -0
- snackbase/infrastructure/persistence/repositories/collection_rule_repository.py +156 -0
- snackbase/infrastructure/persistence/repositories/configuration_repository.py +240 -0
- snackbase/infrastructure/persistence/repositories/email_log_repository.py +114 -0
- snackbase/infrastructure/persistence/repositories/email_template_repository.py +189 -0
- snackbase/infrastructure/persistence/repositories/email_verification_repository.py +180 -0
- snackbase/infrastructure/persistence/repositories/endpoint_execution_repository.py +58 -0
- snackbase/infrastructure/persistence/repositories/endpoint_repository.py +185 -0
- snackbase/infrastructure/persistence/repositories/file_repository.py +52 -0
- snackbase/infrastructure/persistence/repositories/function_repository.py +304 -0
- snackbase/infrastructure/persistence/repositories/group_repository.py +221 -0
- snackbase/infrastructure/persistence/repositories/hook_execution_repository.py +81 -0
- snackbase/infrastructure/persistence/repositories/hook_repository.py +260 -0
- snackbase/infrastructure/persistence/repositories/invitation_repository.py +211 -0
- snackbase/infrastructure/persistence/repositories/job_repository.py +318 -0
- snackbase/infrastructure/persistence/repositories/macro_repository.py +160 -0
- snackbase/infrastructure/persistence/repositories/oauth_state_repository.py +93 -0
- snackbase/infrastructure/persistence/repositories/password_reset_repository.py +191 -0
- snackbase/infrastructure/persistence/repositories/record_repository.py +1198 -0
- snackbase/infrastructure/persistence/repositories/refresh_token_repository.py +137 -0
- snackbase/infrastructure/persistence/repositories/role_repository.py +113 -0
- snackbase/infrastructure/persistence/repositories/sync_audit_log_repository.py +89 -0
- snackbase/infrastructure/persistence/repositories/user_repository.py +449 -0
- snackbase/infrastructure/persistence/repositories/webhook_repository.py +235 -0
- snackbase/infrastructure/persistence/repositories/workflow_instance_repository.py +63 -0
- snackbase/infrastructure/persistence/repositories/workflow_repository.py +127 -0
- snackbase/infrastructure/persistence/repositories/workflow_step_log_repository.py +42 -0
- snackbase/infrastructure/persistence/table_builder.py +434 -0
- snackbase/infrastructure/realtime/__init__.py +0 -0
- snackbase/infrastructure/realtime/event_broadcaster.py +52 -0
- snackbase/infrastructure/realtime/realtime_auth.py +88 -0
- snackbase/infrastructure/realtime/realtime_manager.py +135 -0
- snackbase/infrastructure/realtime/realtime_policy.py +215 -0
- snackbase/infrastructure/security/__init__.py +45 -0
- snackbase/infrastructure/security/encryption.py +335 -0
- snackbase/infrastructure/security/field_encryption.py +224 -0
- snackbase/infrastructure/security/redaction.py +152 -0
- snackbase/infrastructure/security/scopes.py +67 -0
- snackbase/infrastructure/services/email/__init__.py +5 -0
- snackbase/infrastructure/services/email/aws_ses_provider.py +222 -0
- snackbase/infrastructure/services/email/email_provider.py +54 -0
- snackbase/infrastructure/services/email/resend_provider.py +198 -0
- snackbase/infrastructure/services/email/smtp_provider.py +129 -0
- snackbase/infrastructure/services/email/template_renderer.py +71 -0
- snackbase/infrastructure/services/email_service.py +550 -0
- snackbase/infrastructure/services/job_service.py +510 -0
- snackbase/infrastructure/services/scheduler_service.py +179 -0
- snackbase/infrastructure/services/token_service.py +39 -0
- snackbase/infrastructure/storage/__init__.py +18 -0
- snackbase/infrastructure/storage/base.py +49 -0
- snackbase/infrastructure/storage/local_storage_provider.py +54 -0
- snackbase/infrastructure/storage/s3_storage_provider.py +188 -0
- snackbase/infrastructure/storage/storage_service.py +140 -0
- snackbase/infrastructure/webhooks/__init__.py +17 -0
- snackbase/infrastructure/webhooks/webhook_service.py +572 -0
- snackbase/infrastructure/workflows/__init__.py +1 -0
- snackbase/infrastructure/workflows/workflow_executor.py +594 -0
- snackbase/infrastructure/workflows/workflow_trigger.py +226 -0
- snackbase/py.typed +0 -0
- snackbase/testing.py +76 -0
- snackbase-0.12.1.dist-info/METADATA +849 -0
- snackbase-0.12.1.dist-info/RECORD +350 -0
- snackbase-0.12.1.dist-info/WHEEL +4 -0
- snackbase-0.12.1.dist-info/entry_points.txt +2 -0
- snackbase-0.12.1.dist-info/licenses/LICENSE +633 -0
snackbase/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""SnackBase - Open-source Backend-as-a-Service.
|
|
2
|
+
|
|
3
|
+
A self-hosted alternative to PocketBase with multi-tenancy,
|
|
4
|
+
row-level security, and GxP-compliant audit logging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.12.1"
|
|
8
|
+
|
|
9
|
+
from snackbase.infrastructure.api.app import app
|
|
10
|
+
|
|
11
|
+
__all__ = ["app", "__version__"]
|
snackbase/__main__.py
ADDED
snackbase/alembic/README
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Generic single-database configuration with an async dbapi.
|
snackbase/alembic/env.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from logging.config import fileConfig
|
|
3
|
+
|
|
4
|
+
from alembic import context
|
|
5
|
+
from sqlalchemy import pool
|
|
6
|
+
from sqlalchemy.engine import Connection
|
|
7
|
+
from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
8
|
+
|
|
9
|
+
# Import SnackBase settings and models
|
|
10
|
+
from snackbase.core.config import get_settings
|
|
11
|
+
from snackbase.infrastructure.persistence import models # noqa: F401
|
|
12
|
+
from snackbase.infrastructure.persistence.database import Base
|
|
13
|
+
|
|
14
|
+
# this is the Alembic Config object, which provides
|
|
15
|
+
# access to the values within the .ini file in use.
|
|
16
|
+
config = context.config
|
|
17
|
+
|
|
18
|
+
# Interpret the config file for Python logging.
|
|
19
|
+
# This line sets up loggers basically.
|
|
20
|
+
if config.config_file_name is not None:
|
|
21
|
+
fileConfig(config.config_file_name)
|
|
22
|
+
|
|
23
|
+
# Set the target metadata for autogenerate support
|
|
24
|
+
target_metadata = Base.metadata
|
|
25
|
+
|
|
26
|
+
# Set the database URL from settings if not provided in config
|
|
27
|
+
# IMPORTANT: Always use the async URL here because async_engine_from_config needs it
|
|
28
|
+
# Alembic handles the async/sync conversion internally
|
|
29
|
+
settings = get_settings()
|
|
30
|
+
if not config.get_main_option("sqlalchemy.url"):
|
|
31
|
+
config.set_main_option("sqlalchemy.url", settings.database_url)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_migrations_offline() -> None:
|
|
35
|
+
"""Run migrations in 'offline' mode.
|
|
36
|
+
|
|
37
|
+
This configures the context with just a URL
|
|
38
|
+
and not an Engine, though an Engine is acceptable
|
|
39
|
+
here as well. By skipping the Engine creation
|
|
40
|
+
we don't even need a DBAPI to be available.
|
|
41
|
+
|
|
42
|
+
Calls to context.execute() here emit the given string to the
|
|
43
|
+
script output.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
47
|
+
context.configure(
|
|
48
|
+
url=url,
|
|
49
|
+
target_metadata=target_metadata,
|
|
50
|
+
literal_binds=True,
|
|
51
|
+
dialect_opts={"paramstyle": "named"},
|
|
52
|
+
render_as_batch=True if url and url.startswith("sqlite") else False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
with context.begin_transaction():
|
|
56
|
+
context.run_migrations()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def do_run_migrations(connection: Connection) -> None:
|
|
60
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
61
|
+
context.configure(
|
|
62
|
+
connection=connection,
|
|
63
|
+
target_metadata=target_metadata,
|
|
64
|
+
render_as_batch=True if url and url.startswith("sqlite") else False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Use existing transaction if connection is provided, otherwise start a new one
|
|
68
|
+
if config.attributes.get("connection"):
|
|
69
|
+
context.run_migrations()
|
|
70
|
+
else:
|
|
71
|
+
with context.begin_transaction():
|
|
72
|
+
context.run_migrations()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def run_async_migrations() -> None:
|
|
76
|
+
"""In this scenario we need to create an Engine
|
|
77
|
+
and associate a connection with the context.
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
connectable = async_engine_from_config(
|
|
82
|
+
config.get_section(config.config_ini_section, {}),
|
|
83
|
+
prefix="sqlalchemy.",
|
|
84
|
+
poolclass=pool.NullPool,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async with connectable.connect() as connection:
|
|
88
|
+
await connection.run_sync(do_run_migrations)
|
|
89
|
+
|
|
90
|
+
await connectable.dispose()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_migrations_online() -> None:
|
|
94
|
+
"""Run migrations in 'online' mode."""
|
|
95
|
+
|
|
96
|
+
# Check if a connection was passed in the configuration attributes
|
|
97
|
+
# This is used by the application and tests to share a connection
|
|
98
|
+
connection = config.attributes.get("connection")
|
|
99
|
+
|
|
100
|
+
if connection is not None:
|
|
101
|
+
do_run_migrations(connection)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# For async support, we need to handle the event loop differently
|
|
105
|
+
# if it's already running (e.g. when called from application code)
|
|
106
|
+
try:
|
|
107
|
+
loop = asyncio.get_running_loop()
|
|
108
|
+
except RuntimeError:
|
|
109
|
+
loop = None
|
|
110
|
+
|
|
111
|
+
if loop and loop.is_running():
|
|
112
|
+
# This is used for CLI use when an event loop is already running
|
|
113
|
+
# (e.g. when calling from pytest-asyncio or a long-running app)
|
|
114
|
+
# Note: In production, it's better to run migrations before the loop starts
|
|
115
|
+
asyncio.ensure_future(run_async_migrations())
|
|
116
|
+
else:
|
|
117
|
+
asyncio.run(run_async_migrations())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if context.is_offline_mode():
|
|
121
|
+
run_migrations_offline()
|
|
122
|
+
else:
|
|
123
|
+
run_migrations_online()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
${imports if imports else ""}
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = ${repr(up_revision)}
|
|
16
|
+
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
"""Upgrade schema."""
|
|
23
|
+
${upgrades if upgrades else "pass"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
"""Downgrade schema."""
|
|
28
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""enhance password reset template
|
|
2
|
+
|
|
3
|
+
Revision ID: 038a7194dd07
|
|
4
|
+
Revises: bb38e4d658cd
|
|
5
|
+
Create Date: 2026-01-10 18:05:52.469527
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
import sqlalchemy as sa
|
|
11
|
+
from alembic import op
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision: str = '038a7194dd07'
|
|
15
|
+
down_revision: str | Sequence[str] | None = 'bb38e4d658cd'
|
|
16
|
+
branch_labels: str | Sequence[str] | None = None
|
|
17
|
+
depends_on: str | Sequence[str] | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SYSTEM_ACCOUNT_ID = "00000000-0000-0000-0000-000000000000"
|
|
21
|
+
|
|
22
|
+
NEW_SUBJECT = "Reset your {{ app_name }} password"
|
|
23
|
+
NEW_HTML_BODY = """
|
|
24
|
+
<!DOCTYPE html>
|
|
25
|
+
<html>
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="UTF-8">
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
29
|
+
<title>Reset Your Password</title>
|
|
30
|
+
<style>
|
|
31
|
+
@media only screen and (max-width: 600px) {
|
|
32
|
+
.container { padding: 10px !important; }
|
|
33
|
+
.button { padding: 10px 20px !important; font-size: 14px !important; }
|
|
34
|
+
h1 { font-size: 24px !important; }
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
</head>
|
|
38
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f7fa;">
|
|
39
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 20px;">
|
|
40
|
+
<tr>
|
|
41
|
+
<td align="center">
|
|
42
|
+
<table role="presentation" class="container" width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 40px;">
|
|
43
|
+
<!-- Header -->
|
|
44
|
+
<tr>
|
|
45
|
+
<td style="text-align: center; padding-bottom: 30px;">
|
|
46
|
+
<h1 style="margin: 0; color: #2c3e50; font-size: 28px; font-weight: 600;">{{ app_name }}</h1>
|
|
47
|
+
</td>
|
|
48
|
+
</tr>
|
|
49
|
+
<!-- Content -->
|
|
50
|
+
<tr>
|
|
51
|
+
<td>
|
|
52
|
+
<h2 style="margin: 0 0 20px 0; color: #2c3e50; font-size: 24px; font-weight: 600;">Reset Your Password</h2>
|
|
53
|
+
<p style="margin: 0 0 16px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">Hello{% if user_name %} {{ user_name }}{% endif %},</p>
|
|
54
|
+
<p style="margin: 0 0 16px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">We received a request to reset the password for your {{ app_name }} account{% if user_email %} ({{ user_email }}){% endif %}. Click the button below to create a new password:</p>
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
<!-- Button -->
|
|
58
|
+
<tr>
|
|
59
|
+
<td style="text-align: center; padding: 30px 0;">
|
|
60
|
+
<a href="{{ reset_url }}" class="button" style="display: inline-block; background-color: #e74c3c; color: #ffffff; text-decoration: none; padding: 14px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(231,76,60,0.3);">Reset Password</a>
|
|
61
|
+
</td>
|
|
62
|
+
</tr>
|
|
63
|
+
<!-- Alternative Link -->
|
|
64
|
+
<tr>
|
|
65
|
+
<td>
|
|
66
|
+
<p style="margin: 0 0 8px 0; color: #718096; font-size: 14px;">Or copy and paste this link into your browser:</p>
|
|
67
|
+
<p style="margin: 0 0 24px 0; word-break: break-all; color: #e74c3c; font-size: 14px;">{{ reset_url }}</p>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<!-- Token Info -->
|
|
71
|
+
{% if token %}
|
|
72
|
+
<tr>
|
|
73
|
+
<td style="background-color: #fef5f5; padding: 16px; border-radius: 6px; border-left: 4px solid #e74c3c;">
|
|
74
|
+
<p style="margin: 0 0 8px 0; color: #4a5568; font-size: 14px; font-weight: 600;">Reset Code:</p>
|
|
75
|
+
<p style="margin: 0; color: #2d3748; font-size: 16px; font-family: 'Courier New', monospace; letter-spacing: 2px;">{{ token }}</p>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
{% endif %}
|
|
79
|
+
<!-- Expiration -->
|
|
80
|
+
{% if expires_at %}
|
|
81
|
+
<tr>
|
|
82
|
+
<td style="padding-top: 20px;">
|
|
83
|
+
<p style="margin: 0; color: #718096; font-size: 14px;">⏰ This password reset link will expire on {{ expires_at }}.</p>
|
|
84
|
+
</td>
|
|
85
|
+
</tr>
|
|
86
|
+
{% endif %}
|
|
87
|
+
<!-- Security Warning -->
|
|
88
|
+
<tr>
|
|
89
|
+
<td style="padding-top: 20px;">
|
|
90
|
+
<div style="background-color: #fff5f5; border-left: 4px solid #fc8181; padding: 16px; border-radius: 6px;">
|
|
91
|
+
<p style="margin: 0 0 8px 0; color: #c53030; font-size: 14px; font-weight: 600;">🔒 Security Notice</p>
|
|
92
|
+
<p style="margin: 0; color: #742a2a; font-size: 13px; line-height: 1.5;">If you didn't request a password reset, please ignore this email. Your password will remain unchanged.{% if ip_address %} This request was initiated from IP address: <strong>{{ ip_address }}</strong>.{% endif %} For security, we recommend changing your password if you suspect unauthorized access to your account.</p>
|
|
93
|
+
</div>
|
|
94
|
+
</td>
|
|
95
|
+
</tr>
|
|
96
|
+
<!-- Footer -->
|
|
97
|
+
<tr>
|
|
98
|
+
<td style="padding-top: 30px; border-top: 1px solid #e2e8f0; margin-top: 30px;">
|
|
99
|
+
<p style="margin: 0 0 8px 0; color: #a0aec0; font-size: 13px; line-height: 1.5;">This password reset was requested from your {{ app_name }} account.</p>
|
|
100
|
+
<p style="margin: 0; color: #a0aec0; font-size: 13px;">Need help? Visit <a href="{{ app_url }}" style="color: #3498db; text-decoration: none;">{{ app_name }}</a></p>
|
|
101
|
+
</td>
|
|
102
|
+
</tr>
|
|
103
|
+
</table>
|
|
104
|
+
</td>
|
|
105
|
+
</tr>
|
|
106
|
+
</table>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
""".strip()
|
|
110
|
+
|
|
111
|
+
NEW_TEXT_BODY = """
|
|
112
|
+
{{ app_name }} - Reset Your Password
|
|
113
|
+
|
|
114
|
+
Hello{% if user_name %} {{ user_name }}{% endif %},
|
|
115
|
+
|
|
116
|
+
We received a request to reset the password for your {{ app_name }} account{% if user_email %} ({{ user_email }}){% endif %}. To create a new password, visit the following link:
|
|
117
|
+
|
|
118
|
+
{{ reset_url }}
|
|
119
|
+
|
|
120
|
+
{% if token %}Reset Code: {{ token }}{% endif %}
|
|
121
|
+
|
|
122
|
+
{% if expires_at %}This password reset link will expire on {{ expires_at }}.{% endif %}
|
|
123
|
+
|
|
124
|
+
SECURITY NOTICE:
|
|
125
|
+
If you didn't request a password reset, please ignore this email. Your password will remain unchanged.{% if ip_address %} This request was initiated from IP address: {{ ip_address }}.{% endif %} For security, we recommend changing your password if you suspect unauthorized access to your account.
|
|
126
|
+
|
|
127
|
+
This password reset was requested from your {{ app_name }} account.
|
|
128
|
+
|
|
129
|
+
Need help? Visit {{ app_url }}
|
|
130
|
+
""".strip()
|
|
131
|
+
|
|
132
|
+
OLD_SUBJECT = "Reset your password for {{ app_name }}"
|
|
133
|
+
OLD_HTML_BODY = """
|
|
134
|
+
<!DOCTYPE html>
|
|
135
|
+
<html>
|
|
136
|
+
<head>
|
|
137
|
+
<meta charset="UTF-8">
|
|
138
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
139
|
+
<title>Reset Your Password</title>
|
|
140
|
+
<style>
|
|
141
|
+
@media only screen and (max-width: 600px) {
|
|
142
|
+
.container { padding: 10px !important; }
|
|
143
|
+
.button { padding: 10px 20px !important; font-size: 14px !important; }
|
|
144
|
+
h1 { font-size: 24px !important; }
|
|
145
|
+
}
|
|
146
|
+
</style>
|
|
147
|
+
</head>
|
|
148
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f7fa;">
|
|
149
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7fa; padding: 40px 20px;">
|
|
150
|
+
<tr>
|
|
151
|
+
<td align="center">
|
|
152
|
+
<table role="presentation" class="container" width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 40px;">
|
|
153
|
+
<!-- Header -->
|
|
154
|
+
<tr>
|
|
155
|
+
<td style="text-align: center; padding-bottom: 30px;">
|
|
156
|
+
<h1 style="margin: 0; color: #2c3e50; font-size: 28px; font-weight: 600;">{{ app_name }}</h1>
|
|
157
|
+
</td>
|
|
158
|
+
</tr>
|
|
159
|
+
<!-- Content -->
|
|
160
|
+
<tr>
|
|
161
|
+
<td>
|
|
162
|
+
<h2 style="margin: 0 0 20px 0; color: #2c3e50; font-size: 24px; font-weight: 600;">Reset Your Password</h2>
|
|
163
|
+
<p style="margin: 0 0 16px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">Hello{% if user_name %} {{ user_name }}{% endif %},</p>
|
|
164
|
+
<p style="margin: 0 0 16px 0; color: #4a5568; font-size: 16px; line-height: 1.6;">We received a request to reset the password for your {{ app_name }} account{% if user_email %} ({{ user_email }}){% endif %}. Click the button below to create a new password:</p>
|
|
165
|
+
</td>
|
|
166
|
+
</tr>
|
|
167
|
+
<!-- Button -->
|
|
168
|
+
<tr>
|
|
169
|
+
<td style="text-align: center; padding: 30px 0;">
|
|
170
|
+
<a href="{{ reset_url }}" class="button" style="display: inline-block; background-color: #e74c3c; color: #ffffff; text-decoration: none; padding: 14px 40px; border-radius: 6px; font-size: 16px; font-weight: 600; box-shadow: 0 2px 4px rgba(231,76,60,0.3);">Reset Password</a>
|
|
171
|
+
</td>
|
|
172
|
+
</tr>
|
|
173
|
+
<!-- Alternative Link -->
|
|
174
|
+
<tr>
|
|
175
|
+
<td>
|
|
176
|
+
<p style="margin: 0 0 8px 0; color: #718096; font-size: 14px;">Or copy and paste this link into your browser:</p>
|
|
177
|
+
<p style="margin: 0 0 24px 0; word-break: break-all; color: #e74c3c; font-size: 14px;">{{ reset_url }}</p>
|
|
178
|
+
</td>
|
|
179
|
+
</tr>
|
|
180
|
+
<!-- Token Info -->
|
|
181
|
+
{% if token %}
|
|
182
|
+
<tr>
|
|
183
|
+
<td style="background-color: #fef5f5; padding: 16px; border-radius: 6px; border-left: 4px solid #e74c3c;">
|
|
184
|
+
<p style="margin: 0 0 8px 0; color: #4a5568; font-size: 14px; font-weight: 600;">Reset Code:</p>
|
|
185
|
+
<p style="margin: 0; color: #2d3748; font-size: 16px; font-family: 'Courier New', monospace; letter-spacing: 2px;">{{ token }}</p>
|
|
186
|
+
</td>
|
|
187
|
+
</tr>
|
|
188
|
+
{% endif %}
|
|
189
|
+
<!-- Expiration -->
|
|
190
|
+
{% if expires_at %}
|
|
191
|
+
<tr>
|
|
192
|
+
<td style="padding-top: 20px;">
|
|
193
|
+
<p style="margin: 0; color: #718096; font-size: 14px;">⏰ This password reset link will expire on {{ expires_at }}.</p>
|
|
194
|
+
</td>
|
|
195
|
+
</tr>
|
|
196
|
+
{% endif %}
|
|
197
|
+
<!-- Security Warning -->
|
|
198
|
+
<tr>
|
|
199
|
+
<td style="padding-top: 20px;">
|
|
200
|
+
<div style="background-color: #fff5f5; border-left: 4px solid #fc8181; padding: 16px; border-radius: 6px;">
|
|
201
|
+
<p style="margin: 0 0 8px 0; color: #c53030; font-size: 14px; font-weight: 600;">🔒 Security Notice</p>
|
|
202
|
+
<p style="margin: 0; color: #742a2a; font-size: 13px; line-height: 1.5;">If you didn't request a password reset, please ignore this email. Your password will remain unchanged. For security, we recommend changing your password if you suspect unauthorized access to your account.</p>
|
|
203
|
+
</div>
|
|
204
|
+
</td>
|
|
205
|
+
</tr>
|
|
206
|
+
<!-- Footer -->
|
|
207
|
+
<tr>
|
|
208
|
+
<td style="padding-top: 30px; border-top: 1px solid #e2e8f0; margin-top: 30px;">
|
|
209
|
+
<p style="margin: 0 0 8px 0; color: #a0aec0; font-size: 13px; line-height: 1.5;">This password reset was requested from your {{ app_name }} account.</p>
|
|
210
|
+
<p style="margin: 0; color: #a0aec0; font-size: 13px;">Need help? Visit <a href="{{ app_url }}" style="color: #3498db; text-decoration: none;">{{ app_name }}</a></p>
|
|
211
|
+
</td>
|
|
212
|
+
</tr>
|
|
213
|
+
</table>
|
|
214
|
+
</td>
|
|
215
|
+
</tr>
|
|
216
|
+
</table>
|
|
217
|
+
</body>
|
|
218
|
+
</html>
|
|
219
|
+
""".strip()
|
|
220
|
+
|
|
221
|
+
OLD_TEXT_BODY = """
|
|
222
|
+
{{ app_name }} - Reset Your Password
|
|
223
|
+
|
|
224
|
+
Hello{% if user_name %} {{ user_name }}{% endif %},
|
|
225
|
+
|
|
226
|
+
We received a request to reset the password for your {{ app_name }} account{% if user_email %} ({{ user_email }}){% endif %}. To create a new password, visit the following link:
|
|
227
|
+
|
|
228
|
+
{{ reset_url }}
|
|
229
|
+
|
|
230
|
+
{% if token %}Reset Code: {{ token }}{% endif %}
|
|
231
|
+
|
|
232
|
+
{% if expires_at %}This password reset link will expire on {{ expires_at }}.{% endif %}
|
|
233
|
+
|
|
234
|
+
SECURITY NOTICE:
|
|
235
|
+
If you didn't request a password reset, please ignore this email. Your password will remain unchanged. For security, we recommend changing your password if you suspect unauthorized access to your account.
|
|
236
|
+
|
|
237
|
+
This password reset was requested from your {{ app_name }} account.
|
|
238
|
+
|
|
239
|
+
Need help? Visit {{ app_url }}
|
|
240
|
+
""".strip()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def upgrade() -> None:
|
|
244
|
+
"""Upgrade schema."""
|
|
245
|
+
op.execute(
|
|
246
|
+
sa.text(
|
|
247
|
+
"UPDATE email_templates "
|
|
248
|
+
"SET subject = :subject, html_body = :html_body, text_body = :text_body, updated_at = CURRENT_TIMESTAMP "
|
|
249
|
+
"WHERE account_id = :account_id AND template_type = 'password_reset'"
|
|
250
|
+
).bindparams(
|
|
251
|
+
subject=NEW_SUBJECT,
|
|
252
|
+
html_body=NEW_HTML_BODY,
|
|
253
|
+
text_body=NEW_TEXT_BODY,
|
|
254
|
+
account_id=SYSTEM_ACCOUNT_ID
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def downgrade() -> None:
|
|
260
|
+
"""Downgrade schema."""
|
|
261
|
+
op.execute(
|
|
262
|
+
sa.text(
|
|
263
|
+
"UPDATE email_templates "
|
|
264
|
+
"SET subject = :subject, html_body = :html_body, text_body = :text_body, updated_at = CURRENT_TIMESTAMP "
|
|
265
|
+
"WHERE account_id = :account_id AND template_type = 'password_reset'"
|
|
266
|
+
).bindparams(
|
|
267
|
+
subject=OLD_SUBJECT,
|
|
268
|
+
html_body=OLD_HTML_BODY,
|
|
269
|
+
text_body=OLD_TEXT_BODY,
|
|
270
|
+
account_id=SYSTEM_ACCOUNT_ID
|
|
271
|
+
)
|
|
272
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""create_collection_rules_table
|
|
2
|
+
|
|
3
|
+
Revision ID: 084e50e30ae8
|
|
4
|
+
Revises: 7bccaa56da18
|
|
5
|
+
Create Date: 2026-01-19 00:19:12.172897
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = "084e50e30ae8"
|
|
16
|
+
down_revision: str | Sequence[str] | None = "7bccaa56da18"
|
|
17
|
+
branch_labels: str | Sequence[str] | None = None
|
|
18
|
+
depends_on: str | Sequence[str] | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
"""Create collection_rules table."""
|
|
23
|
+
op.create_table(
|
|
24
|
+
"collection_rules",
|
|
25
|
+
sa.Column(
|
|
26
|
+
"id", sa.String(length=36), nullable=False, comment="Collection rule ID (UUID)"
|
|
27
|
+
),
|
|
28
|
+
sa.Column(
|
|
29
|
+
"collection_id",
|
|
30
|
+
sa.String(length=36),
|
|
31
|
+
nullable=False,
|
|
32
|
+
comment="Foreign key to collections table",
|
|
33
|
+
),
|
|
34
|
+
# 5 operation rules (NULL = locked, "" = public, "expr" = filter)
|
|
35
|
+
sa.Column(
|
|
36
|
+
"list_rule",
|
|
37
|
+
sa.Text(),
|
|
38
|
+
nullable=True,
|
|
39
|
+
comment="Filter expression for listing records",
|
|
40
|
+
),
|
|
41
|
+
sa.Column(
|
|
42
|
+
"view_rule",
|
|
43
|
+
sa.Text(),
|
|
44
|
+
nullable=True,
|
|
45
|
+
comment="Filter expression for viewing single record",
|
|
46
|
+
),
|
|
47
|
+
sa.Column(
|
|
48
|
+
"create_rule",
|
|
49
|
+
sa.Text(),
|
|
50
|
+
nullable=True,
|
|
51
|
+
comment="Validation expression for creating records",
|
|
52
|
+
),
|
|
53
|
+
sa.Column(
|
|
54
|
+
"update_rule",
|
|
55
|
+
sa.Text(),
|
|
56
|
+
nullable=True,
|
|
57
|
+
comment="Filter/validation expression for updates",
|
|
58
|
+
),
|
|
59
|
+
sa.Column(
|
|
60
|
+
"delete_rule",
|
|
61
|
+
sa.Text(),
|
|
62
|
+
nullable=True,
|
|
63
|
+
comment="Filter expression for deletions",
|
|
64
|
+
),
|
|
65
|
+
# Field-level permissions per operation
|
|
66
|
+
sa.Column(
|
|
67
|
+
"list_fields",
|
|
68
|
+
sa.Text(),
|
|
69
|
+
nullable=False,
|
|
70
|
+
server_default="*",
|
|
71
|
+
comment="Fields visible in list operations (JSON array or '*')",
|
|
72
|
+
),
|
|
73
|
+
sa.Column(
|
|
74
|
+
"view_fields",
|
|
75
|
+
sa.Text(),
|
|
76
|
+
nullable=False,
|
|
77
|
+
server_default="*",
|
|
78
|
+
comment="Fields visible in view operations (JSON array or '*')",
|
|
79
|
+
),
|
|
80
|
+
sa.Column(
|
|
81
|
+
"create_fields",
|
|
82
|
+
sa.Text(),
|
|
83
|
+
nullable=False,
|
|
84
|
+
server_default="*",
|
|
85
|
+
comment="Fields allowed in create requests (JSON array or '*')",
|
|
86
|
+
),
|
|
87
|
+
sa.Column(
|
|
88
|
+
"update_fields",
|
|
89
|
+
sa.Text(),
|
|
90
|
+
nullable=False,
|
|
91
|
+
server_default="*",
|
|
92
|
+
comment="Fields allowed in update requests (JSON array or '*')",
|
|
93
|
+
),
|
|
94
|
+
# Timestamps
|
|
95
|
+
sa.Column(
|
|
96
|
+
"created_at",
|
|
97
|
+
sa.DateTime(),
|
|
98
|
+
nullable=False,
|
|
99
|
+
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
|
100
|
+
),
|
|
101
|
+
sa.Column(
|
|
102
|
+
"updated_at",
|
|
103
|
+
sa.DateTime(),
|
|
104
|
+
nullable=False,
|
|
105
|
+
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
|
106
|
+
),
|
|
107
|
+
# Constraints
|
|
108
|
+
sa.ForeignKeyConstraint(
|
|
109
|
+
["collection_id"],
|
|
110
|
+
["collections.id"],
|
|
111
|
+
ondelete="CASCADE",
|
|
112
|
+
),
|
|
113
|
+
sa.PrimaryKeyConstraint("id"),
|
|
114
|
+
sa.UniqueConstraint("collection_id", name="uq_collection_rules_collection_id"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Create index on collection_id for performance
|
|
118
|
+
with op.batch_alter_table("collection_rules", schema=None) as batch_op:
|
|
119
|
+
batch_op.create_index(
|
|
120
|
+
batch_op.f("ix_collection_rules_collection_id"),
|
|
121
|
+
["collection_id"],
|
|
122
|
+
unique=False,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def downgrade() -> None:
|
|
127
|
+
"""Drop collection_rules table."""
|
|
128
|
+
with op.batch_alter_table("collection_rules", schema=None) as batch_op:
|
|
129
|
+
batch_op.drop_index(batch_op.f("ix_collection_rules_collection_id"))
|
|
130
|
+
|
|
131
|
+
op.drop_table("collection_rules")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""add_email_sent_to_invitations
|
|
2
|
+
|
|
3
|
+
Revision ID: 0ea276bf29bc
|
|
4
|
+
Revises: 038a7194dd07
|
|
5
|
+
Create Date: 2026-01-10 18:58:33.816670
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
import sqlalchemy as sa
|
|
11
|
+
from alembic import op
|
|
12
|
+
|
|
13
|
+
# revision identifiers, used by Alembic.
|
|
14
|
+
revision: str = '0ea276bf29bc'
|
|
15
|
+
down_revision: str | Sequence[str] | None = '038a7194dd07'
|
|
16
|
+
branch_labels: str | Sequence[str] | None = None
|
|
17
|
+
depends_on: str | Sequence[str] | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
"""Upgrade schema."""
|
|
22
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
23
|
+
with op.batch_alter_table('invitations', schema=None) as batch_op:
|
|
24
|
+
batch_op.add_column(sa.Column('email_sent', sa.Boolean(), nullable=False, server_default=sa.text('false'), comment='Whether the invitation email has been sent'))
|
|
25
|
+
batch_op.add_column(sa.Column('email_sent_at', sa.DateTime(), nullable=True, comment='Timestamp when the email was sent'))
|
|
26
|
+
batch_op.create_index(batch_op.f('ix_invitations_email_sent'), ['email_sent'], unique=False)
|
|
27
|
+
|
|
28
|
+
# ### end Alembic commands ###
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def downgrade() -> None:
|
|
32
|
+
"""Downgrade schema."""
|
|
33
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
34
|
+
with op.batch_alter_table('invitations', schema=None) as batch_op:
|
|
35
|
+
batch_op.drop_index(batch_op.f('ix_invitations_email_sent'))
|
|
36
|
+
batch_op.drop_column('email_sent_at')
|
|
37
|
+
batch_op.drop_column('email_sent')
|
|
38
|
+
|
|
39
|
+
# ### end Alembic commands ###
|