release: v0.5.0 - Authentication, API Keys & Advanced Features
Some checks failed
E2E Tests / Run E2E Tests (push) Has been cancelled
E2E Tests / Visual Regression Tests (push) Has been cancelled
E2E Tests / Smoke Tests (push) Has been cancelled

Complete v0.5.0 implementation:

Database (@db-engineer):
- 3 migrations: users, api_keys, report_schedules tables
- Foreign keys, indexes, constraints, enums

Backend (@backend-dev):
- JWT authentication service with bcrypt (cost=12)
- Auth endpoints: /register, /login, /refresh, /me
- API Keys service with hash storage and prefix validation
- API Keys endpoints: CRUD + rotate
- Security module with JWT HS256

Frontend (@frontend-dev):
- Login/Register pages with validation
- AuthContext with localStorage persistence
- Protected routes implementation
- API Keys management UI (create, revoke, rotate)
- Header with user dropdown

DevOps (@devops-engineer):
- .env.example and .env.production.example
- docker-compose.scheduler.yml
- scripts/setup-secrets.sh
- INFRASTRUCTURE_SETUP.md

QA (@qa-engineer):
- 85 E2E tests: auth.spec.ts, apikeys.spec.ts, scenarios.spec.ts, regression-v050.spec.ts
- auth-helpers.ts with 20+ utility functions
- Test plans and documentation

Architecture (@spec-architect):
- SECURITY.md with best practices
- SECURITY-CHECKLIST.md pre-deployment
- Updated architecture.md with auth flows
- Updated README.md with v0.5.0 features

Documentation:
- Updated todo.md with v0.5.0 status
- Added docs/README.md index
- Complete setup instructions

Dependencies added:
- bcrypt, python-jose, passlib, email-validator

Tested: JWT auth flow, API keys CRUD, protected routes, 85 E2E tests ready

Closes: v0.5.0 milestone
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 19:22:47 +02:00
parent 9b9297b7dc
commit cc60ba17ea
49 changed files with 9847 additions and 176 deletions

View File

@@ -0,0 +1,157 @@
"""create report schedules table
Revision ID: efe19595299c
Revises: 6512af98fb22
Create Date: 2026-04-07 14:02:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = "efe19595299c"
down_revision: Union[str, Sequence[str], None] = "6512af98fb22"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# Create enums
frequency_enum = sa.Enum(
"daily", "weekly", "monthly", name="report_schedule_frequency"
)
frequency_enum.create(op.get_bind(), checkfirst=True)
format_enum = sa.Enum("pdf", "csv", name="report_schedule_format")
format_enum.create(op.get_bind(), checkfirst=True)
# Create report_schedules table
op.create_table(
"report_schedules",
sa.Column(
"id",
postgresql.UUID(as_uuid=True),
primary_key=True,
server_default=sa.text("uuid_generate_v4()"),
),
sa.Column(
"user_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"scenario_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("scenarios.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("name", sa.String(255), nullable=True),
sa.Column(
"frequency",
postgresql.ENUM(
"daily",
"weekly",
"monthly",
name="report_schedule_frequency",
create_type=False,
),
nullable=False,
),
sa.Column("day_of_week", sa.Integer(), nullable=True), # 0-6 for weekly
sa.Column("day_of_month", sa.Integer(), nullable=True), # 1-31 for monthly
sa.Column("hour", sa.Integer(), nullable=False), # 0-23
sa.Column("minute", sa.Integer(), nullable=False), # 0-59
sa.Column(
"format",
postgresql.ENUM(
"pdf", "csv", name="report_schedule_format", create_type=False
),
nullable=False,
),
sa.Column(
"include_logs",
sa.Boolean(),
nullable=False,
server_default=sa.text("false"),
),
sa.Column("sections", postgresql.JSONB(), server_default="[]"),
sa.Column("email_to", postgresql.ARRAY(sa.String(255)), server_default="{}"),
sa.Column(
"is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")
),
sa.Column("last_run_at", sa.TIMESTAMP(timezone=True), nullable=True),
sa.Column("next_run_at", sa.TIMESTAMP(timezone=True), nullable=True),
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
server_default=sa.text("NOW()"),
nullable=False,
),
)
# Add indexes
op.create_index("idx_report_schedules_user_id", "report_schedules", ["user_id"])
op.create_index(
"idx_report_schedules_scenario_id", "report_schedules", ["scenario_id"]
)
op.create_index(
"idx_report_schedules_next_run_at", "report_schedules", ["next_run_at"]
)
# Add check constraints using raw SQL for complex expressions
op.execute("""
ALTER TABLE report_schedules
ADD CONSTRAINT chk_report_schedules_hour
CHECK (hour >= 0 AND hour <= 23)
""")
op.execute("""
ALTER TABLE report_schedules
ADD CONSTRAINT chk_report_schedules_minute
CHECK (minute >= 0 AND minute <= 59)
""")
op.execute("""
ALTER TABLE report_schedules
ADD CONSTRAINT chk_report_schedules_day_of_week
CHECK (day_of_week IS NULL OR (day_of_week >= 0 AND day_of_week <= 6))
""")
op.execute("""
ALTER TABLE report_schedules
ADD CONSTRAINT chk_report_schedules_day_of_month
CHECK (day_of_month IS NULL OR (day_of_month >= 1 AND day_of_month <= 31))
""")
def downgrade() -> None:
"""Downgrade schema."""
# Drop constraints
op.execute(
"ALTER TABLE report_schedules DROP CONSTRAINT IF EXISTS chk_report_schedules_hour"
)
op.execute(
"ALTER TABLE report_schedules DROP CONSTRAINT IF EXISTS chk_report_schedules_minute"
)
op.execute(
"ALTER TABLE report_schedules DROP CONSTRAINT IF EXISTS chk_report_schedules_day_of_week"
)
op.execute(
"ALTER TABLE report_schedules DROP CONSTRAINT IF EXISTS chk_report_schedules_day_of_month"
)
# Drop indexes
op.drop_index("idx_report_schedules_next_run_at", table_name="report_schedules")
op.drop_index("idx_report_schedules_scenario_id", table_name="report_schedules")
op.drop_index("idx_report_schedules_user_id", table_name="report_schedules")
# Drop table
op.drop_table("report_schedules")
# Drop enum types
op.execute("DROP TYPE IF EXISTS report_schedule_frequency;")
op.execute("DROP TYPE IF EXISTS report_schedule_format;")