Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ APP__VERSION=0.1.0
APP__DEBUG=false
APP__HOST=0.0.0.0
APP__PORT=8000
APP__TIMEZONE=8

LOG__LEVEL=INFO
LOG__JSON_LOGS=false
Expand Down
2 changes: 2 additions & 0 deletions migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from alembic import context
from src.config import get_settings

from src.audit.schemas import AuditLog # noqa: F401

config = context.config

# Get settings before setting URL
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""update_timestamp_to_timezone_aware

Revision ID: 22abd37dc0b2
Revises: 9f5f1dfe3a30
Create Date: 2026-01-09 22:42:39.007596

"""

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 = "22abd37dc0b2"
down_revision: Union[str, Sequence[str], None] = "9f5f1dfe3a30"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
bind = op.get_bind()
dialect = bind.dialect.name

if dialect == "postgresql":
op.alter_column(
"audit_logs",
"occurred_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)


def downgrade() -> None:
"""Downgrade schema."""
bind = op.get_bind()
dialect = bind.dialect.name

if dialect == "postgresql":
op.alter_column(
"audit_logs",
"occurred_at",
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
"""update_timestamp_fields_with_timezone

Revision ID: dcaa4d1b381d
Revises: 9f5f1dfe3a30
Create Date: 2026-01-09 22:15:58.804158

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = "dcaa4d1b381d"
down_revision: Union[str, Sequence[str], None] = "9f5f1dfe3a30"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
bind = op.get_bind()
dialect = bind.dialect.name

if dialect == "sqlite":
pass
elif dialect == "postgresql":
op.alter_column(
"audit_logs",
"occurred_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)

op.drop_index(op.f("ix_audit_logs_request_id"), table_name="audit_logs")
op.drop_constraint(
op.f("audit_logs_actor_id_fkey"), "audit_logs", type_="foreignkey"
)
op.create_foreign_key(
"audit_logs_actor_id_fkey_new", "audit_logs", "users", ["actor_id"], ["id"]
)

op.alter_column(
"permissions",
"created_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)
op.alter_column(
"permissions",
"updated_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)

op.drop_constraint(
op.f("role_permissions_permission_id_fkey"),
"role_permissions",
type_="foreignkey",
)
op.drop_constraint(
op.f("role_permissions_role_id_fkey"),
"role_permissions",
type_="foreignkey",
)
op.create_foreign_key(
"role_permissions_role_id_fkey_new",
"role_permissions",
"roles",
["role_id"],
["id"],
)
op.create_foreign_key(
"role_permissions_permission_id_fkey_new",
"role_permissions",
"permissions",
["permission_id"],
["id"],
)

op.alter_column(
"roles",
"created_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)
op.alter_column(
"roles",
"updated_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)

op.drop_constraint(
op.f("user_roles_user_id_fkey"), "user_roles", type_="foreignkey"
)
op.drop_constraint(
op.f("user_roles_role_id_fkey"), "user_roles", type_="foreignkey"
)
op.create_foreign_key(
"user_roles_role_id_fkey_new", "user_roles", "roles", ["role_id"], ["id"]
)
op.create_foreign_key(
"user_roles_user_id_fkey_new", "user_roles", "users", ["user_id"], ["id"]
)

op.alter_column(
"users",
"created_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=False,
)
op.alter_column(
"users",
"updated_at",
existing_type=postgresql.TIMESTAMP(),
type_=sa.DateTime(timezone=True),
existing_nullable=False,
)
op.alter_column(
"users",
"hashed_password",
existing_type=sa.VARCHAR(length=255),
type_=sqlmodel.sql.sqltypes.AutoString(length=1024),
existing_nullable=False,
)


def downgrade() -> None:
"""Downgrade schema."""
bind = op.get_bind()
dialect = bind.dialect.name

if dialect == "sqlite":
pass
elif dialect == "postgresql":
op.alter_column(
"users",
"hashed_password",
existing_type=sqlmodel.sql.sqltypes.AutoString(length=1024),
type_=sa.VARCHAR(length=255),
existing_nullable=False,
)
op.alter_column(
"users",
"updated_at",
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
)
op.alter_column(
"users",
"created_at",
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
)

op.drop_constraint(
"user_roles_role_id_fkey_new", "user_roles", type_="foreignkey"
)
op.drop_constraint(
"user_roles_user_id_fkey_new", "user_roles", type_="foreignkey"
)
op.create_foreign_key(
op.f("user_roles_role_id_fkey"),
"user_roles",
"roles",
["role_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
op.f("user_roles_user_id_fkey"),
"user_roles",
"users",
["user_id"],
["id"],
ondelete="CASCADE",
)

op.alter_column(
"roles",
"updated_at",
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)
op.alter_column(
"roles",
"created_at",
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)

op.drop_constraint(
"role_permissions_role_id_fkey_new", "role_permissions", type_="foreignkey"
)
op.drop_constraint(
"role_permissions_permission_id_fkey_new",
"role_permissions",
type_="foreignkey",
)
op.create_foreign_key(
op.f("role_permissions_role_id_fkey"),
"role_permissions",
"roles",
["role_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
op.f("role_permissions_permission_id_fkey"),
"role_permissions",
"permissions",
["permission_id"],
["id"],
ondelete="CASCADE",
)

op.alter_column(
"permissions",
"updated_at",
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)
op.alter_column(
"permissions",
"created_at",
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)

op.drop_constraint(
"audit_logs_actor_id_fkey_new", "audit_logs", type_="foreignkey"
)
op.create_foreign_key(
op.f("audit_logs_actor_id_fkey"),
"audit_logs",
"users",
["actor_id"],
["id"],
ondelete="SET NULL",
)
op.create_index(
op.f("ix_audit_logs_request_id"), "audit_logs", ["request_id"], unique=False
)

op.alter_column(
"audit_logs",
"occurred_at",
existing_type=sa.DateTime(timezone=True),
type_=postgresql.TIMESTAMP(),
existing_nullable=False,
existing_server_default=sa.text("CURRENT_TIMESTAMP"),
)
9 changes: 6 additions & 3 deletions src/audit/schemas.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from datetime import UTC, datetime
from datetime import datetime
from enum import StrEnum
from typing import Any

from sqlalchemy import JSON, Column, Text
from sqlalchemy import JSON, Column, DateTime, Text
from sqlmodel import Field, SQLModel

from src.shared.mixins import now


class AuditAction(StrEnum):
LOGIN = "login"
Expand All @@ -31,7 +33,8 @@ class AuditLog(SQLModel, table=True):

id: int | None = Field(default=None, primary_key=True)
occurred_at: datetime = Field(
default_factory=lambda: datetime.now(UTC),
default_factory=now,
sa_type=DateTime(timezone=True),
nullable=False,
index=True,
)
Expand Down
1 change: 1 addition & 0 deletions src/config/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ class AppSettings(BaseSettings):
debug: bool = Field(default=False)
host: str = Field(default="0.0.0.0")
port: int = Field(default=8000)
timezone: int = Field(default=8, description="UTC offset in hours")
2 changes: 0 additions & 2 deletions src/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ async def init_db(url: str, echo: bool = False) -> None:
@event.listens_for(engine.sync_engine, "connect")
def set_sqlite_pragma(dbapi_conn, connection_record):
cursor = dbapi_conn.cursor()
# By default, sqlite disables foreign key constraint enforcement
# see https://www.sqlite.org/foreignkeys.html#fk_enable
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()

Expand Down
Loading
Loading