m20260512_000001_fediverse_protocol_schema.rs
532 lignes · 18112 octets
//! Migration `m20260512_000001_fediverse_protocol_schema`. //! //! Fused migration that creates every fediverse-protocol-only table //! in one step. Replaces (and supersedes) the granular legacy //! migrations `m20260510_000010/000012/000013/000015` that used to //! live in `rustwarden-core/migrations`. //! //! - `fediverse_trusted_domains` + `fediverse_blocked_domains` (RFC 0004) //! - seeds for default `app_settings` fediverse keys (RFC 0004) //! - `fediverse_outbound_queue` (RFC 0011) //! - `fediverse_auth_challenges` (RFC 0006) //! - `fediverse_pending_invites` (RFC 0009) //! //! Indexes on the core `team_members` table that enforce remote-row //! uniqueness live in the core `fediverse_interop` migration instead, //! since they target a core-owned relation. use sea_orm_migration::prelude::*; use sea_orm_migration::sea_orm::ConnectionTrait; use sea_orm_migration::sea_orm::sea_query::{Expr, OnConflict, Query}; use uuid::Uuid; #[derive(DeriveMigrationName)] pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { // ---- RFC 0004: trust model ----------------------------------- manager .create_table( Table::create() .table(FediverseTrustedDomains::Table) .col( ColumnDef::new(FediverseTrustedDomains::Id) .uuid() .not_null() .primary_key(), ) .col( ColumnDef::new(FediverseTrustedDomains::Domain) .text() .not_null() .unique_key(), ) .col(ColumnDef::new(FediverseTrustedDomains::Note).text().null()) .col( ColumnDef::new(FediverseTrustedDomains::AddedByUserId) .uuid() .null(), ) .col( ColumnDef::new(FediverseTrustedDomains::CreatedAt) .timestamp_with_time_zone() .not_null(), ) .to_owned(), ) .await?; manager .create_table( Table::create() .table(FediverseBlockedDomains::Table) .col( ColumnDef::new(FediverseBlockedDomains::Id) .uuid() .not_null() .primary_key(), ) .col( ColumnDef::new(FediverseBlockedDomains::Domain) .text() .not_null() .unique_key(), ) .col( ColumnDef::new(FediverseBlockedDomains::Severity) .text() .not_null() .default("suspend"), ) .col( ColumnDef::new(FediverseBlockedDomains::Reason) .text() .null(), ) .col( ColumnDef::new(FediverseBlockedDomains::AddedByUserId) .uuid() .null(), ) .col( ColumnDef::new(FediverseBlockedDomains::CreatedAt) .timestamp_with_time_zone() .not_null(), ) .to_owned(), ) .await?; // Seed default app_settings keys. Idempotent via ON CONFLICT. // `app_settings` is a core table created by an earlier core // migration; this seeding is fediverse-specific but writes into // a shared table, so it lives here on purpose. Bound parameters // are used rather than `format!()` so the pattern stays safe if // any seed value ever becomes dynamic. for (key, value) in [ ("fediverse_enabled", "false"), ("fediverse_trust_mode", "allowlist"), ("fediverse_interop_scope", "rustwarden_only"), ("fediverse_authorized_fetch", "false"), ("fediverse_node_name", "Rustwarden"), ("fediverse_node_description", ""), ] { let insert = Query::insert() .into_table(AppSettings::Table) .columns([ AppSettings::Id, AppSettings::Key, AppSettings::Value, AppSettings::UpdatedAt, ]) .values_panic([ Uuid::new_v4().into(), key.into(), value.into(), Expr::current_timestamp(), ]) .on_conflict(OnConflict::column(AppSettings::Key).do_nothing().to_owned()) .to_owned(); manager.get_connection().execute(&insert).await?; } // ---- RFC 0011: outbound delivery queue ------------------------ manager .create_table( Table::create() .table(FediverseOutboundQueue::Table) .col( ColumnDef::new(FediverseOutboundQueue::Id) .uuid() .not_null() .primary_key(), ) .col( ColumnDef::new(FediverseOutboundQueue::ActivityId) .text() .not_null(), ) .col( ColumnDef::new(FediverseOutboundQueue::PayloadJson) .text() .not_null(), ) .col( ColumnDef::new(FediverseOutboundQueue::TargetInbox) .text() .not_null(), ) .col( ColumnDef::new(FediverseOutboundQueue::TargetDomain) .text() .not_null(), ) .col( ColumnDef::new(FediverseOutboundQueue::Status) .text() .not_null() .default("pending"), ) .col( ColumnDef::new(FediverseOutboundQueue::Attempts) .integer() .not_null() .default(0), ) .col( ColumnDef::new(FediverseOutboundQueue::MaxAttempts) .integer() .not_null() .default(8), ) .col( ColumnDef::new(FediverseOutboundQueue::LastError) .text() .null(), ) .col( ColumnDef::new(FediverseOutboundQueue::NextRetryAt) .timestamp_with_time_zone() .null(), ) .col( ColumnDef::new(FediverseOutboundQueue::CreatedAt) .timestamp_with_time_zone() .not_null(), ) .col( ColumnDef::new(FediverseOutboundQueue::DeliveredAt) .timestamp_with_time_zone() .null(), ) .col( ColumnDef::new(FediverseOutboundQueue::ActorUserId) .uuid() .null(), ) .to_owned(), ) .await?; manager .create_index( Index::create() .name("fediverse_outbound_pending_idx") .table(FediverseOutboundQueue::Table) .col(FediverseOutboundQueue::Status) .col(FediverseOutboundQueue::NextRetryAt) .to_owned(), ) .await?; // ---- RFC 0006: client-side auth challenges ------------------- manager .create_table( Table::create() .table(FediverseAuthChallenges::Table) .col( ColumnDef::new(FediverseAuthChallenges::Id) .uuid() .not_null() .primary_key(), ) .col( ColumnDef::new(FediverseAuthChallenges::Nonce) .text() .not_null() .unique_key(), ) .col( ColumnDef::new(FediverseAuthChallenges::Handle) .text() .not_null(), ) .col( ColumnDef::new(FediverseAuthChallenges::Domain) .text() .not_null(), ) .col( ColumnDef::new(FediverseAuthChallenges::RequestedRemoteId) .uuid() .null(), ) .col( ColumnDef::new(FediverseAuthChallenges::Status) .text() .not_null() .default("pending"), ) .col( ColumnDef::new(FediverseAuthChallenges::RedirectUrl) .text() .null(), ) .col( ColumnDef::new(FediverseAuthChallenges::CreatedAt) .timestamp_with_time_zone() .not_null(), ) .col( ColumnDef::new(FediverseAuthChallenges::ExpiresAt) .timestamp_with_time_zone() .not_null(), ) .col( ColumnDef::new(FediverseAuthChallenges::ConsumedAt) .timestamp_with_time_zone() .null(), ) .to_owned(), ) .await?; manager .create_index( Index::create() .name("fediverse_auth_challenges_pending_idx") .table(FediverseAuthChallenges::Table) .col(FediverseAuthChallenges::Status) .col(FediverseAuthChallenges::ExpiresAt) .to_owned(), ) .await?; // ---- RFC 0009: cross-instance team invites ------------------- manager .create_table( Table::create() .table(FediversePendingInvites::Table) .col( ColumnDef::new(FediversePendingInvites::Id) .uuid() .not_null() .primary_key(), ) .col( ColumnDef::new(FediversePendingInvites::ActivityId) .text() .not_null() .unique_key(), ) .col( ColumnDef::new(FediversePendingInvites::TeamId) .uuid() .not_null(), ) .col( ColumnDef::new(FediversePendingInvites::InvitedHandle) .text() .not_null(), ) .col( ColumnDef::new(FediversePendingInvites::InvitedRemoteIdentityId) .uuid() .null(), ) .col( ColumnDef::new(FediversePendingInvites::Role) .text() .not_null(), ) .col( ColumnDef::new(FediversePendingInvites::InvitedByUserId) .uuid() .not_null(), ) .col( ColumnDef::new(FediversePendingInvites::Status) .text() .not_null() .default("pending"), ) .col( ColumnDef::new(FediversePendingInvites::CreatedAt) .timestamp_with_time_zone() .not_null(), ) .col( ColumnDef::new(FediversePendingInvites::ExpiresAt) .timestamp_with_time_zone() .not_null(), ) .col( ColumnDef::new(FediversePendingInvites::DecidedAt) .timestamp_with_time_zone() .null(), ) .to_owned(), ) .await?; manager .get_connection() .execute_unprepared( "CREATE INDEX fediverse_pending_invites_status_idx \ ON fediverse_pending_invites (status, expires_at)", ) .await?; Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { let conn = manager.get_connection(); conn.execute_unprepared("DROP INDEX IF EXISTS fediverse_pending_invites_status_idx") .await?; manager .drop_table( Table::drop() .table(FediversePendingInvites::Table) .to_owned(), ) .await?; manager .drop_index( Index::drop() .name("fediverse_auth_challenges_pending_idx") .table(FediverseAuthChallenges::Table) .to_owned(), ) .await?; manager .drop_table( Table::drop() .table(FediverseAuthChallenges::Table) .to_owned(), ) .await?; manager .drop_index( Index::drop() .name("fediverse_outbound_pending_idx") .table(FediverseOutboundQueue::Table) .to_owned(), ) .await?; manager .drop_table( Table::drop() .table(FediverseOutboundQueue::Table) .to_owned(), ) .await?; manager .drop_table( Table::drop() .table(FediverseBlockedDomains::Table) .to_owned(), ) .await?; manager .drop_table( Table::drop() .table(FediverseTrustedDomains::Table) .to_owned(), ) .await?; // Best-effort: remove the seeded settings. Parameter-bound for // safety even though the keys are literals. for key in [ "fediverse_enabled", "fediverse_trust_mode", "fediverse_interop_scope", "fediverse_authorized_fetch", "fediverse_node_name", "fediverse_node_description", ] { let delete = Query::delete() .from_table(AppSettings::Table) .and_where(Expr::col(AppSettings::Key).eq(key)) .to_owned(); conn.execute(&delete).await?; } Ok(()) } } #[derive(DeriveIden)] enum AppSettings { Table, Id, Key, Value, UpdatedAt, } #[derive(DeriveIden)] enum FediverseTrustedDomains { Table, Id, Domain, Note, AddedByUserId, CreatedAt, } #[derive(DeriveIden)] enum FediverseBlockedDomains { Table, Id, Domain, Severity, Reason, AddedByUserId, CreatedAt, } #[derive(DeriveIden)] enum FediverseOutboundQueue { Table, Id, ActivityId, PayloadJson, TargetInbox, TargetDomain, Status, Attempts, MaxAttempts, LastError, NextRetryAt, CreatedAt, DeliveredAt, ActorUserId, } #[derive(DeriveIden)] enum FediverseAuthChallenges { Table, Id, Nonce, Handle, Domain, RequestedRemoteId, Status, RedirectUrl, CreatedAt, ExpiresAt, ConsumedAt, } #[derive(DeriveIden)] enum FediversePendingInvites { Table, Id, ActivityId, TeamId, InvitedHandle, InvitedRemoteIdentityId, Role, InvitedByUserId, Status, CreatedAt, ExpiresAt, DecidedAt, }
GitRust