m20260514_000001_fediverse_guard_schema.rs 242 lignes · 8546 octets
//! Migration `m20260514_000001_fediverse_guard_schema`.
//!
//! Creates the three tables backing the fediverse-guard runtime:
//!
//! - `fediverse_guard_events` — raw event log, indexed by
//!   `(domain, created_at DESC)` to feed sliding-window detectors.
//! - `fediverse_guard_bans` — active + historical bans, indexed by
//!   `(domain)` with a partial-unique on rows whose `lifted_at IS NULL`.
//! - `fediverse_guard_acl` — manual allow/deny rules with FK to
//!   `users.id`.
//!
//! All columns use UTC timestamps. UUID primary keys are caller-
//! generated (Rust-side `uuid::Uuid::new_v4()`) — Postgres' `gen_random_uuid()`
//! is reserved for the rare DDL-only inserts (none here).

use sea_orm_migration::prelude::*;
use sea_orm_migration::sea_orm::ConnectionTrait;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // ---- fediverse_guard_events ----------------------------------
        manager
            .create_table(
                Table::create()
                    .table(FediverseGuardEvents::Table)
                    .col(
                        ColumnDef::new(FediverseGuardEvents::Id)
                            .uuid()
                            .not_null()
                            .primary_key(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardEvents::EventType)
                            .text()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardEvents::Domain)
                            .text()
                            .not_null(),
                    )
                    .col(ColumnDef::new(FediverseGuardEvents::ActorUri).text().null())
                    .col(
                        ColumnDef::new(FediverseGuardEvents::RemoteIdentityId)
                            .uuid()
                            .null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardEvents::Severity)
                            .text()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardEvents::HttpStatus)
                            .integer()
                            .null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardEvents::Details)
                            .json_binary()
                            .null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardEvents::CreatedAt)
                            .timestamp_with_time_zone()
                            .not_null(),
                    )
                    .to_owned(),
            )
            .await?;

        let conn = manager.get_connection();
        conn.execute_unprepared(
            "CREATE INDEX fediverse_guard_events_domain_time_idx \
             ON fediverse_guard_events (domain, created_at DESC)",
        )
        .await?;
        conn.execute_unprepared(
            "CREATE INDEX fediverse_guard_events_type_time_idx \
             ON fediverse_guard_events (event_type, created_at DESC)",
        )
        .await?;

        // ---- fediverse_guard_bans ------------------------------------
        manager
            .create_table(
                Table::create()
                    .table(FediverseGuardBans::Table)
                    .col(
                        ColumnDef::new(FediverseGuardBans::Id)
                            .uuid()
                            .not_null()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(FediverseGuardBans::Domain).text().not_null())
                    .col(ColumnDef::new(FediverseGuardBans::ActorUri).text().null())
                    .col(ColumnDef::new(FediverseGuardBans::Reason).text().not_null())
                    .col(
                        ColumnDef::new(FediverseGuardBans::Severity)
                            .text()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardBans::TriggeredBy)
                            .text()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardBans::BannedAt)
                            .timestamp_with_time_zone()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardBans::ExpiresAt)
                            .timestamp_with_time_zone()
                            .null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardBans::LiftedAt)
                            .timestamp_with_time_zone()
                            .null(),
                    )
                    .col(ColumnDef::new(FediverseGuardBans::LiftedBy).uuid().null())
                    .to_owned(),
            )
            .await?;

        conn.execute_unprepared(
            "CREATE INDEX fediverse_guard_bans_active_idx \
             ON fediverse_guard_bans (domain) WHERE lifted_at IS NULL",
        )
        .await?;

        // ---- fediverse_guard_acl -------------------------------------
        manager
            .create_table(
                Table::create()
                    .table(FediverseGuardAcl::Table)
                    .col(
                        ColumnDef::new(FediverseGuardAcl::Id)
                            .uuid()
                            .not_null()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(FediverseGuardAcl::Kind).text().not_null())
                    .col(ColumnDef::new(FediverseGuardAcl::Domain).text().not_null())
                    .col(ColumnDef::new(FediverseGuardAcl::Reason).text().null())
                    .col(
                        ColumnDef::new(FediverseGuardAcl::CreatedBy)
                            .uuid()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardAcl::CreatedAt)
                            .timestamp_with_time_zone()
                            .not_null(),
                    )
                    .col(
                        ColumnDef::new(FediverseGuardAcl::UpdatedAt)
                            .timestamp_with_time_zone()
                            .not_null(),
                    )
                    .to_owned(),
            )
            .await?;

        conn.execute_unprepared(
            "CREATE UNIQUE INDEX fediverse_guard_acl_kind_domain_uniq \
             ON fediverse_guard_acl (kind, domain)",
        )
        .await?;

        Ok(())
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        let conn = manager.get_connection();
        conn.execute_unprepared("DROP INDEX IF EXISTS fediverse_guard_acl_kind_domain_uniq")
            .await?;
        manager
            .drop_table(Table::drop().table(FediverseGuardAcl::Table).to_owned())
            .await?;
        conn.execute_unprepared("DROP INDEX IF EXISTS fediverse_guard_bans_active_idx")
            .await?;
        manager
            .drop_table(Table::drop().table(FediverseGuardBans::Table).to_owned())
            .await?;
        conn.execute_unprepared("DROP INDEX IF EXISTS fediverse_guard_events_type_time_idx")
            .await?;
        conn.execute_unprepared("DROP INDEX IF EXISTS fediverse_guard_events_domain_time_idx")
            .await?;
        manager
            .drop_table(Table::drop().table(FediverseGuardEvents::Table).to_owned())
            .await?;
        Ok(())
    }
}

#[derive(DeriveIden)]
enum FediverseGuardEvents {
    Table,
    Id,
    EventType,
    Domain,
    ActorUri,
    RemoteIdentityId,
    Severity,
    HttpStatus,
    Details,
    CreatedAt,
}

#[derive(DeriveIden)]
enum FediverseGuardBans {
    Table,
    Id,
    Domain,
    ActorUri,
    Reason,
    Severity,
    TriggeredBy,
    BannedAt,
    ExpiresAt,
    LiftedAt,
    LiftedBy,
}

#[derive(DeriveIden)]
enum FediverseGuardAcl {
    Table,
    Id,
    Kind,
    Domain,
    Reason,
    CreatedBy,
    CreatedAt,
    UpdatedAt,
}