update CLI
EBO <eric.bouhana@softalys.com> committé le 2026-05-14 17:17
45bc01854cdc20ae7f6c015a2ef44c654bb0132c
1 parent(s)
21 fichiers modifiés
+3052
-2914
D
crates/rustwarden-core/migrations/src/m20260305_000001_initial_schema.rs
+0
-1281
@@ -1,1281 +0,0 @@
| 1 | -use sea_orm_migration::prelude::*; | |
| 2 | - | |
| 3 | -/// Initial schema migration: creates the core users, roles, and sessions tables. | |
| 4 | -#[derive(DeriveMigrationName)] | |
| 5 | -pub struct Migration; | |
| 6 | - | |
| 7 | -#[async_trait::async_trait] | |
| 8 | -impl MigrationTrait for Migration { | |
| 9 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | - // --- 1. users --- | |
| 11 | - manager | |
| 12 | - .create_table( | |
| 13 | - Table::create() | |
| 14 | - .table(Users::Table) | |
| 15 | - .if_not_exists() | |
| 16 | - .col(ColumnDef::new(Users::Id).uuid().not_null().primary_key()) | |
| 17 | - .col( | |
| 18 | - ColumnDef::new(Users::Username) | |
| 19 | - .string() | |
| 20 | - .string_len(64) | |
| 21 | - .not_null() | |
| 22 | - .unique_key(), | |
| 23 | - ) | |
| 24 | - .col( | |
| 25 | - ColumnDef::new(Users::Email) | |
| 26 | - .string() | |
| 27 | - .string_len(255) | |
| 28 | - .not_null() | |
| 29 | - .unique_key(), | |
| 30 | - ) | |
| 31 | - .col( | |
| 32 | - ColumnDef::new(Users::PasswordHash) | |
| 33 | - .string() | |
| 34 | - .string_len(255) | |
| 35 | - .not_null(), | |
| 36 | - ) | |
| 37 | - .col( | |
| 38 | - ColumnDef::new(Users::MustChangePassword) | |
| 39 | - .boolean() | |
| 40 | - .not_null() | |
| 41 | - .default(false), | |
| 42 | - ) | |
| 43 | - .col( | |
| 44 | - ColumnDef::new(Users::EmailVerified) | |
| 45 | - .boolean() | |
| 46 | - .not_null() | |
| 47 | - .default(false), | |
| 48 | - ) | |
| 49 | - .col( | |
| 50 | - ColumnDef::new(Users::EmailVerifiedAt) | |
| 51 | - .timestamp_with_time_zone() | |
| 52 | - .null(), | |
| 53 | - ) | |
| 54 | - .col( | |
| 55 | - ColumnDef::new(Users::LastLoginAt) | |
| 56 | - .timestamp_with_time_zone() | |
| 57 | - .null(), | |
| 58 | - ) | |
| 59 | - .col( | |
| 60 | - ColumnDef::new(Users::LastLoginIp) | |
| 61 | - .string() | |
| 62 | - .string_len(45) | |
| 63 | - .null(), | |
| 64 | - ) | |
| 65 | - .col( | |
| 66 | - ColumnDef::new(Users::PasswordChangedAt) | |
| 67 | - .timestamp_with_time_zone() | |
| 68 | - .null(), | |
| 69 | - ) | |
| 70 | - .col( | |
| 71 | - ColumnDef::new(Users::CreatedAt) | |
| 72 | - .timestamp_with_time_zone() | |
| 73 | - .not_null(), | |
| 74 | - ) | |
| 75 | - .col( | |
| 76 | - ColumnDef::new(Users::UpdatedAt) | |
| 77 | - .timestamp_with_time_zone() | |
| 78 | - .not_null(), | |
| 79 | - ) | |
| 80 | - .to_owned(), | |
| 81 | - ) | |
| 82 | - .await?; | |
| 83 | - | |
| 84 | - manager | |
| 85 | - .create_index( | |
| 86 | - Index::create() | |
| 87 | - .if_not_exists() | |
| 88 | - .name("idx_users_email_verified") | |
| 89 | - .table(Users::Table) | |
| 90 | - .col(Users::EmailVerified) | |
| 91 | - .to_owned(), | |
| 92 | - ) | |
| 93 | - .await?; | |
| 94 | - | |
| 95 | - manager | |
| 96 | - .create_index( | |
| 97 | - Index::create() | |
| 98 | - .if_not_exists() | |
| 99 | - .name("idx_users_last_login_at") | |
| 100 | - .table(Users::Table) | |
| 101 | - .col(Users::LastLoginAt) | |
| 102 | - .to_owned(), | |
| 103 | - ) | |
| 104 | - .await?; | |
| 105 | - | |
| 106 | - manager | |
| 107 | - .create_index( | |
| 108 | - Index::create() | |
| 109 | - .if_not_exists() | |
| 110 | - .name("idx_users_password_changed_at") | |
| 111 | - .table(Users::Table) | |
| 112 | - .col(Users::PasswordChangedAt) | |
| 113 | - .to_owned(), | |
| 114 | - ) | |
| 115 | - .await?; | |
| 116 | - | |
| 117 | - // --- 2. roles --- | |
| 118 | - manager | |
| 119 | - .create_table( | |
| 120 | - Table::create() | |
| 121 | - .table(Roles::Table) | |
| 122 | - .if_not_exists() | |
| 123 | - .col(ColumnDef::new(Roles::Id).uuid().not_null().primary_key()) | |
| 124 | - .col( | |
| 125 | - ColumnDef::new(Roles::Name) | |
| 126 | - .string() | |
| 127 | - .string_len(64) | |
| 128 | - .not_null() | |
| 129 | - .unique_key(), | |
| 130 | - ) | |
| 131 | - .col(ColumnDef::new(Roles::Description).text().null()) | |
| 132 | - .col( | |
| 133 | - ColumnDef::new(Roles::CreatedAt) | |
| 134 | - .timestamp_with_time_zone() | |
| 135 | - .not_null(), | |
| 136 | - ) | |
| 137 | - .col( | |
| 138 | - ColumnDef::new(Roles::UpdatedAt) | |
| 139 | - .timestamp_with_time_zone() | |
| 140 | - .not_null(), | |
| 141 | - ) | |
| 142 | - .to_owned(), | |
| 143 | - ) | |
| 144 | - .await?; | |
| 145 | - | |
| 146 | - // --- 3. user_roles --- | |
| 147 | - manager | |
| 148 | - .create_table( | |
| 149 | - Table::create() | |
| 150 | - .table(UserRoles::Table) | |
| 151 | - .if_not_exists() | |
| 152 | - .col(ColumnDef::new(UserRoles::UserId).uuid().not_null()) | |
| 153 | - .col(ColumnDef::new(UserRoles::RoleId).uuid().not_null()) | |
| 154 | - .col( | |
| 155 | - ColumnDef::new(UserRoles::CreatedAt) | |
| 156 | - .timestamp_with_time_zone() | |
| 157 | - .not_null(), | |
| 158 | - ) | |
| 159 | - .primary_key( | |
| 160 | - Index::create() | |
| 161 | - .col(UserRoles::UserId) | |
| 162 | - .col(UserRoles::RoleId), | |
| 163 | - ) | |
| 164 | - .foreign_key( | |
| 165 | - ForeignKey::create() | |
| 166 | - .name("fk_user_roles_user_id") | |
| 167 | - .from(UserRoles::Table, UserRoles::UserId) | |
| 168 | - .to(Users::Table, Users::Id) | |
| 169 | - .on_delete(ForeignKeyAction::Cascade), | |
| 170 | - ) | |
| 171 | - .foreign_key( | |
| 172 | - ForeignKey::create() | |
| 173 | - .name("fk_user_roles_role_id") | |
| 174 | - .from(UserRoles::Table, UserRoles::RoleId) | |
| 175 | - .to(Roles::Table, Roles::Id) | |
| 176 | - .on_delete(ForeignKeyAction::Cascade), | |
| 177 | - ) | |
| 178 | - .to_owned(), | |
| 179 | - ) | |
| 180 | - .await?; | |
| 181 | - | |
| 182 | - // --- 4. refresh_token --- | |
| 183 | - manager | |
| 184 | - .create_table( | |
| 185 | - Table::create() | |
| 186 | - .table(RefreshToken::Table) | |
| 187 | - .if_not_exists() | |
| 188 | - .col( | |
| 189 | - ColumnDef::new(RefreshToken::Id) | |
| 190 | - .uuid() | |
| 191 | - .not_null() | |
| 192 | - .primary_key(), | |
| 193 | - ) | |
| 194 | - .col(ColumnDef::new(RefreshToken::UserId).uuid().not_null()) | |
| 195 | - .col( | |
| 196 | - ColumnDef::new(RefreshToken::Token) | |
| 197 | - .string() | |
| 198 | - .not_null() | |
| 199 | - .unique_key(), | |
| 200 | - ) | |
| 201 | - .col( | |
| 202 | - ColumnDef::new(RefreshToken::ExpiresAt) | |
| 203 | - .timestamp_with_time_zone() | |
| 204 | - .not_null(), | |
| 205 | - ) | |
| 206 | - .col( | |
| 207 | - ColumnDef::new(RefreshToken::CreatedAt) | |
| 208 | - .timestamp_with_time_zone() | |
| 209 | - .not_null(), | |
| 210 | - ) | |
| 211 | - .col( | |
| 212 | - ColumnDef::new(RefreshToken::Revoked) | |
| 213 | - .boolean() | |
| 214 | - .not_null() | |
| 215 | - .default(false), | |
| 216 | - ) | |
| 217 | - .col( | |
| 218 | - ColumnDef::new(RefreshToken::RevokedAt) | |
| 219 | - .timestamp_with_time_zone() | |
| 220 | - .null(), | |
| 221 | - ) | |
| 222 | - .col(ColumnDef::new(RefreshToken::ReplacedBy).uuid().null()) | |
| 223 | - .foreign_key( | |
| 224 | - ForeignKey::create() | |
| 225 | - .name("fk_refresh_token_user_id") | |
| 226 | - .from(RefreshToken::Table, RefreshToken::UserId) | |
| 227 | - .to(Users::Table, Users::Id) | |
| 228 | - .on_delete(ForeignKeyAction::Cascade), | |
| 229 | - ) | |
| 230 | - .foreign_key( | |
| 231 | - ForeignKey::create() | |
| 232 | - .name("fk_refresh_token_replaced_by") | |
| 233 | - .from(RefreshToken::Table, RefreshToken::ReplacedBy) | |
| 234 | - .to(RefreshToken::Table, RefreshToken::Id) | |
| 235 | - .on_delete(ForeignKeyAction::SetNull), | |
| 236 | - ) | |
| 237 | - .to_owned(), | |
| 238 | - ) | |
| 239 | - .await?; | |
| 240 | - | |
| 241 | - manager | |
| 242 | - .create_index( | |
| 243 | - Index::create() | |
| 244 | - .if_not_exists() | |
| 245 | - .name("idx_refresh_token_user_id") | |
| 246 | - .table(RefreshToken::Table) | |
| 247 | - .col(RefreshToken::UserId) | |
| 248 | - .to_owned(), | |
| 249 | - ) | |
| 250 | - .await?; | |
| 251 | - | |
| 252 | - manager | |
| 253 | - .create_index( | |
| 254 | - Index::create() | |
| 255 | - .if_not_exists() | |
| 256 | - .name("idx_refresh_token_token") | |
| 257 | - .table(RefreshToken::Table) | |
| 258 | - .col(RefreshToken::Token) | |
| 259 | - .to_owned(), | |
| 260 | - ) | |
| 261 | - .await?; | |
| 262 | - | |
| 263 | - manager | |
| 264 | - .create_index( | |
| 265 | - Index::create() | |
| 266 | - .if_not_exists() | |
| 267 | - .name("idx_refresh_token_expires_at") | |
| 268 | - .table(RefreshToken::Table) | |
| 269 | - .col(RefreshToken::ExpiresAt) | |
| 270 | - .to_owned(), | |
| 271 | - ) | |
| 272 | - .await?; | |
| 273 | - | |
| 274 | - // --- 5. jwt_blacklist --- | |
| 275 | - manager | |
| 276 | - .create_table( | |
| 277 | - Table::create() | |
| 278 | - .table(JwtBlacklist::Table) | |
| 279 | - .if_not_exists() | |
| 280 | - .col( | |
| 281 | - ColumnDef::new(JwtBlacklist::Id) | |
| 282 | - .uuid() | |
| 283 | - .not_null() | |
| 284 | - .primary_key(), | |
| 285 | - ) | |
| 286 | - .col( | |
| 287 | - ColumnDef::new(JwtBlacklist::TokenHash) | |
| 288 | - .string() | |
| 289 | - .not_null() | |
| 290 | - .unique_key(), | |
| 291 | - ) | |
| 292 | - .col(ColumnDef::new(JwtBlacklist::UserId).uuid().not_null()) | |
| 293 | - .col( | |
| 294 | - ColumnDef::new(JwtBlacklist::ExpiresAt) | |
| 295 | - .timestamp_with_time_zone() | |
| 296 | - .not_null(), | |
| 297 | - ) | |
| 298 | - .col( | |
| 299 | - ColumnDef::new(JwtBlacklist::RevokedAt) | |
| 300 | - .timestamp_with_time_zone() | |
| 301 | - .not_null(), | |
| 302 | - ) | |
| 303 | - .col( | |
| 304 | - ColumnDef::new(JwtBlacklist::CreatedAt) | |
| 305 | - .timestamp_with_time_zone() | |
| 306 | - .not_null(), | |
| 307 | - ) | |
| 308 | - .foreign_key( | |
| 309 | - ForeignKey::create() | |
| 310 | - .name("fk_jwt_blacklist_user_id") | |
| 311 | - .from(JwtBlacklist::Table, JwtBlacklist::UserId) | |
| 312 | - .to(Users::Table, Users::Id) | |
| 313 | - .on_delete(ForeignKeyAction::Cascade), | |
| 314 | - ) | |
| 315 | - .to_owned(), | |
| 316 | - ) | |
| 317 | - .await?; | |
| 318 | - | |
| 319 | - manager | |
| 320 | - .create_index( | |
| 321 | - Index::create() | |
| 322 | - .if_not_exists() | |
| 323 | - .name("idx_jwt_blacklist_token_hash") | |
| 324 | - .table(JwtBlacklist::Table) | |
| 325 | - .col(JwtBlacklist::TokenHash) | |
| 326 | - .unique() | |
| 327 | - .to_owned(), | |
| 328 | - ) | |
| 329 | - .await?; | |
| 330 | - | |
| 331 | - manager | |
| 332 | - .create_index( | |
| 333 | - Index::create() | |
| 334 | - .if_not_exists() | |
| 335 | - .name("idx_jwt_blacklist_expires_at") | |
| 336 | - .table(JwtBlacklist::Table) | |
| 337 | - .col(JwtBlacklist::ExpiresAt) | |
| 338 | - .to_owned(), | |
| 339 | - ) | |
| 340 | - .await?; | |
| 341 | - | |
| 342 | - manager | |
| 343 | - .create_index( | |
| 344 | - Index::create() | |
| 345 | - .if_not_exists() | |
| 346 | - .name("idx_jwt_blacklist_user_id") | |
| 347 | - .table(JwtBlacklist::Table) | |
| 348 | - .col(JwtBlacklist::UserId) | |
| 349 | - .to_owned(), | |
| 350 | - ) | |
| 351 | - .await?; | |
| 352 | - | |
| 353 | - // --- 6. user_preferences --- | |
| 354 | - manager | |
| 355 | - .create_table( | |
| 356 | - Table::create() | |
| 357 | - .table(UserPreferences::Table) | |
| 358 | - .if_not_exists() | |
| 359 | - .col( | |
| 360 | - ColumnDef::new(UserPreferences::Id) | |
| 361 | - .uuid() | |
| 362 | - .not_null() | |
| 363 | - .primary_key() | |
| 364 | - .default(Expr::cust("gen_random_uuid()")), | |
| 365 | - ) | |
| 366 | - .col( | |
| 367 | - ColumnDef::new(UserPreferences::UserId) | |
| 368 | - .uuid() | |
| 369 | - .not_null() | |
| 370 | - .unique_key(), | |
| 371 | - ) | |
| 372 | - .col( | |
| 373 | - ColumnDef::new(UserPreferences::Theme) | |
| 374 | - .string() | |
| 375 | - .string_len(10) | |
| 376 | - .not_null() | |
| 377 | - .default("light"), | |
| 378 | - ) | |
| 379 | - .col( | |
| 380 | - ColumnDef::new(UserPreferences::Language) | |
| 381 | - .string() | |
| 382 | - .string_len(5) | |
| 383 | - .not_null() | |
| 384 | - .default("fr"), | |
| 385 | - ) | |
| 386 | - .col( | |
| 387 | - ColumnDef::new(UserPreferences::CreatedAt) | |
| 388 | - .timestamp_with_time_zone() | |
| 389 | - .not_null() | |
| 390 | - .default(Expr::cust("NOW()")), | |
| 391 | - ) | |
| 392 | - .col( | |
| 393 | - ColumnDef::new(UserPreferences::UpdatedAt) | |
| 394 | - .timestamp_with_time_zone() | |
| 395 | - .not_null() | |
| 396 | - .default(Expr::cust("NOW()")), | |
| 397 | - ) | |
| 398 | - .foreign_key( | |
| 399 | - ForeignKey::create() | |
| 400 | - .name("fk_user_preferences_user_id") | |
| 401 | - .from(UserPreferences::Table, UserPreferences::UserId) | |
| 402 | - .to(Users::Table, Users::Id) | |
| 403 | - .on_delete(ForeignKeyAction::Cascade), | |
| 404 | - ) | |
| 405 | - .to_owned(), | |
| 406 | - ) | |
| 407 | - .await?; | |
| 408 | - | |
| 409 | - manager | |
| 410 | - .get_connection() | |
| 411 | - .execute_unprepared( | |
| 412 | - "ALTER TABLE user_preferences ADD CONSTRAINT check_theme CHECK (theme IN ('light', 'dark', 'auto'))", | |
| 413 | - ) | |
| 414 | - .await?; | |
| 415 | - | |
| 416 | - manager | |
| 417 | - .get_connection() | |
| 418 | - .execute_unprepared( | |
| 419 | - "ALTER TABLE user_preferences ADD CONSTRAINT check_language CHECK (language IN ('fr', 'en'))", | |
| 420 | - ) | |
| 421 | - .await?; | |
| 422 | - | |
| 423 | - manager | |
| 424 | - .create_index( | |
| 425 | - Index::create() | |
| 426 | - .if_not_exists() | |
| 427 | - .name("idx_user_preferences_user_id") | |
| 428 | - .table(UserPreferences::Table) | |
| 429 | - .col(UserPreferences::UserId) | |
| 430 | - .to_owned(), | |
| 431 | - ) | |
| 432 | - .await?; | |
| 433 | - | |
| 434 | - // --- 7. password_reset_tokens --- | |
| 435 | - manager | |
| 436 | - .create_table( | |
| 437 | - Table::create() | |
| 438 | - .table(PasswordResetTokens::Table) | |
| 439 | - .if_not_exists() | |
| 440 | - .col( | |
| 441 | - ColumnDef::new(PasswordResetTokens::Id) | |
| 442 | - .uuid() | |
| 443 | - .not_null() | |
| 444 | - .primary_key(), | |
| 445 | - ) | |
| 446 | - .col( | |
| 447 | - ColumnDef::new(PasswordResetTokens::UserId) | |
| 448 | - .uuid() | |
| 449 | - .not_null(), | |
| 450 | - ) | |
| 451 | - .col( | |
| 452 | - ColumnDef::new(PasswordResetTokens::TokenHash) | |
| 453 | - .string_len(64) | |
| 454 | - .not_null() | |
| 455 | - .unique_key(), | |
| 456 | - ) | |
| 457 | - .col( | |
| 458 | - ColumnDef::new(PasswordResetTokens::ExpiresAt) | |
| 459 | - .timestamp_with_time_zone() | |
| 460 | - .not_null(), | |
| 461 | - ) | |
| 462 | - .col( | |
| 463 | - ColumnDef::new(PasswordResetTokens::UsedAt) | |
| 464 | - .timestamp_with_time_zone() | |
| 465 | - .null(), | |
| 466 | - ) | |
| 467 | - .col( | |
| 468 | - ColumnDef::new(PasswordResetTokens::CreatedAt) | |
| 469 | - .timestamp_with_time_zone() | |
| 470 | - .not_null(), | |
| 471 | - ) | |
| 472 | - .foreign_key( | |
| 473 | - ForeignKey::create() | |
| 474 | - .name("fk_password_reset_tokens_user_id") | |
| 475 | - .from(PasswordResetTokens::Table, PasswordResetTokens::UserId) | |
| 476 | - .to(Users::Table, Users::Id) | |
| 477 | - .on_delete(ForeignKeyAction::Cascade), | |
| 478 | - ) | |
| 479 | - .to_owned(), | |
| 480 | - ) | |
| 481 | - .await?; | |
| 482 | - | |
| 483 | - manager | |
| 484 | - .create_index( | |
| 485 | - Index::create() | |
| 486 | - .if_not_exists() | |
| 487 | - .name("idx_password_reset_tokens_user_id") | |
| 488 | - .table(PasswordResetTokens::Table) | |
| 489 | - .col(PasswordResetTokens::UserId) | |
| 490 | - .to_owned(), | |
| 491 | - ) | |
| 492 | - .await?; | |
| 493 | - | |
| 494 | - manager | |
| 495 | - .create_index( | |
| 496 | - Index::create() | |
| 497 | - .if_not_exists() | |
| 498 | - .name("idx_password_reset_tokens_token_hash") | |
| 499 | - .table(PasswordResetTokens::Table) | |
| 500 | - .col(PasswordResetTokens::TokenHash) | |
| 501 | - .unique() | |
| 502 | - .to_owned(), | |
| 503 | - ) | |
| 504 | - .await?; | |
| 505 | - | |
| 506 | - manager | |
| 507 | - .create_index( | |
| 508 | - Index::create() | |
| 509 | - .if_not_exists() | |
| 510 | - .name("idx_password_reset_tokens_expires_at") | |
| 511 | - .table(PasswordResetTokens::Table) | |
| 512 | - .col(PasswordResetTokens::ExpiresAt) | |
| 513 | - .to_owned(), | |
| 514 | - ) | |
| 515 | - .await?; | |
| 516 | - | |
| 517 | - // --- 8. email_validation_tokens --- | |
| 518 | - manager | |
| 519 | - .create_table( | |
| 520 | - Table::create() | |
| 521 | - .table(EmailValidationTokens::Table) | |
| 522 | - .if_not_exists() | |
| 523 | - .col( | |
| 524 | - ColumnDef::new(EmailValidationTokens::Id) | |
| 525 | - .uuid() | |
| 526 | - .not_null() | |
| 527 | - .primary_key(), | |
| 528 | - ) | |
| 529 | - .col( | |
| 530 | - ColumnDef::new(EmailValidationTokens::UserId) | |
| 531 | - .uuid() | |
| 532 | - .not_null(), | |
| 533 | - ) | |
| 534 | - .col( | |
| 535 | - ColumnDef::new(EmailValidationTokens::TokenHash) | |
| 536 | - .string_len(64) | |
| 537 | - .not_null() | |
| 538 | - .unique_key(), | |
| 539 | - ) | |
| 540 | - .col( | |
| 541 | - ColumnDef::new(EmailValidationTokens::ExpiresAt) | |
| 542 | - .timestamp_with_time_zone() | |
| 543 | - .not_null(), | |
| 544 | - ) | |
| 545 | - .col( | |
| 546 | - ColumnDef::new(EmailValidationTokens::UsedAt) | |
| 547 | - .timestamp_with_time_zone() | |
| 548 | - .null(), | |
| 549 | - ) | |
| 550 | - .col( | |
| 551 | - ColumnDef::new(EmailValidationTokens::CreatedAt) | |
| 552 | - .timestamp_with_time_zone() | |
| 553 | - .not_null(), | |
| 554 | - ) | |
| 555 | - .foreign_key( | |
| 556 | - ForeignKey::create() | |
| 557 | - .name("fk_email_validation_tokens_user_id") | |
| 558 | - .from(EmailValidationTokens::Table, EmailValidationTokens::UserId) | |
| 559 | - .to(Users::Table, Users::Id) | |
| 560 | - .on_delete(ForeignKeyAction::Cascade), | |
| 561 | - ) | |
| 562 | - .to_owned(), | |
| 563 | - ) | |
| 564 | - .await?; | |
| 565 | - | |
| 566 | - manager | |
| 567 | - .create_index( | |
| 568 | - Index::create() | |
| 569 | - .if_not_exists() | |
| 570 | - .name("idx_email_validation_tokens_user_id") | |
| 571 | - .table(EmailValidationTokens::Table) | |
| 572 | - .col(EmailValidationTokens::UserId) | |
| 573 | - .to_owned(), | |
| 574 | - ) | |
| 575 | - .await?; | |
| 576 | - | |
| 577 | - manager | |
| 578 | - .create_index( | |
| 579 | - Index::create() | |
| 580 | - .if_not_exists() | |
| 581 | - .name("idx_email_validation_tokens_token_hash") | |
| 582 | - .table(EmailValidationTokens::Table) | |
| 583 | - .col(EmailValidationTokens::TokenHash) | |
| 584 | - .unique() | |
| 585 | - .to_owned(), | |
| 586 | - ) | |
| 587 | - .await?; | |
| 588 | - | |
| 589 | - manager | |
| 590 | - .create_index( | |
| 591 | - Index::create() | |
| 592 | - .if_not_exists() | |
| 593 | - .name("idx_email_validation_tokens_expires_at") | |
| 594 | - .table(EmailValidationTokens::Table) | |
| 595 | - .col(EmailValidationTokens::ExpiresAt) | |
| 596 | - .to_owned(), | |
| 597 | - ) | |
| 598 | - .await?; | |
| 599 | - | |
| 600 | - // --- 9. password_change_requests --- | |
| 601 | - manager | |
| 602 | - .create_table( | |
| 603 | - Table::create() | |
| 604 | - .table(PasswordChangeRequests::Table) | |
| 605 | - .if_not_exists() | |
| 606 | - .col( | |
| 607 | - ColumnDef::new(PasswordChangeRequests::Id) | |
| 608 | - .uuid() | |
| 609 | - .not_null() | |
| 610 | - .primary_key(), | |
| 611 | - ) | |
| 612 | - .col( | |
| 613 | - ColumnDef::new(PasswordChangeRequests::UserId) | |
| 614 | - .uuid() | |
| 615 | - .not_null(), | |
| 616 | - ) | |
| 617 | - .col( | |
| 618 | - ColumnDef::new(PasswordChangeRequests::NewPasswordHash) | |
| 619 | - .text() | |
| 620 | - .not_null(), | |
| 621 | - ) | |
| 622 | - .col( | |
| 623 | - ColumnDef::new(PasswordChangeRequests::ConfirmationToken) | |
| 624 | - .string_len(64) | |
| 625 | - .not_null() | |
| 626 | - .unique_key(), | |
| 627 | - ) | |
| 628 | - .col( | |
| 629 | - ColumnDef::new(PasswordChangeRequests::ExpiresAt) | |
| 630 | - .timestamp_with_time_zone() | |
| 631 | - .not_null(), | |
| 632 | - ) | |
| 633 | - .col( | |
| 634 | - ColumnDef::new(PasswordChangeRequests::CreatedAt) | |
| 635 | - .timestamp_with_time_zone() | |
| 636 | - .not_null(), | |
| 637 | - ) | |
| 638 | - .col( | |
| 639 | - ColumnDef::new(PasswordChangeRequests::ConfirmedAt) | |
| 640 | - .timestamp_with_time_zone() | |
| 641 | - .null(), | |
| 642 | - ) | |
| 643 | - .col( | |
| 644 | - ColumnDef::new(PasswordChangeRequests::EmailSentAt) | |
| 645 | - .timestamp_with_time_zone() | |
| 646 | - .null(), | |
| 647 | - ) | |
| 648 | - .col( | |
| 649 | - ColumnDef::new(PasswordChangeRequests::RetryCount) | |
| 650 | - .integer() | |
| 651 | - .not_null() | |
| 652 | - .default(0), | |
| 653 | - ) | |
| 654 | - .foreign_key( | |
| 655 | - ForeignKey::create() | |
| 656 | - .name("fk_password_change_requests_user_id") | |
| 657 | - .from( | |
| 658 | - PasswordChangeRequests::Table, | |
| 659 | - PasswordChangeRequests::UserId, | |
| 660 | - ) | |
| 661 | - .to(Users::Table, Users::Id) | |
| 662 | - .on_delete(ForeignKeyAction::Cascade), | |
| 663 | - ) | |
| 664 | - .to_owned(), | |
| 665 | - ) | |
| 666 | - .await?; | |
| 667 | - | |
| 668 | - manager | |
| 669 | - .create_index( | |
| 670 | - Index::create() | |
| 671 | - .if_not_exists() | |
| 672 | - .name("idx_password_change_requests_user_id") | |
| 673 | - .table(PasswordChangeRequests::Table) | |
| 674 | - .col(PasswordChangeRequests::UserId) | |
| 675 | - .to_owned(), | |
| 676 | - ) | |
| 677 | - .await?; | |
| 678 | - | |
| 679 | - manager | |
| 680 | - .create_index( | |
| 681 | - Index::create() | |
| 682 | - .if_not_exists() | |
| 683 | - .name("idx_password_change_requests_confirmation_token") | |
| 684 | - .table(PasswordChangeRequests::Table) | |
| 685 | - .col(PasswordChangeRequests::ConfirmationToken) | |
| 686 | - .unique() | |
| 687 | - .to_owned(), | |
| 688 | - ) | |
| 689 | - .await?; | |
| 690 | - | |
| 691 | - manager | |
| 692 | - .create_index( | |
| 693 | - Index::create() | |
| 694 | - .if_not_exists() | |
| 695 | - .name("idx_password_change_requests_expires_at") | |
| 696 | - .table(PasswordChangeRequests::Table) | |
| 697 | - .col(PasswordChangeRequests::ExpiresAt) | |
| 698 | - .to_owned(), | |
| 699 | - ) | |
| 700 | - .await?; | |
| 701 | - | |
| 702 | - manager | |
| 703 | - .get_connection() | |
| 704 | - .execute_unprepared( | |
| 705 | - r#" | |
| 706 | - CREATE UNIQUE INDEX IF NOT EXISTS idx_password_change_requests_user_id_pending | |
| 707 | - ON password_change_requests (user_id) | |
| 708 | - WHERE confirmed_at IS NULL | |
| 709 | - "#, | |
| 710 | - ) | |
| 711 | - .await?; | |
| 712 | - | |
| 713 | - // --- 10. email_queue --- | |
| 714 | - manager | |
| 715 | - .create_table( | |
| 716 | - Table::create() | |
| 717 | - .table(EmailQueue::Table) | |
| 718 | - .if_not_exists() | |
| 719 | - .col( | |
| 720 | - ColumnDef::new(EmailQueue::Id) | |
| 721 | - .uuid() | |
| 722 | - .not_null() | |
| 723 | - .primary_key(), | |
| 724 | - ) | |
| 725 | - .col( | |
| 726 | - ColumnDef::new(EmailQueue::ToEmail) | |
| 727 | - .string_len(255) | |
| 728 | - .not_null(), | |
| 729 | - ) | |
| 730 | - .col(ColumnDef::new(EmailQueue::ToName).string_len(255).null()) | |
| 731 | - .col( | |
| 732 | - ColumnDef::new(EmailQueue::FromEmail) | |
| 733 | - .string_len(255) | |
| 734 | - .not_null(), | |
| 735 | - ) | |
| 736 | - .col(ColumnDef::new(EmailQueue::FromName).string_len(255).null()) | |
| 737 | - .col( | |
| 738 | - ColumnDef::new(EmailQueue::Subject) | |
| 739 | - .string_len(500) | |
| 740 | - .not_null(), | |
| 741 | - ) | |
| 742 | - .col(ColumnDef::new(EmailQueue::BodyHtml).text().not_null()) | |
| 743 | - .col(ColumnDef::new(EmailQueue::BodyText).text().not_null()) | |
| 744 | - .col( | |
| 745 | - ColumnDef::new(EmailQueue::EmailType) | |
| 746 | - .string_len(50) | |
| 747 | - .not_null(), | |
| 748 | - ) | |
| 749 | - .col( | |
| 750 | - ColumnDef::new(EmailQueue::Status) | |
| 751 | - .string_len(20) | |
| 752 | - .not_null() | |
| 753 | - .default("pending"), | |
| 754 | - ) | |
| 755 | - .col( | |
| 756 | - ColumnDef::new(EmailQueue::Attempts) | |
| 757 | - .integer() | |
| 758 | - .not_null() | |
| 759 | - .default(0), | |
| 760 | - ) | |
| 761 | - .col( | |
| 762 | - ColumnDef::new(EmailQueue::LastAttemptAt) | |
| 763 | - .timestamp_with_time_zone() | |
| 764 | - .null(), | |
| 765 | - ) | |
| 766 | - .col( | |
| 767 | - ColumnDef::new(EmailQueue::SentAt) | |
| 768 | - .timestamp_with_time_zone() | |
| 769 | - .null(), | |
| 770 | - ) | |
| 771 | - .col(ColumnDef::new(EmailQueue::ErrorMessage).text().null()) | |
| 772 | - .col(ColumnDef::new(EmailQueue::SmtpCode).string_len(10).null()) | |
| 773 | - .col(ColumnDef::new(EmailQueue::SmtpResponse).text().null()) | |
| 774 | - .col( | |
| 775 | - ColumnDef::new(EmailQueue::CreatedAt) | |
| 776 | - .timestamp_with_time_zone() | |
| 777 | - .not_null(), | |
| 778 | - ) | |
| 779 | - .col( | |
| 780 | - ColumnDef::new(EmailQueue::UpdatedAt) | |
| 781 | - .timestamp_with_time_zone() | |
| 782 | - .not_null(), | |
| 783 | - ) | |
| 784 | - .to_owned(), | |
| 785 | - ) | |
| 786 | - .await?; | |
| 787 | - | |
| 788 | - manager | |
| 789 | - .create_index( | |
| 790 | - Index::create() | |
| 791 | - .if_not_exists() | |
| 792 | - .name("idx_email_queue_status") | |
| 793 | - .table(EmailQueue::Table) | |
| 794 | - .col(EmailQueue::Status) | |
| 795 | - .to_owned(), | |
| 796 | - ) | |
| 797 | - .await?; | |
| 798 | - | |
| 799 | - manager | |
| 800 | - .create_index( | |
| 801 | - Index::create() | |
| 802 | - .if_not_exists() | |
| 803 | - .name("idx_email_queue_created_at") | |
| 804 | - .table(EmailQueue::Table) | |
| 805 | - .col(EmailQueue::CreatedAt) | |
| 806 | - .to_owned(), | |
| 807 | - ) | |
| 808 | - .await?; | |
| 809 | - | |
| 810 | - manager | |
| 811 | - .create_index( | |
| 812 | - Index::create() | |
| 813 | - .if_not_exists() | |
| 814 | - .name("idx_email_queue_email_type") | |
| 815 | - .table(EmailQueue::Table) | |
| 816 | - .col(EmailQueue::EmailType) | |
| 817 | - .to_owned(), | |
| 818 | - ) | |
| 819 | - .await?; | |
| 820 | - | |
| 821 | - manager | |
| 822 | - .get_connection() | |
| 823 | - .execute_unprepared( | |
| 824 | - r#" | |
| 825 | - ALTER TABLE email_queue | |
| 826 | - ADD CONSTRAINT chk_email_queue_email_type | |
| 827 | - CHECK (email_type IN ( | |
| 828 | - 'password_reset', | |
| 829 | - 'email_validation', | |
| 830 | - 'password_changed', | |
| 831 | - 'account_modified', | |
| 832 | - 'welcome', | |
| 833 | - 'admin_new_user', | |
| 834 | - 'notification' | |
| 835 | - )) | |
| 836 | - "#, | |
| 837 | - ) | |
| 838 | - .await?; | |
| 839 | - | |
| 840 | - manager | |
| 841 | - .get_connection() | |
| 842 | - .execute_unprepared( | |
| 843 | - r#" | |
| 844 | - ALTER TABLE email_queue | |
| 845 | - ADD CONSTRAINT chk_email_queue_status | |
| 846 | - CHECK (status IN ('pending', 'sent', 'failed')) | |
| 847 | - "#, | |
| 848 | - ) | |
| 849 | - .await?; | |
| 850 | - | |
| 851 | - // --- 11. email_delivery_status --- | |
| 852 | - manager | |
| 853 | - .create_table( | |
| 854 | - Table::create() | |
| 855 | - .table(EmailDeliveryStatus::Table) | |
| 856 | - .if_not_exists() | |
| 857 | - .col( | |
| 858 | - ColumnDef::new(EmailDeliveryStatus::Id) | |
| 859 | - .uuid() | |
| 860 | - .not_null() | |
| 861 | - .primary_key(), | |
| 862 | - ) | |
| 863 | - .col( | |
| 864 | - ColumnDef::new(EmailDeliveryStatus::EmailType) | |
| 865 | - .string_len(50) | |
| 866 | - .not_null(), | |
| 867 | - ) | |
| 868 | - .col( | |
| 869 | - ColumnDef::new(EmailDeliveryStatus::Recipient) | |
| 870 | - .string_len(255) | |
| 871 | - .not_null(), | |
| 872 | - ) | |
| 873 | - .col( | |
| 874 | - ColumnDef::new(EmailDeliveryStatus::Template) | |
| 875 | - .string_len(255) | |
| 876 | - .not_null(), | |
| 877 | - ) | |
| 878 | - .col( | |
| 879 | - ColumnDef::new(EmailDeliveryStatus::State) | |
| 880 | - .string_len(20) | |
| 881 | - .not_null() | |
| 882 | - .default("queued"), | |
| 883 | - ) | |
| 884 | - .col( | |
| 885 | - ColumnDef::new(EmailDeliveryStatus::SmtpCode) | |
| 886 | - .string_len(10) | |
| 887 | - .null(), | |
| 888 | - ) | |
| 889 | - .col( | |
| 890 | - ColumnDef::new(EmailDeliveryStatus::SmtpResponse) | |
| 891 | - .text() | |
| 892 | - .null(), | |
| 893 | - ) | |
| 894 | - .col( | |
| 895 | - ColumnDef::new(EmailDeliveryStatus::AttemptCount) | |
| 896 | - .integer() | |
| 897 | - .not_null() | |
| 898 | - .default(0), | |
| 899 | - ) | |
| 900 | - .col( | |
| 901 | - ColumnDef::new(EmailDeliveryStatus::LastAttemptAt) | |
| 902 | - .timestamp_with_time_zone() | |
| 903 | - .null(), | |
| 904 | - ) | |
| 905 | - .col( | |
| 906 | - ColumnDef::new(EmailDeliveryStatus::LastStateAt) | |
| 907 | - .timestamp_with_time_zone() | |
| 908 | - .not_null(), | |
| 909 | - ) | |
| 910 | - .col( | |
| 911 | - ColumnDef::new(EmailDeliveryStatus::CorrelationId) | |
| 912 | - .uuid() | |
| 913 | - .null(), | |
| 914 | - ) | |
| 915 | - .col( | |
| 916 | - ColumnDef::new(EmailDeliveryStatus::CreatedAt) | |
| 917 | - .timestamp_with_time_zone() | |
| 918 | - .not_null(), | |
| 919 | - ) | |
| 920 | - .col( | |
| 921 | - ColumnDef::new(EmailDeliveryStatus::UpdatedAt) | |
| 922 | - .timestamp_with_time_zone() | |
| 923 | - .not_null(), | |
| 924 | - ) | |
| 925 | - .to_owned(), | |
| 926 | - ) | |
| 927 | - .await?; | |
| 928 | - | |
| 929 | - manager | |
| 930 | - .create_index( | |
| 931 | - Index::create() | |
| 932 | - .if_not_exists() | |
| 933 | - .name("idx_email_delivery_status_type") | |
| 934 | - .table(EmailDeliveryStatus::Table) | |
| 935 | - .col(EmailDeliveryStatus::EmailType) | |
| 936 | - .to_owned(), | |
| 937 | - ) | |
| 938 | - .await?; | |
| 939 | - | |
| 940 | - manager | |
| 941 | - .create_index( | |
| 942 | - Index::create() | |
| 943 | - .if_not_exists() | |
| 944 | - .name("idx_email_delivery_status_state") | |
| 945 | - .table(EmailDeliveryStatus::Table) | |
| 946 | - .col(EmailDeliveryStatus::State) | |
| 947 | - .to_owned(), | |
| 948 | - ) | |
| 949 | - .await?; | |
| 950 | - | |
| 951 | - manager | |
| 952 | - .create_index( | |
| 953 | - Index::create() | |
| 954 | - .if_not_exists() | |
| 955 | - .name("idx_email_delivery_status_recipient") | |
| 956 | - .table(EmailDeliveryStatus::Table) | |
| 957 | - .col(EmailDeliveryStatus::Recipient) | |
| 958 | - .to_owned(), | |
| 959 | - ) | |
| 960 | - .await?; | |
| 961 | - | |
| 962 | - manager | |
| 963 | - .create_index( | |
| 964 | - Index::create() | |
| 965 | - .if_not_exists() | |
| 966 | - .name("idx_email_delivery_status_last_state_at") | |
| 967 | - .table(EmailDeliveryStatus::Table) | |
| 968 | - .col(EmailDeliveryStatus::LastStateAt) | |
| 969 | - .to_owned(), | |
| 970 | - ) | |
| 971 | - .await?; | |
| 972 | - | |
| 973 | - // --- Section 12: audit_logs table --- | |
| 974 | - manager | |
| 975 | - .create_table( | |
| 976 | - Table::create() | |
| 977 | - .table(AuditLogs::Table) | |
| 978 | - .if_not_exists() | |
| 979 | - .col( | |
| 980 | - ColumnDef::new(AuditLogs::Id) | |
| 981 | - .uuid() | |
| 982 | - .not_null() | |
| 983 | - .primary_key(), | |
| 984 | - ) | |
| 985 | - .col(ColumnDef::new(AuditLogs::UserId).uuid()) | |
| 986 | - .col(ColumnDef::new(AuditLogs::Action).string_len(100).not_null()) | |
| 987 | - .col(ColumnDef::new(AuditLogs::TargetUserId).uuid()) | |
| 988 | - .col( | |
| 989 | - ColumnDef::new(AuditLogs::Level) | |
| 990 | - .string_len(10) | |
| 991 | - .not_null() | |
| 992 | - .default("INFO"), | |
| 993 | - ) | |
| 994 | - .col(ColumnDef::new(AuditLogs::Message).text().not_null()) | |
| 995 | - .col( | |
| 996 | - ColumnDef::new(AuditLogs::CreatedAt) | |
| 997 | - .timestamp_with_time_zone() | |
| 998 | - .not_null() | |
| 999 | - .default(Expr::current_timestamp()), | |
| 1000 | - ) | |
| 1001 | - .foreign_key( | |
| 1002 | - ForeignKey::create() | |
| 1003 | - .name("fk_audit_logs_user") | |
| 1004 | - .from(AuditLogs::Table, AuditLogs::UserId) | |
| 1005 | - .to(Users::Table, Users::Id) | |
| 1006 | - .on_delete(ForeignKeyAction::SetNull), | |
| 1007 | - ) | |
| 1008 | - .foreign_key( | |
| 1009 | - ForeignKey::create() | |
| 1010 | - .name("fk_audit_logs_target_user") | |
| 1011 | - .from(AuditLogs::Table, AuditLogs::TargetUserId) | |
| 1012 | - .to(Users::Table, Users::Id) | |
| 1013 | - .on_delete(ForeignKeyAction::SetNull), | |
| 1014 | - ) | |
| 1015 | - .to_owned(), | |
| 1016 | - ) | |
| 1017 | - .await?; | |
| 1018 | - | |
| 1019 | - manager | |
| 1020 | - .create_index( | |
| 1021 | - Index::create() | |
| 1022 | - .if_not_exists() | |
| 1023 | - .name("idx_audit_logs_user_id") | |
| 1024 | - .table(AuditLogs::Table) | |
| 1025 | - .col(AuditLogs::UserId) | |
| 1026 | - .to_owned(), | |
| 1027 | - ) | |
| 1028 | - .await?; | |
| 1029 | - | |
| 1030 | - manager | |
| 1031 | - .create_index( | |
| 1032 | - Index::create() | |
| 1033 | - .if_not_exists() | |
| 1034 | - .name("idx_audit_logs_action") | |
| 1035 | - .table(AuditLogs::Table) | |
| 1036 | - .col(AuditLogs::Action) | |
| 1037 | - .to_owned(), | |
| 1038 | - ) | |
| 1039 | - .await?; | |
| 1040 | - | |
| 1041 | - manager | |
| 1042 | - .create_index( | |
| 1043 | - Index::create() | |
| 1044 | - .if_not_exists() | |
| 1045 | - .name("idx_audit_logs_created_at") | |
| 1046 | - .table(AuditLogs::Table) | |
| 1047 | - .col(AuditLogs::CreatedAt) | |
| 1048 | - .to_owned(), | |
| 1049 | - ) | |
| 1050 | - .await?; | |
| 1051 | - | |
| 1052 | - // --- Seed default roles --- | |
| 1053 | - manager | |
| 1054 | - .get_connection() | |
| 1055 | - .execute_unprepared( | |
| 1056 | - r#" | |
| 1057 | - INSERT INTO roles (id, name, description, created_at, updated_at) | |
| 1058 | - VALUES | |
| 1059 | - (gen_random_uuid(), 'admin', 'Administrator role', NOW(), NOW()), | |
| 1060 | - (gen_random_uuid(), 'user', 'Standard user role', NOW(), NOW()) | |
| 1061 | - ON CONFLICT (name) DO NOTHING | |
| 1062 | - "#, | |
| 1063 | - ) | |
| 1064 | - .await?; | |
| 1065 | - | |
| 1066 | - Ok(()) | |
| 1067 | - } | |
| 1068 | - | |
| 1069 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 1070 | - manager | |
| 1071 | - .drop_table(Table::drop().table(AuditLogs::Table).to_owned()) | |
| 1072 | - .await?; | |
| 1073 | - | |
| 1074 | - manager | |
| 1075 | - .drop_table(Table::drop().table(EmailDeliveryStatus::Table).to_owned()) | |
| 1076 | - .await?; | |
| 1077 | - | |
| 1078 | - manager | |
| 1079 | - .drop_table(Table::drop().table(EmailQueue::Table).to_owned()) | |
| 1080 | - .await?; | |
| 1081 | - | |
| 1082 | - manager | |
| 1083 | - .drop_table( | |
| 1084 | - Table::drop() | |
| 1085 | - .table(PasswordChangeRequests::Table) | |
| 1086 | - .to_owned(), | |
| 1087 | - ) | |
| 1088 | - .await?; | |
| 1089 | - | |
| 1090 | - manager | |
| 1091 | - .drop_table(Table::drop().table(EmailValidationTokens::Table).to_owned()) | |
| 1092 | - .await?; | |
| 1093 | - | |
| 1094 | - manager | |
| 1095 | - .drop_table(Table::drop().table(PasswordResetTokens::Table).to_owned()) | |
| 1096 | - .await?; | |
| 1097 | - | |
| 1098 | - manager | |
| 1099 | - .drop_table(Table::drop().table(UserPreferences::Table).to_owned()) | |
| 1100 | - .await?; | |
| 1101 | - | |
| 1102 | - manager | |
| 1103 | - .drop_table(Table::drop().table(JwtBlacklist::Table).to_owned()) | |
| 1104 | - .await?; | |
| 1105 | - | |
| 1106 | - manager | |
| 1107 | - .drop_table(Table::drop().table(RefreshToken::Table).to_owned()) | |
| 1108 | - .await?; | |
| 1109 | - | |
| 1110 | - manager | |
| 1111 | - .drop_table(Table::drop().table(UserRoles::Table).to_owned()) | |
| 1112 | - .await?; | |
| 1113 | - | |
| 1114 | - manager | |
| 1115 | - .drop_table(Table::drop().table(Roles::Table).to_owned()) | |
| 1116 | - .await?; | |
| 1117 | - | |
| 1118 | - manager | |
| 1119 | - .drop_table(Table::drop().table(Users::Table).to_owned()) | |
| 1120 | - .await | |
| 1121 | - } | |
| 1122 | -} | |
| 1123 | - | |
| 1124 | -#[derive(DeriveIden)] | |
| 1125 | -enum Users { | |
| 1126 | - Table, | |
| 1127 | - Id, | |
| 1128 | - Username, | |
| 1129 | - Email, | |
| 1130 | - PasswordHash, | |
| 1131 | - MustChangePassword, | |
| 1132 | - EmailVerified, | |
| 1133 | - EmailVerifiedAt, | |
| 1134 | - LastLoginAt, | |
| 1135 | - LastLoginIp, | |
| 1136 | - PasswordChangedAt, | |
| 1137 | - CreatedAt, | |
| 1138 | - UpdatedAt, | |
| 1139 | -} | |
| 1140 | - | |
| 1141 | -#[derive(DeriveIden)] | |
| 1142 | -enum Roles { | |
| 1143 | - Table, | |
| 1144 | - Id, | |
| 1145 | - Name, | |
| 1146 | - Description, | |
| 1147 | - CreatedAt, | |
| 1148 | - UpdatedAt, | |
| 1149 | -} | |
| 1150 | - | |
| 1151 | -#[derive(DeriveIden)] | |
| 1152 | -enum UserRoles { | |
| 1153 | - Table, | |
| 1154 | - UserId, | |
| 1155 | - RoleId, | |
| 1156 | - CreatedAt, | |
| 1157 | -} | |
| 1158 | - | |
| 1159 | -#[derive(DeriveIden)] | |
| 1160 | -enum RefreshToken { | |
| 1161 | - Table, | |
| 1162 | - Id, | |
| 1163 | - UserId, | |
| 1164 | - Token, | |
| 1165 | - ExpiresAt, | |
| 1166 | - CreatedAt, | |
| 1167 | - Revoked, | |
| 1168 | - RevokedAt, | |
| 1169 | - ReplacedBy, | |
| 1170 | -} | |
| 1171 | - | |
| 1172 | -#[derive(DeriveIden)] | |
| 1173 | -enum JwtBlacklist { | |
| 1174 | - Table, | |
| 1175 | - Id, | |
| 1176 | - TokenHash, | |
| 1177 | - UserId, | |
| 1178 | - ExpiresAt, | |
| 1179 | - RevokedAt, | |
| 1180 | - CreatedAt, | |
| 1181 | -} | |
| 1182 | - | |
| 1183 | -#[derive(DeriveIden)] | |
| 1184 | -enum UserPreferences { | |
| 1185 | - Table, | |
| 1186 | - Id, | |
| 1187 | - UserId, | |
| 1188 | - Theme, | |
| 1189 | - Language, | |
| 1190 | - CreatedAt, | |
| 1191 | - UpdatedAt, | |
| 1192 | -} | |
| 1193 | - | |
| 1194 | -#[derive(DeriveIden)] | |
| 1195 | -enum PasswordResetTokens { | |
| 1196 | - Table, | |
| 1197 | - Id, | |
| 1198 | - UserId, | |
| 1199 | - TokenHash, | |
| 1200 | - ExpiresAt, | |
| 1201 | - UsedAt, | |
| 1202 | - CreatedAt, | |
| 1203 | -} | |
| 1204 | - | |
| 1205 | -#[derive(DeriveIden)] | |
| 1206 | -enum EmailValidationTokens { | |
| 1207 | - Table, | |
| 1208 | - Id, | |
| 1209 | - UserId, | |
| 1210 | - TokenHash, | |
| 1211 | - ExpiresAt, | |
| 1212 | - UsedAt, | |
| 1213 | - CreatedAt, | |
| 1214 | -} | |
| 1215 | - | |
| 1216 | -#[derive(DeriveIden)] | |
| 1217 | -enum PasswordChangeRequests { | |
| 1218 | - Table, | |
| 1219 | - Id, | |
| 1220 | - UserId, | |
| 1221 | - NewPasswordHash, | |
| 1222 | - ConfirmationToken, | |
| 1223 | - ExpiresAt, | |
| 1224 | - CreatedAt, | |
| 1225 | - ConfirmedAt, | |
| 1226 | - EmailSentAt, | |
| 1227 | - RetryCount, | |
| 1228 | -} | |
| 1229 | - | |
| 1230 | -#[derive(DeriveIden)] | |
| 1231 | -enum EmailQueue { | |
| 1232 | - Table, | |
| 1233 | - Id, | |
| 1234 | - ToEmail, | |
| 1235 | - ToName, | |
| 1236 | - FromEmail, | |
| 1237 | - FromName, | |
| 1238 | - Subject, | |
| 1239 | - BodyHtml, | |
| 1240 | - BodyText, | |
| 1241 | - EmailType, | |
| 1242 | - Status, | |
| 1243 | - Attempts, | |
| 1244 | - LastAttemptAt, | |
| 1245 | - SentAt, | |
| 1246 | - ErrorMessage, | |
| 1247 | - SmtpCode, | |
| 1248 | - SmtpResponse, | |
| 1249 | - CreatedAt, | |
| 1250 | - UpdatedAt, | |
| 1251 | -} | |
| 1252 | - | |
| 1253 | -#[derive(DeriveIden)] | |
| 1254 | -enum EmailDeliveryStatus { | |
| 1255 | - Table, | |
| 1256 | - Id, | |
| 1257 | - EmailType, | |
| 1258 | - Recipient, | |
| 1259 | - Template, | |
| 1260 | - State, | |
| 1261 | - SmtpCode, | |
| 1262 | - SmtpResponse, | |
| 1263 | - AttemptCount, | |
| 1264 | - LastAttemptAt, | |
| 1265 | - LastStateAt, | |
| 1266 | - CorrelationId, | |
| 1267 | - CreatedAt, | |
| 1268 | - UpdatedAt, | |
| 1269 | -} | |
| 1270 | - | |
| 1271 | -#[derive(DeriveIden)] | |
| 1272 | -enum AuditLogs { | |
| 1273 | - Table, | |
| 1274 | - Id, | |
| 1275 | - UserId, | |
| 1276 | - Action, | |
| 1277 | - TargetUserId, | |
| 1278 | - Level, | |
| 1279 | - Message, | |
| 1280 | - CreatedAt, | |
| 1281 | -} |
D
crates/rustwarden-core/migrations/src/m20260306_000002_create_app_settings_table.rs
+0
-83
@@ -1,83 +0,0 @@
| 1 | -use sea_orm_migration::prelude::*; | |
| 2 | - | |
| 3 | -/// Migration: creates the `app_settings` key-value table. | |
| 4 | -#[derive(DeriveMigrationName)] | |
| 5 | -pub struct Migration; | |
| 6 | - | |
| 7 | -#[async_trait::async_trait] | |
| 8 | -impl MigrationTrait for Migration { | |
| 9 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | - manager | |
| 11 | - .create_table( | |
| 12 | - Table::create() | |
| 13 | - .table(AppSettings::Table) | |
| 14 | - .if_not_exists() | |
| 15 | - .col( | |
| 16 | - ColumnDef::new(AppSettings::Id) | |
| 17 | - .uuid() | |
| 18 | - .not_null() | |
| 19 | - .primary_key(), | |
| 20 | - ) | |
| 21 | - .col( | |
| 22 | - ColumnDef::new(AppSettings::Key) | |
| 23 | - .string() | |
| 24 | - .string_len(100) | |
| 25 | - .not_null() | |
| 26 | - .unique_key(), | |
| 27 | - ) | |
| 28 | - .col(ColumnDef::new(AppSettings::Value).text().not_null()) | |
| 29 | - .col(ColumnDef::new(AppSettings::Description).text().null()) | |
| 30 | - .col(ColumnDef::new(AppSettings::UpdatedBy).uuid().null()) | |
| 31 | - .col( | |
| 32 | - ColumnDef::new(AppSettings::UpdatedAt) | |
| 33 | - .timestamp_with_time_zone() | |
| 34 | - .not_null(), | |
| 35 | - ) | |
| 36 | - .foreign_key( | |
| 37 | - ForeignKey::create() | |
| 38 | - .name("fk_app_settings_updated_by") | |
| 39 | - .from(AppSettings::Table, AppSettings::UpdatedBy) | |
| 40 | - .to(Users::Table, Users::Id) | |
| 41 | - .on_delete(ForeignKeyAction::SetNull), | |
| 42 | - ) | |
| 43 | - .to_owned(), | |
| 44 | - ) | |
| 45 | - .await?; | |
| 46 | - | |
| 47 | - manager | |
| 48 | - .create_index( | |
| 49 | - Index::create() | |
| 50 | - .if_not_exists() | |
| 51 | - .name("idx_app_settings_key") | |
| 52 | - .table(AppSettings::Table) | |
| 53 | - .col(AppSettings::Key) | |
| 54 | - .to_owned(), | |
| 55 | - ) | |
| 56 | - .await?; | |
| 57 | - | |
| 58 | - Ok(()) | |
| 59 | - } | |
| 60 | - | |
| 61 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 62 | - manager | |
| 63 | - .drop_table(Table::drop().table(AppSettings::Table).to_owned()) | |
| 64 | - .await | |
| 65 | - } | |
| 66 | -} | |
| 67 | - | |
| 68 | -#[derive(DeriveIden)] | |
| 69 | -enum AppSettings { | |
| 70 | - Table, | |
| 71 | - Id, | |
| 72 | - Key, | |
| 73 | - Value, | |
| 74 | - Description, | |
| 75 | - UpdatedBy, | |
| 76 | - UpdatedAt, | |
| 77 | -} | |
| 78 | - | |
| 79 | -#[derive(DeriveIden)] | |
| 80 | -enum Users { | |
| 81 | - Table, | |
| 82 | - Id, | |
| 83 | -} |
D
crates/rustwarden-core/migrations/src/m20260309_000003_create_permissions_tables.rs
+0
-176
@@ -1,176 +0,0 @@
| 1 | -use sea_orm_migration::prelude::*; | |
| 2 | - | |
| 3 | -/// Migration: creates the `permissions`, `roles`, and `role_permissions` tables. | |
| 4 | -#[derive(DeriveMigrationName)] | |
| 5 | -pub struct Migration; | |
| 6 | - | |
| 7 | -#[async_trait::async_trait] | |
| 8 | -impl MigrationTrait for Migration { | |
| 9 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | - // --- 1. permissions table --- | |
| 11 | - manager | |
| 12 | - .create_table( | |
| 13 | - Table::create() | |
| 14 | - .table(Permissions::Table) | |
| 15 | - .if_not_exists() | |
| 16 | - .col( | |
| 17 | - ColumnDef::new(Permissions::Id) | |
| 18 | - .uuid() | |
| 19 | - .not_null() | |
| 20 | - .primary_key(), | |
| 21 | - ) | |
| 22 | - .col( | |
| 23 | - ColumnDef::new(Permissions::Name) | |
| 24 | - .string() | |
| 25 | - .string_len(100) | |
| 26 | - .not_null() | |
| 27 | - .unique_key(), | |
| 28 | - ) | |
| 29 | - .col(ColumnDef::new(Permissions::Description).text().null()) | |
| 30 | - .col( | |
| 31 | - ColumnDef::new(Permissions::Category) | |
| 32 | - .string() | |
| 33 | - .string_len(50) | |
| 34 | - .not_null(), | |
| 35 | - ) | |
| 36 | - .col( | |
| 37 | - ColumnDef::new(Permissions::CreatedAt) | |
| 38 | - .timestamp_with_time_zone() | |
| 39 | - .not_null(), | |
| 40 | - ) | |
| 41 | - .to_owned(), | |
| 42 | - ) | |
| 43 | - .await?; | |
| 44 | - | |
| 45 | - // --- 2. role_permissions junction table --- | |
| 46 | - manager | |
| 47 | - .create_table( | |
| 48 | - Table::create() | |
| 49 | - .table(RolePermissions::Table) | |
| 50 | - .if_not_exists() | |
| 51 | - .col(ColumnDef::new(RolePermissions::RoleId).uuid().not_null()) | |
| 52 | - .col( | |
| 53 | - ColumnDef::new(RolePermissions::PermissionId) | |
| 54 | - .uuid() | |
| 55 | - .not_null(), | |
| 56 | - ) | |
| 57 | - .primary_key( | |
| 58 | - Index::create() | |
| 59 | - .col(RolePermissions::RoleId) | |
| 60 | - .col(RolePermissions::PermissionId), | |
| 61 | - ) | |
| 62 | - .foreign_key( | |
| 63 | - ForeignKey::create() | |
| 64 | - .name("fk_role_permissions_role_id") | |
| 65 | - .from(RolePermissions::Table, RolePermissions::RoleId) | |
| 66 | - .to(Roles::Table, Roles::Id) | |
| 67 | - .on_delete(ForeignKeyAction::Cascade), | |
| 68 | - ) | |
| 69 | - .foreign_key( | |
| 70 | - ForeignKey::create() | |
| 71 | - .name("fk_role_permissions_permission_id") | |
| 72 | - .from(RolePermissions::Table, RolePermissions::PermissionId) | |
| 73 | - .to(Permissions::Table, Permissions::Id) | |
| 74 | - .on_delete(ForeignKeyAction::Cascade), | |
| 75 | - ) | |
| 76 | - .to_owned(), | |
| 77 | - ) | |
| 78 | - .await?; | |
| 79 | - | |
| 80 | - // --- 3. Indexes --- | |
| 81 | - manager | |
| 82 | - .create_index( | |
| 83 | - Index::create() | |
| 84 | - .if_not_exists() | |
| 85 | - .name("idx_permissions_category") | |
| 86 | - .table(Permissions::Table) | |
| 87 | - .col(Permissions::Category) | |
| 88 | - .to_owned(), | |
| 89 | - ) | |
| 90 | - .await?; | |
| 91 | - | |
| 92 | - manager | |
| 93 | - .create_index( | |
| 94 | - Index::create() | |
| 95 | - .if_not_exists() | |
| 96 | - .name("idx_role_permissions_role_id") | |
| 97 | - .table(RolePermissions::Table) | |
| 98 | - .col(RolePermissions::RoleId) | |
| 99 | - .to_owned(), | |
| 100 | - ) | |
| 101 | - .await?; | |
| 102 | - | |
| 103 | - // --- 4. Seed default permissions --- | |
| 104 | - manager | |
| 105 | - .get_connection() | |
| 106 | - .execute_unprepared( | |
| 107 | - r#" | |
| 108 | - INSERT INTO permissions (id, name, description, category, created_at) VALUES | |
| 109 | - (gen_random_uuid(), 'users.list', 'View the list of users', 'users', NOW()), | |
| 110 | - (gen_random_uuid(), 'users.create', 'Create new users', 'users', NOW()), | |
| 111 | - (gen_random_uuid(), 'users.edit', 'Edit existing users', 'users', NOW()), | |
| 112 | - (gen_random_uuid(), 'users.delete', 'Delete users', 'users', NOW()), | |
| 113 | - (gen_random_uuid(), 'users.reset_password', 'Reset user passwords', 'users', NOW()), | |
| 114 | - (gen_random_uuid(), 'roles.list', 'View the list of roles', 'roles', NOW()), | |
| 115 | - (gen_random_uuid(), 'roles.create', 'Create new roles', 'roles', NOW()), | |
| 116 | - (gen_random_uuid(), 'roles.edit', 'Edit existing roles', 'roles', NOW()), | |
| 117 | - (gen_random_uuid(), 'roles.delete', 'Delete roles', 'roles', NOW()), | |
| 118 | - (gen_random_uuid(), 'settings.view', 'View application settings', 'settings', NOW()), | |
| 119 | - (gen_random_uuid(), 'settings.edit', 'Edit application settings', 'settings', NOW()), | |
| 120 | - (gen_random_uuid(), 'audit.view', 'View audit logs', 'audit', NOW()) | |
| 121 | - ON CONFLICT (name) DO NOTHING | |
| 122 | - "#, | |
| 123 | - ) | |
| 124 | - .await?; | |
| 125 | - | |
| 126 | - // --- 5. Assign all permissions to the admin role --- | |
| 127 | - manager | |
| 128 | - .get_connection() | |
| 129 | - .execute_unprepared( | |
| 130 | - r#" | |
| 131 | - INSERT INTO role_permissions (role_id, permission_id) | |
| 132 | - SELECT r.id, p.id | |
| 133 | - FROM roles r | |
| 134 | - CROSS JOIN permissions p | |
| 135 | - WHERE r.name = 'admin' | |
| 136 | - ON CONFLICT DO NOTHING | |
| 137 | - "#, | |
| 138 | - ) | |
| 139 | - .await?; | |
| 140 | - | |
| 141 | - Ok(()) | |
| 142 | - } | |
| 143 | - | |
| 144 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 145 | - manager | |
| 146 | - .drop_table(Table::drop().table(RolePermissions::Table).to_owned()) | |
| 147 | - .await?; | |
| 148 | - manager | |
| 149 | - .drop_table(Table::drop().table(Permissions::Table).to_owned()) | |
| 150 | - .await?; | |
| 151 | - Ok(()) | |
| 152 | - } | |
| 153 | -} | |
| 154 | - | |
| 155 | -#[derive(DeriveIden)] | |
| 156 | -enum Permissions { | |
| 157 | - Table, | |
| 158 | - Id, | |
| 159 | - Name, | |
| 160 | - Description, | |
| 161 | - Category, | |
| 162 | - CreatedAt, | |
| 163 | -} | |
| 164 | - | |
| 165 | -#[derive(DeriveIden)] | |
| 166 | -enum RolePermissions { | |
| 167 | - Table, | |
| 168 | - RoleId, | |
| 169 | - PermissionId, | |
| 170 | -} | |
| 171 | - | |
| 172 | -#[derive(DeriveIden)] | |
| 173 | -enum Roles { | |
| 174 | - Table, | |
| 175 | - Id, | |
| 176 | -} |
D
crates/rustwarden-core/migrations/src/m20260309_000004_create_resources_tables.rs
+0
-212
@@ -1,212 +0,0 @@
| 1 | -use sea_orm_migration::prelude::*; | |
| 2 | - | |
| 3 | -/// Migration: creates the `resources` and `resource_shares` tables. | |
| 4 | -#[derive(DeriveMigrationName)] | |
| 5 | -pub struct Migration; | |
| 6 | - | |
| 7 | -#[async_trait::async_trait] | |
| 8 | -impl MigrationTrait for Migration { | |
| 9 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | - // --- 1. resources table --- | |
| 11 | - manager | |
| 12 | - .create_table( | |
| 13 | - Table::create() | |
| 14 | - .table(Resources::Table) | |
| 15 | - .if_not_exists() | |
| 16 | - .col( | |
| 17 | - ColumnDef::new(Resources::Id) | |
| 18 | - .uuid() | |
| 19 | - .not_null() | |
| 20 | - .primary_key(), | |
| 21 | - ) | |
| 22 | - .col( | |
| 23 | - ColumnDef::new(Resources::ResourceType) | |
| 24 | - .string() | |
| 25 | - .string_len(100) | |
| 26 | - .not_null(), | |
| 27 | - ) | |
| 28 | - .col(ColumnDef::new(Resources::ResourceId).uuid().not_null()) | |
| 29 | - .col(ColumnDef::new(Resources::OwnerId).uuid().not_null()) | |
| 30 | - .col( | |
| 31 | - ColumnDef::new(Resources::IsPublic) | |
| 32 | - .boolean() | |
| 33 | - .not_null() | |
| 34 | - .default(false), | |
| 35 | - ) | |
| 36 | - .col( | |
| 37 | - ColumnDef::new(Resources::CreatedAt) | |
| 38 | - .timestamp_with_time_zone() | |
| 39 | - .not_null(), | |
| 40 | - ) | |
| 41 | - .foreign_key( | |
| 42 | - ForeignKey::create() | |
| 43 | - .name("fk_resources_owner_id") | |
| 44 | - .from(Resources::Table, Resources::OwnerId) | |
| 45 | - .to(Users::Table, Users::Id) | |
| 46 | - .on_delete(ForeignKeyAction::Cascade), | |
| 47 | - ) | |
| 48 | - .to_owned(), | |
| 49 | - ) | |
| 50 | - .await?; | |
| 51 | - | |
| 52 | - // Unique constraint: one registry entry per (type, id) | |
| 53 | - manager | |
| 54 | - .create_index( | |
| 55 | - Index::create() | |
| 56 | - .if_not_exists() | |
| 57 | - .name("idx_resources_type_id_unique") | |
| 58 | - .table(Resources::Table) | |
| 59 | - .col(Resources::ResourceType) | |
| 60 | - .col(Resources::ResourceId) | |
| 61 | - .unique() | |
| 62 | - .to_owned(), | |
| 63 | - ) | |
| 64 | - .await?; | |
| 65 | - | |
| 66 | - manager | |
| 67 | - .create_index( | |
| 68 | - Index::create() | |
| 69 | - .if_not_exists() | |
| 70 | - .name("idx_resources_owner_id") | |
| 71 | - .table(Resources::Table) | |
| 72 | - .col(Resources::OwnerId) | |
| 73 | - .to_owned(), | |
| 74 | - ) | |
| 75 | - .await?; | |
| 76 | - | |
| 77 | - manager | |
| 78 | - .create_index( | |
| 79 | - Index::create() | |
| 80 | - .if_not_exists() | |
| 81 | - .name("idx_resources_type") | |
| 82 | - .table(Resources::Table) | |
| 83 | - .col(Resources::ResourceType) | |
| 84 | - .to_owned(), | |
| 85 | - ) | |
| 86 | - .await?; | |
| 87 | - | |
| 88 | - // --- 2. resource_shares table --- | |
| 89 | - manager | |
| 90 | - .create_table( | |
| 91 | - Table::create() | |
| 92 | - .table(ResourceShares::Table) | |
| 93 | - .if_not_exists() | |
| 94 | - .col( | |
| 95 | - ColumnDef::new(ResourceShares::Id) | |
| 96 | - .uuid() | |
| 97 | - .not_null() | |
| 98 | - .primary_key(), | |
| 99 | - ) | |
| 100 | - .col(ColumnDef::new(ResourceShares::ResourceId).uuid().not_null()) | |
| 101 | - .col( | |
| 102 | - ColumnDef::new(ResourceShares::SharedWithUserId) | |
| 103 | - .uuid() | |
| 104 | - .not_null(), | |
| 105 | - ) | |
| 106 | - .col( | |
| 107 | - ColumnDef::new(ResourceShares::PermissionLevel) | |
| 108 | - .string() | |
| 109 | - .string_len(20) | |
| 110 | - .not_null(), | |
| 111 | - ) | |
| 112 | - .col( | |
| 113 | - ColumnDef::new(ResourceShares::SharedByUserId) | |
| 114 | - .uuid() | |
| 115 | - .not_null(), | |
| 116 | - ) | |
| 117 | - .col( | |
| 118 | - ColumnDef::new(ResourceShares::CreatedAt) | |
| 119 | - .timestamp_with_time_zone() | |
| 120 | - .not_null(), | |
| 121 | - ) | |
| 122 | - .foreign_key( | |
| 123 | - ForeignKey::create() | |
| 124 | - .name("fk_resource_shares_resource_id") | |
| 125 | - .from(ResourceShares::Table, ResourceShares::ResourceId) | |
| 126 | - .to(Resources::Table, Resources::Id) | |
| 127 | - .on_delete(ForeignKeyAction::Cascade), | |
| 128 | - ) | |
| 129 | - .foreign_key( | |
| 130 | - ForeignKey::create() | |
| 131 | - .name("fk_resource_shares_shared_with") | |
| 132 | - .from(ResourceShares::Table, ResourceShares::SharedWithUserId) | |
| 133 | - .to(Users::Table, Users::Id) | |
| 134 | - .on_delete(ForeignKeyAction::Cascade), | |
| 135 | - ) | |
| 136 | - .foreign_key( | |
| 137 | - ForeignKey::create() | |
| 138 | - .name("fk_resource_shares_shared_by") | |
| 139 | - .from(ResourceShares::Table, ResourceShares::SharedByUserId) | |
| 140 | - .to(Users::Table, Users::Id) | |
| 141 | - .on_delete(ForeignKeyAction::Cascade), | |
| 142 | - ) | |
| 143 | - .to_owned(), | |
| 144 | - ) | |
| 145 | - .await?; | |
| 146 | - | |
| 147 | - // One share per (resource, user) pair | |
| 148 | - manager | |
| 149 | - .create_index( | |
| 150 | - Index::create() | |
| 151 | - .if_not_exists() | |
| 152 | - .name("idx_resource_shares_resource_user_unique") | |
| 153 | - .table(ResourceShares::Table) | |
| 154 | - .col(ResourceShares::ResourceId) | |
| 155 | - .col(ResourceShares::SharedWithUserId) | |
| 156 | - .unique() | |
| 157 | - .to_owned(), | |
| 158 | - ) | |
| 159 | - .await?; | |
| 160 | - | |
| 161 | - manager | |
| 162 | - .create_index( | |
| 163 | - Index::create() | |
| 164 | - .if_not_exists() | |
| 165 | - .name("idx_resource_shares_shared_with") | |
| 166 | - .table(ResourceShares::Table) | |
| 167 | - .col(ResourceShares::SharedWithUserId) | |
| 168 | - .to_owned(), | |
| 169 | - ) | |
| 170 | - .await?; | |
| 171 | - | |
| 172 | - Ok(()) | |
| 173 | - } | |
| 174 | - | |
| 175 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 176 | - manager | |
| 177 | - .drop_table(Table::drop().table(ResourceShares::Table).to_owned()) | |
| 178 | - .await?; | |
| 179 | - manager | |
| 180 | - .drop_table(Table::drop().table(Resources::Table).to_owned()) | |
| 181 | - .await?; | |
| 182 | - Ok(()) | |
| 183 | - } | |
| 184 | -} | |
| 185 | - | |
| 186 | -#[derive(DeriveIden)] | |
| 187 | -enum Resources { | |
| 188 | - Table, | |
| 189 | - Id, | |
| 190 | - ResourceType, | |
| 191 | - ResourceId, | |
| 192 | - OwnerId, | |
| 193 | - IsPublic, | |
| 194 | - CreatedAt, | |
| 195 | -} | |
| 196 | - | |
| 197 | -#[derive(DeriveIden)] | |
| 198 | -enum ResourceShares { | |
| 199 | - Table, | |
| 200 | - Id, | |
| 201 | - ResourceId, | |
| 202 | - SharedWithUserId, | |
| 203 | - PermissionLevel, | |
| 204 | - SharedByUserId, | |
| 205 | - CreatedAt, | |
| 206 | -} | |
| 207 | - | |
| 208 | -#[derive(DeriveIden)] | |
| 209 | -enum Users { | |
| 210 | - Table, | |
| 211 | - Id, | |
| 212 | -} |
D
crates/rustwarden-core/migrations/src/m20260310_000005_create_oauth_accounts_table.rs
+0
-126
@@ -1,126 +0,0 @@
| 1 | -use sea_orm_migration::prelude::*; | |
| 2 | - | |
| 3 | -/// Migration: creates the `oauth_accounts` table for linked OAuth identities. | |
| 4 | -#[derive(DeriveMigrationName)] | |
| 5 | -pub struct Migration; | |
| 6 | - | |
| 7 | -#[async_trait::async_trait] | |
| 8 | -impl MigrationTrait for Migration { | |
| 9 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | - manager | |
| 11 | - .create_table( | |
| 12 | - Table::create() | |
| 13 | - .table(OauthAccounts::Table) | |
| 14 | - .if_not_exists() | |
| 15 | - .col( | |
| 16 | - ColumnDef::new(OauthAccounts::Id) | |
| 17 | - .uuid() | |
| 18 | - .not_null() | |
| 19 | - .primary_key(), | |
| 20 | - ) | |
| 21 | - .col(ColumnDef::new(OauthAccounts::UserId).uuid().not_null()) | |
| 22 | - .col( | |
| 23 | - ColumnDef::new(OauthAccounts::Provider) | |
| 24 | - .string() | |
| 25 | - .string_len(50) | |
| 26 | - .not_null(), | |
| 27 | - ) | |
| 28 | - .col( | |
| 29 | - ColumnDef::new(OauthAccounts::ProviderUserId) | |
| 30 | - .string() | |
| 31 | - .string_len(255) | |
| 32 | - .not_null(), | |
| 33 | - ) | |
| 34 | - .col( | |
| 35 | - ColumnDef::new(OauthAccounts::Email) | |
| 36 | - .string() | |
| 37 | - .string_len(255) | |
| 38 | - .null(), | |
| 39 | - ) | |
| 40 | - .col( | |
| 41 | - ColumnDef::new(OauthAccounts::DisplayName) | |
| 42 | - .string() | |
| 43 | - .string_len(255) | |
| 44 | - .null(), | |
| 45 | - ) | |
| 46 | - .col( | |
| 47 | - ColumnDef::new(OauthAccounts::AvatarUrl) | |
| 48 | - .string() | |
| 49 | - .string_len(1024) | |
| 50 | - .null(), | |
| 51 | - ) | |
| 52 | - .col( | |
| 53 | - ColumnDef::new(OauthAccounts::CreatedAt) | |
| 54 | - .timestamp_with_time_zone() | |
| 55 | - .not_null() | |
| 56 | - .default(Expr::current_timestamp()), | |
| 57 | - ) | |
| 58 | - .col( | |
| 59 | - ColumnDef::new(OauthAccounts::UpdatedAt) | |
| 60 | - .timestamp_with_time_zone() | |
| 61 | - .not_null() | |
| 62 | - .default(Expr::current_timestamp()), | |
| 63 | - ) | |
| 64 | - .foreign_key( | |
| 65 | - ForeignKey::create() | |
| 66 | - .name("fk_oauth_accounts_user_id") | |
| 67 | - .from(OauthAccounts::Table, OauthAccounts::UserId) | |
| 68 | - .to(Users::Table, Users::Id) | |
| 69 | - .on_delete(ForeignKeyAction::Cascade), | |
| 70 | - ) | |
| 71 | - .to_owned(), | |
| 72 | - ) | |
| 73 | - .await?; | |
| 74 | - | |
| 75 | - // Unique constraint: one provider account per user per provider | |
| 76 | - manager | |
| 77 | - .create_index( | |
| 78 | - Index::create() | |
| 79 | - .name("idx_oauth_accounts_provider_user") | |
| 80 | - .table(OauthAccounts::Table) | |
| 81 | - .col(OauthAccounts::Provider) | |
| 82 | - .col(OauthAccounts::ProviderUserId) | |
| 83 | - .unique() | |
| 84 | - .to_owned(), | |
| 85 | - ) | |
| 86 | - .await?; | |
| 87 | - | |
| 88 | - manager | |
| 89 | - .create_index( | |
| 90 | - Index::create() | |
| 91 | - .name("idx_oauth_accounts_user_id") | |
| 92 | - .table(OauthAccounts::Table) | |
| 93 | - .col(OauthAccounts::UserId) | |
| 94 | - .to_owned(), | |
| 95 | - ) | |
| 96 | - .await?; | |
| 97 | - | |
| 98 | - Ok(()) | |
| 99 | - } | |
| 100 | - | |
| 101 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 102 | - manager | |
| 103 | - .drop_table(Table::drop().table(OauthAccounts::Table).to_owned()) | |
| 104 | - .await | |
| 105 | - } | |
| 106 | -} | |
| 107 | - | |
| 108 | -#[derive(Iden)] | |
| 109 | -enum OauthAccounts { | |
| 110 | - Table, | |
| 111 | - Id, | |
| 112 | - UserId, | |
| 113 | - Provider, | |
| 114 | - ProviderUserId, | |
| 115 | - Email, | |
| 116 | - DisplayName, | |
| 117 | - AvatarUrl, | |
| 118 | - CreatedAt, | |
| 119 | - UpdatedAt, | |
| 120 | -} | |
| 121 | - | |
| 122 | -#[derive(Iden)] | |
| 123 | -enum Users { | |
| 124 | - Table, | |
| 125 | - Id, | |
| 126 | -} |
D
crates/rustwarden-core/migrations/src/m20260409_000006_create_totp_tables.rs
+0
-184
@@ -1,184 +0,0 @@
| 1 | -use sea_orm_migration::prelude::*; | |
| 2 | - | |
| 3 | -/// Migration: creates the TOTP secrets and backup-codes tables for 2FA. | |
| 4 | -#[derive(DeriveMigrationName)] | |
| 5 | -pub struct Migration; | |
| 6 | - | |
| 7 | -#[async_trait::async_trait] | |
| 8 | -impl MigrationTrait for Migration { | |
| 9 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | - // Table: user_totp (TOTP 2FA data per user) | |
| 11 | - manager | |
| 12 | - .create_table( | |
| 13 | - Table::create() | |
| 14 | - .table(UserTotp::Table) | |
| 15 | - .if_not_exists() | |
| 16 | - .col(ColumnDef::new(UserTotp::Id).uuid().not_null().primary_key()) | |
| 17 | - .col( | |
| 18 | - ColumnDef::new(UserTotp::UserId) | |
| 19 | - .uuid() | |
| 20 | - .not_null() | |
| 21 | - .unique_key(), | |
| 22 | - ) | |
| 23 | - .col(ColumnDef::new(UserTotp::SecretEncrypted).text().not_null()) | |
| 24 | - .col( | |
| 25 | - ColumnDef::new(UserTotp::Enabled) | |
| 26 | - .boolean() | |
| 27 | - .not_null() | |
| 28 | - .default(false), | |
| 29 | - ) | |
| 30 | - .col(ColumnDef::new(UserTotp::BackupCodesEncrypted).text().null()) | |
| 31 | - .col( | |
| 32 | - ColumnDef::new(UserTotp::BackupCodesRemaining) | |
| 33 | - .integer() | |
| 34 | - .not_null() | |
| 35 | - .default(0), | |
| 36 | - ) | |
| 37 | - .col( | |
| 38 | - ColumnDef::new(UserTotp::EnabledAt) | |
| 39 | - .timestamp_with_time_zone() | |
| 40 | - .null(), | |
| 41 | - ) | |
| 42 | - .col( | |
| 43 | - ColumnDef::new(UserTotp::CreatedAt) | |
| 44 | - .timestamp_with_time_zone() | |
| 45 | - .not_null() | |
| 46 | - .default(Expr::current_timestamp()), | |
| 47 | - ) | |
| 48 | - .col( | |
| 49 | - ColumnDef::new(UserTotp::UpdatedAt) | |
| 50 | - .timestamp_with_time_zone() | |
| 51 | - .not_null() | |
| 52 | - .default(Expr::current_timestamp()), | |
| 53 | - ) | |
| 54 | - .foreign_key( | |
| 55 | - ForeignKey::create() | |
| 56 | - .name("fk_user_totp_user_id") | |
| 57 | - .from(UserTotp::Table, UserTotp::UserId) | |
| 58 | - .to(Users::Table, Users::Id) | |
| 59 | - .on_delete(ForeignKeyAction::Cascade), | |
| 60 | - ) | |
| 61 | - .to_owned(), | |
| 62 | - ) | |
| 63 | - .await?; | |
| 64 | - | |
| 65 | - // Table: totp_challenges (ephemeral login 2FA challenges) | |
| 66 | - manager | |
| 67 | - .create_table( | |
| 68 | - Table::create() | |
| 69 | - .table(TotpChallenges::Table) | |
| 70 | - .if_not_exists() | |
| 71 | - .col( | |
| 72 | - ColumnDef::new(TotpChallenges::Id) | |
| 73 | - .uuid() | |
| 74 | - .not_null() | |
| 75 | - .primary_key(), | |
| 76 | - ) | |
| 77 | - .col(ColumnDef::new(TotpChallenges::UserId).uuid().not_null()) | |
| 78 | - .col( | |
| 79 | - ColumnDef::new(TotpChallenges::ChallengeToken) | |
| 80 | - .string() | |
| 81 | - .string_len(128) | |
| 82 | - .not_null() | |
| 83 | - .unique_key(), | |
| 84 | - ) | |
| 85 | - .col( | |
| 86 | - ColumnDef::new(TotpChallenges::Attempts) | |
| 87 | - .integer() | |
| 88 | - .not_null() | |
| 89 | - .default(0), | |
| 90 | - ) | |
| 91 | - .col( | |
| 92 | - ColumnDef::new(TotpChallenges::MaxAttempts) | |
| 93 | - .integer() | |
| 94 | - .not_null() | |
| 95 | - .default(5), | |
| 96 | - ) | |
| 97 | - .col( | |
| 98 | - ColumnDef::new(TotpChallenges::ExpiresAt) | |
| 99 | - .timestamp_with_time_zone() | |
| 100 | - .not_null(), | |
| 101 | - ) | |
| 102 | - .col( | |
| 103 | - ColumnDef::new(TotpChallenges::CreatedAt) | |
| 104 | - .timestamp_with_time_zone() | |
| 105 | - .not_null() | |
| 106 | - .default(Expr::current_timestamp()), | |
| 107 | - ) | |
| 108 | - .foreign_key( | |
| 109 | - ForeignKey::create() | |
| 110 | - .name("fk_totp_challenges_user_id") | |
| 111 | - .from(TotpChallenges::Table, TotpChallenges::UserId) | |
| 112 | - .to(Users::Table, Users::Id) | |
| 113 | - .on_delete(ForeignKeyAction::Cascade), | |
| 114 | - ) | |
| 115 | - .to_owned(), | |
| 116 | - ) | |
| 117 | - .await?; | |
| 118 | - | |
| 119 | - // Index on challenge_token for fast lookup | |
| 120 | - manager | |
| 121 | - .create_index( | |
| 122 | - Index::create() | |
| 123 | - .name("idx_totp_challenges_token") | |
| 124 | - .table(TotpChallenges::Table) | |
| 125 | - .col(TotpChallenges::ChallengeToken) | |
| 126 | - .to_owned(), | |
| 127 | - ) | |
| 128 | - .await?; | |
| 129 | - | |
| 130 | - // Index on expires_at for cleanup queries | |
| 131 | - manager | |
| 132 | - .create_index( | |
| 133 | - Index::create() | |
| 134 | - .name("idx_totp_challenges_expires") | |
| 135 | - .table(TotpChallenges::Table) | |
| 136 | - .col(TotpChallenges::ExpiresAt) | |
| 137 | - .to_owned(), | |
| 138 | - ) | |
| 139 | - .await?; | |
| 140 | - | |
| 141 | - Ok(()) | |
| 142 | - } | |
| 143 | - | |
| 144 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 145 | - manager | |
| 146 | - .drop_table(Table::drop().table(TotpChallenges::Table).to_owned()) | |
| 147 | - .await?; | |
| 148 | - manager | |
| 149 | - .drop_table(Table::drop().table(UserTotp::Table).to_owned()) | |
| 150 | - .await | |
| 151 | - } | |
| 152 | -} | |
| 153 | - | |
| 154 | -#[derive(Iden)] | |
| 155 | -enum UserTotp { | |
| 156 | - Table, | |
| 157 | - Id, | |
| 158 | - UserId, | |
| 159 | - SecretEncrypted, | |
| 160 | - Enabled, | |
| 161 | - BackupCodesEncrypted, | |
| 162 | - BackupCodesRemaining, | |
| 163 | - EnabledAt, | |
| 164 | - CreatedAt, | |
| 165 | - UpdatedAt, | |
| 166 | -} | |
| 167 | - | |
| 168 | -#[derive(Iden)] | |
| 169 | -enum TotpChallenges { | |
| 170 | - Table, | |
| 171 | - Id, | |
| 172 | - UserId, | |
| 173 | - ChallengeToken, | |
| 174 | - Attempts, | |
| 175 | - MaxAttempts, | |
| 176 | - ExpiresAt, | |
| 177 | - CreatedAt, | |
| 178 | -} | |
| 179 | - | |
| 180 | -#[derive(Iden)] | |
| 181 | -enum Users { | |
| 182 | - Table, | |
| 183 | - Id, | |
| 184 | -} |
D
crates/rustwarden-core/migrations/src/m20260422_000007_create_personal_access_tokens.rs
+0
-147
@@ -1,147 +0,0 @@
| 1 | -use sea_orm_migration::prelude::*; | |
| 2 | - | |
| 3 | -/// Migration: creates the `personal_access_tokens` table. | |
| 4 | -#[derive(DeriveMigrationName)] | |
| 5 | -pub struct Migration; | |
| 6 | - | |
| 7 | -#[async_trait::async_trait] | |
| 8 | -impl MigrationTrait for Migration { | |
| 9 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | - // Table: personal_access_tokens (long-lived API tokens with scopes) | |
| 11 | - // | |
| 12 | - // Complements refresh_token: whereas refresh_token implements JWT | |
| 13 | - // rotation (one-use, detects reuse), PATs are static secrets consumed | |
| 14 | - // unchanged across calls (e.g. git push over HTTPS). Hash-only storage, | |
| 15 | - // nullable expiration, and per-user unique names. | |
| 16 | - manager | |
| 17 | - .create_table( | |
| 18 | - Table::create() | |
| 19 | - .table(PersonalAccessTokens::Table) | |
| 20 | - .if_not_exists() | |
| 21 | - .col( | |
| 22 | - ColumnDef::new(PersonalAccessTokens::Id) | |
| 23 | - .uuid() | |
| 24 | - .not_null() | |
| 25 | - .primary_key(), | |
| 26 | - ) | |
| 27 | - .col( | |
| 28 | - ColumnDef::new(PersonalAccessTokens::UserId) | |
| 29 | - .uuid() | |
| 30 | - .not_null(), | |
| 31 | - ) | |
| 32 | - .col( | |
| 33 | - ColumnDef::new(PersonalAccessTokens::Name) | |
| 34 | - .string() | |
| 35 | - .string_len(128) | |
| 36 | - .not_null(), | |
| 37 | - ) | |
| 38 | - .col( | |
| 39 | - ColumnDef::new(PersonalAccessTokens::TokenHash) | |
| 40 | - .string() | |
| 41 | - .string_len(64) // SHA-256 hex | |
| 42 | - .not_null() | |
| 43 | - .unique_key(), | |
| 44 | - ) | |
| 45 | - .col( | |
| 46 | - ColumnDef::new(PersonalAccessTokens::Scopes) | |
| 47 | - .array(sea_query::ColumnType::Text) | |
| 48 | - .not_null(), | |
| 49 | - ) | |
| 50 | - .col( | |
| 51 | - ColumnDef::new(PersonalAccessTokens::ExpiresAt) | |
| 52 | - .timestamp_with_time_zone() | |
| 53 | - .null(), | |
| 54 | - ) | |
| 55 | - .col( | |
| 56 | - ColumnDef::new(PersonalAccessTokens::LastUsedAt) | |
| 57 | - .timestamp_with_time_zone() | |
| 58 | - .null(), | |
| 59 | - ) | |
| 60 | - .col( | |
| 61 | - ColumnDef::new(PersonalAccessTokens::RevokedAt) | |
| 62 | - .timestamp_with_time_zone() | |
| 63 | - .null(), | |
| 64 | - ) | |
| 65 | - .col( | |
| 66 | - ColumnDef::new(PersonalAccessTokens::CreatedAt) | |
| 67 | - .timestamp_with_time_zone() | |
| 68 | - .not_null() | |
| 69 | - .default(Expr::current_timestamp()), | |
| 70 | - ) | |
| 71 | - .foreign_key( | |
| 72 | - ForeignKey::create() | |
| 73 | - .name("fk_personal_access_tokens_user_id") | |
| 74 | - .from(PersonalAccessTokens::Table, PersonalAccessTokens::UserId) | |
| 75 | - .to(Users::Table, Users::Id) | |
| 76 | - .on_delete(ForeignKeyAction::Cascade), | |
| 77 | - ) | |
| 78 | - .to_owned(), | |
| 79 | - ) | |
| 80 | - .await?; | |
| 81 | - | |
| 82 | - // One token name per user (UI uniqueness guarantee). | |
| 83 | - manager | |
| 84 | - .create_index( | |
| 85 | - Index::create() | |
| 86 | - .name("idx_pat_name_per_user") | |
| 87 | - .table(PersonalAccessTokens::Table) | |
| 88 | - .col(PersonalAccessTokens::UserId) | |
| 89 | - .col(PersonalAccessTokens::Name) | |
| 90 | - .unique() | |
| 91 | - .to_owned(), | |
| 92 | - ) | |
| 93 | - .await?; | |
| 94 | - | |
| 95 | - // Fast lookup for `verify(token)` — token_hash is already unique, but | |
| 96 | - // an explicit btree index documents intent and lets EXPLAIN stay stable | |
| 97 | - // across rewrites. | |
| 98 | - manager | |
| 99 | - .create_index( | |
| 100 | - Index::create() | |
| 101 | - .name("idx_pat_token_hash") | |
| 102 | - .table(PersonalAccessTokens::Table) | |
| 103 | - .col(PersonalAccessTokens::TokenHash) | |
| 104 | - .to_owned(), | |
| 105 | - ) | |
| 106 | - .await?; | |
| 107 | - | |
| 108 | - // Supports the cleanup job `WHERE expires_at < now()`. | |
| 109 | - manager | |
| 110 | - .create_index( | |
| 111 | - Index::create() | |
| 112 | - .name("idx_pat_expires_at") | |
| 113 | - .table(PersonalAccessTokens::Table) | |
| 114 | - .col(PersonalAccessTokens::ExpiresAt) | |
| 115 | - .to_owned(), | |
| 116 | - ) | |
| 117 | - .await?; | |
| 118 | - | |
| 119 | - Ok(()) | |
| 120 | - } | |
| 121 | - | |
| 122 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 123 | - manager | |
| 124 | - .drop_table(Table::drop().table(PersonalAccessTokens::Table).to_owned()) | |
| 125 | - .await | |
| 126 | - } | |
| 127 | -} | |
| 128 | - | |
| 129 | -#[derive(Iden)] | |
| 130 | -enum PersonalAccessTokens { | |
| 131 | - Table, | |
| 132 | - Id, | |
| 133 | - UserId, | |
| 134 | - Name, | |
| 135 | - TokenHash, | |
| 136 | - Scopes, | |
| 137 | - ExpiresAt, | |
| 138 | - LastUsedAt, | |
| 139 | - RevokedAt, | |
| 140 | - CreatedAt, | |
| 141 | -} | |
| 142 | - | |
| 143 | -#[derive(Iden)] | |
| 144 | -enum Users { | |
| 145 | - Table, | |
| 146 | - Id, | |
| 147 | -} |
D
crates/rustwarden-core/migrations/src/m20260423_000008_create_teams.rs
+0
-277
@@ -1,277 +0,0 @@
| 1 | -use sea_orm_migration::prelude::*; | |
| 2 | - | |
| 3 | -/// Migration: creates the `teams` and `team_members` tables. | |
| 4 | -#[derive(DeriveMigrationName)] | |
| 5 | -pub struct Migration; | |
| 6 | - | |
| 7 | -#[async_trait::async_trait] | |
| 8 | -impl MigrationTrait for Migration { | |
| 9 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | - // ----------------------------------------------------------------- | |
| 11 | - // 1. teams — named groups of users with an owner | |
| 12 | - // ----------------------------------------------------------------- | |
| 13 | - manager | |
| 14 | - .create_table( | |
| 15 | - Table::create() | |
| 16 | - .table(Teams::Table) | |
| 17 | - .if_not_exists() | |
| 18 | - .col(ColumnDef::new(Teams::Id).uuid().not_null().primary_key()) | |
| 19 | - .col( | |
| 20 | - ColumnDef::new(Teams::Slug) | |
| 21 | - .string() | |
| 22 | - .string_len(64) | |
| 23 | - .not_null() | |
| 24 | - .unique_key(), | |
| 25 | - ) | |
| 26 | - .col( | |
| 27 | - ColumnDef::new(Teams::Name) | |
| 28 | - .string() | |
| 29 | - .string_len(128) | |
| 30 | - .not_null(), | |
| 31 | - ) | |
| 32 | - .col(ColumnDef::new(Teams::OwnerId).uuid().not_null()) | |
| 33 | - .col( | |
| 34 | - ColumnDef::new(Teams::CreatedAt) | |
| 35 | - .timestamp_with_time_zone() | |
| 36 | - .not_null() | |
| 37 | - .default(Expr::current_timestamp()), | |
| 38 | - ) | |
| 39 | - .foreign_key( | |
| 40 | - ForeignKey::create() | |
| 41 | - .name("fk_teams_owner_id") | |
| 42 | - .from(Teams::Table, Teams::OwnerId) | |
| 43 | - .to(Users::Table, Users::Id) | |
| 44 | - .on_delete(ForeignKeyAction::Cascade), | |
| 45 | - ) | |
| 46 | - .to_owned(), | |
| 47 | - ) | |
| 48 | - .await?; | |
| 49 | - | |
| 50 | - // ----------------------------------------------------------------- | |
| 51 | - // 2. team_members — N:N with role (owner/admin/member) | |
| 52 | - // ----------------------------------------------------------------- | |
| 53 | - manager | |
| 54 | - .create_table( | |
| 55 | - Table::create() | |
| 56 | - .table(TeamMembers::Table) | |
| 57 | - .if_not_exists() | |
| 58 | - .col(ColumnDef::new(TeamMembers::TeamId).uuid().not_null()) | |
| 59 | - .col(ColumnDef::new(TeamMembers::UserId).uuid().not_null()) | |
| 60 | - .col( | |
| 61 | - ColumnDef::new(TeamMembers::Role) | |
| 62 | - .string() | |
| 63 | - .string_len(16) | |
| 64 | - .not_null() | |
| 65 | - .default("member"), | |
| 66 | - ) | |
| 67 | - .col( | |
| 68 | - ColumnDef::new(TeamMembers::JoinedAt) | |
| 69 | - .timestamp_with_time_zone() | |
| 70 | - .not_null() | |
| 71 | - .default(Expr::current_timestamp()), | |
| 72 | - ) | |
| 73 | - .primary_key( | |
| 74 | - Index::create() | |
| 75 | - .col(TeamMembers::TeamId) | |
| 76 | - .col(TeamMembers::UserId), | |
| 77 | - ) | |
| 78 | - .foreign_key( | |
| 79 | - ForeignKey::create() | |
| 80 | - .name("fk_team_members_team_id") | |
| 81 | - .from(TeamMembers::Table, TeamMembers::TeamId) | |
| 82 | - .to(Teams::Table, Teams::Id) | |
| 83 | - .on_delete(ForeignKeyAction::Cascade), | |
| 84 | - ) | |
| 85 | - .foreign_key( | |
| 86 | - ForeignKey::create() | |
| 87 | - .name("fk_team_members_user_id") | |
| 88 | - .from(TeamMembers::Table, TeamMembers::UserId) | |
| 89 | - .to(Users::Table, Users::Id) | |
| 90 | - .on_delete(ForeignKeyAction::Cascade), | |
| 91 | - ) | |
| 92 | - .to_owned(), | |
| 93 | - ) | |
| 94 | - .await?; | |
| 95 | - | |
| 96 | - // Index: list teams of a user in constant time. | |
| 97 | - manager | |
| 98 | - .create_index( | |
| 99 | - Index::create() | |
| 100 | - .name("idx_team_members_user") | |
| 101 | - .table(TeamMembers::Table) | |
| 102 | - .col(TeamMembers::UserId) | |
| 103 | - .to_owned(), | |
| 104 | - ) | |
| 105 | - .await?; | |
| 106 | - | |
| 107 | - // ----------------------------------------------------------------- | |
| 108 | - // 3. resource_shares — extend to accept team principals | |
| 109 | - // | |
| 110 | - // Strategy (option B of the evolution plan): keep the existing | |
| 111 | - // `shared_with_user_id` column (now nullable) and add a companion | |
| 112 | - // `shared_with_team_id`. A DB-level CHECK constraint enforces that | |
| 113 | - // exactly one is set — keeps schema-level referential integrity | |
| 114 | - // instead of a polymorphic ID + discriminator string. | |
| 115 | - // ----------------------------------------------------------------- | |
| 116 | - manager | |
| 117 | - .get_connection() | |
| 118 | - .execute_unprepared( | |
| 119 | - "ALTER TABLE resource_shares ALTER COLUMN shared_with_user_id DROP NOT NULL", | |
| 120 | - ) | |
| 121 | - .await?; | |
| 122 | - | |
| 123 | - manager | |
| 124 | - .alter_table( | |
| 125 | - Table::alter() | |
| 126 | - .table(ResourceShares::Table) | |
| 127 | - .add_column( | |
| 128 | - ColumnDef::new(ResourceShares::SharedWithTeamId) | |
| 129 | - .uuid() | |
| 130 | - .null(), | |
| 131 | - ) | |
| 132 | - .to_owned(), | |
| 133 | - ) | |
| 134 | - .await?; | |
| 135 | - | |
| 136 | - manager | |
| 137 | - .get_connection() | |
| 138 | - .execute_unprepared( | |
| 139 | - "ALTER TABLE resource_shares \ | |
| 140 | - ADD CONSTRAINT fk_resource_shares_shared_with_team \ | |
| 141 | - FOREIGN KEY (shared_with_team_id) REFERENCES teams(id) ON DELETE CASCADE", | |
| 142 | - ) | |
| 143 | - .await?; | |
| 144 | - | |
| 145 | - // Exactly one of (user, team) must be set. | |
| 146 | - manager | |
| 147 | - .get_connection() | |
| 148 | - .execute_unprepared( | |
| 149 | - "ALTER TABLE resource_shares \ | |
| 150 | - ADD CONSTRAINT chk_resource_shares_principal \ | |
| 151 | - CHECK ((shared_with_user_id IS NOT NULL)::int + \ | |
| 152 | - (shared_with_team_id IS NOT NULL)::int = 1)", | |
| 153 | - ) | |
| 154 | - .await?; | |
| 155 | - | |
| 156 | - // The existing unique (resource_id, shared_with_user_id) index becomes | |
| 157 | - // partial — only enforce uniqueness when the user column is set. | |
| 158 | - // We drop & recreate instead of altering to keep this cross-backend-friendly. | |
| 159 | - manager | |
| 160 | - .get_connection() | |
| 161 | - .execute_unprepared("DROP INDEX IF EXISTS idx_resource_shares_resource_user_unique") | |
| 162 | - .await?; | |
| 163 | - | |
| 164 | - manager | |
| 165 | - .get_connection() | |
| 166 | - .execute_unprepared( | |
| 167 | - "CREATE UNIQUE INDEX idx_resource_shares_resource_user_unique \ | |
| 168 | - ON resource_shares (resource_id, shared_with_user_id) \ | |
| 169 | - WHERE shared_with_user_id IS NOT NULL", | |
| 170 | - ) | |
| 171 | - .await?; | |
| 172 | - | |
| 173 | - manager | |
| 174 | - .get_connection() | |
| 175 | - .execute_unprepared( | |
| 176 | - "CREATE UNIQUE INDEX idx_resource_shares_resource_team_unique \ | |
| 177 | - ON resource_shares (resource_id, shared_with_team_id) \ | |
| 178 | - WHERE shared_with_team_id IS NOT NULL", | |
| 179 | - ) | |
| 180 | - .await?; | |
| 181 | - | |
| 182 | - manager | |
| 183 | - .get_connection() | |
| 184 | - .execute_unprepared( | |
| 185 | - "CREATE INDEX idx_resource_shares_team ON resource_shares (shared_with_team_id) \ | |
| 186 | - WHERE shared_with_team_id IS NOT NULL", | |
| 187 | - ) | |
| 188 | - .await?; | |
| 189 | - | |
| 190 | - Ok(()) | |
| 191 | - } | |
| 192 | - | |
| 193 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 194 | - manager | |
| 195 | - .get_connection() | |
| 196 | - .execute_unprepared("DROP INDEX IF EXISTS idx_resource_shares_team") | |
| 197 | - .await?; | |
| 198 | - manager | |
| 199 | - .get_connection() | |
| 200 | - .execute_unprepared("DROP INDEX IF EXISTS idx_resource_shares_resource_team_unique") | |
| 201 | - .await?; | |
| 202 | - manager | |
| 203 | - .get_connection() | |
| 204 | - .execute_unprepared("DROP INDEX IF EXISTS idx_resource_shares_resource_user_unique") | |
| 205 | - .await?; | |
| 206 | - manager | |
| 207 | - .get_connection() | |
| 208 | - .execute_unprepared( | |
| 209 | - "CREATE UNIQUE INDEX idx_resource_shares_resource_user_unique \ | |
| 210 | - ON resource_shares (resource_id, shared_with_user_id)", | |
| 211 | - ) | |
| 212 | - .await?; | |
| 213 | - manager | |
| 214 | - .get_connection() | |
| 215 | - .execute_unprepared( | |
| 216 | - "ALTER TABLE resource_shares DROP CONSTRAINT IF EXISTS chk_resource_shares_principal", | |
| 217 | - ) | |
| 218 | - .await?; | |
| 219 | - manager | |
| 220 | - .get_connection() | |
| 221 | - .execute_unprepared( | |
| 222 | - "ALTER TABLE resource_shares DROP CONSTRAINT IF EXISTS fk_resource_shares_shared_with_team", | |
| 223 | - ) | |
| 224 | - .await?; | |
| 225 | - manager | |
| 226 | - .alter_table( | |
| 227 | - Table::alter() | |
| 228 | - .table(ResourceShares::Table) | |
| 229 | - .drop_column(ResourceShares::SharedWithTeamId) | |
| 230 | - .to_owned(), | |
| 231 | - ) | |
| 232 | - .await?; | |
| 233 | - manager | |
| 234 | - .get_connection() | |
| 235 | - .execute_unprepared( | |
| 236 | - "ALTER TABLE resource_shares ALTER COLUMN shared_with_user_id SET NOT NULL", | |
| 237 | - ) | |
| 238 | - .await?; | |
| 239 | - manager | |
| 240 | - .drop_table(Table::drop().table(TeamMembers::Table).to_owned()) | |
| 241 | - .await?; | |
| 242 | - manager | |
| 243 | - .drop_table(Table::drop().table(Teams::Table).to_owned()) | |
| 244 | - .await | |
| 245 | - } | |
| 246 | -} | |
| 247 | - | |
| 248 | -#[derive(Iden)] | |
| 249 | -enum Teams { | |
| 250 | - Table, | |
| 251 | - Id, | |
| 252 | - Slug, | |
| 253 | - Name, | |
| 254 | - OwnerId, | |
| 255 | - CreatedAt, | |
| 256 | -} | |
| 257 | - | |
| 258 | -#[derive(Iden)] | |
| 259 | -enum TeamMembers { | |
| 260 | - Table, | |
| 261 | - TeamId, | |
| 262 | - UserId, | |
| 263 | - Role, | |
| 264 | - JoinedAt, | |
| 265 | -} | |
| 266 | - | |
| 267 | -#[derive(Iden)] | |
| 268 | -enum ResourceShares { | |
| 269 | - Table, | |
| 270 | - SharedWithTeamId, | |
| 271 | -} | |
| 272 | - | |
| 273 | -#[derive(Iden)] | |
| 274 | -enum Users { | |
| 275 | - Table, | |
| 276 | - Id, | |
| 277 | -} |
D
crates/rustwarden-core/migrations/src/m20260510_000009_fediverse_interop.rs
+0
-364
@@ -1,364 +0,0 @@
| 1 | -//! Migration `m20260510_000009_fediverse_interop`. | |
| 2 | -//! | |
| 3 | -//! Fused migration that defines the fediverse-aware surface of the | |
| 4 | -//! core schema. Replaces the legacy granular migrations: | |
| 5 | -//! | |
| 6 | -//! - `m20260510_000009_remote_identities` — `remote_identities` table (RFC 0003) | |
| 7 | -//! - `m20260510_000011_extend_users_with_actor_keys` — `users.ap_*` columns (RFC 0005) | |
| 8 | -//! - `m20260510_000014_identity_sibling_fks` — sibling FK columns on | |
| 9 | -//! `team_members`, `resource_shares`, `audit_logs` + their partial | |
| 10 | -//! indexes (RFC 0008) | |
| 11 | -//! - `m20260510_000016_relax_team_members_pk` — surrogate `team_members.id` | |
| 12 | -//! PK, XOR check on `(user_id, remote_identity_id)`, partial | |
| 13 | -//! uniqueness indexes (RFC 0018) | |
| 14 | -//! | |
| 15 | -//! Pure-protocol fediverse tables (`fediverse_trusted_domains`, | |
| 16 | -//! `fediverse_outbound_queue`, `fediverse_auth_challenges`, | |
| 17 | -//! `fediverse_pending_invites`) live in the dedicated | |
| 18 | -//! `rustwarden-fediverse` migrator instead. | |
| 19 | - | |
| 20 | -use sea_orm_migration::prelude::*; | |
| 21 | -use sea_orm_migration::sea_orm::ConnectionTrait; | |
| 22 | - | |
| 23 | -#[derive(DeriveMigrationName)] | |
| 24 | -pub struct Migration; | |
| 25 | - | |
| 26 | -#[async_trait::async_trait] | |
| 27 | -impl MigrationTrait for Migration { | |
| 28 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 29 | - // ---- RFC 0003: cache for remote ActivityPub actors ----------- | |
| 30 | - manager | |
| 31 | - .create_table( | |
| 32 | - Table::create() | |
| 33 | - .table(RemoteIdentities::Table) | |
| 34 | - .col( | |
| 35 | - ColumnDef::new(RemoteIdentities::Id) | |
| 36 | - .uuid() | |
| 37 | - .not_null() | |
| 38 | - .primary_key(), | |
| 39 | - ) | |
| 40 | - .col( | |
| 41 | - ColumnDef::new(RemoteIdentities::ApId) | |
| 42 | - .text() | |
| 43 | - .not_null() | |
| 44 | - .unique_key(), | |
| 45 | - ) | |
| 46 | - .col(ColumnDef::new(RemoteIdentities::Username).text().not_null()) | |
| 47 | - .col(ColumnDef::new(RemoteIdentities::Domain).text().not_null()) | |
| 48 | - .col(ColumnDef::new(RemoteIdentities::DisplayName).text().null()) | |
| 49 | - .col(ColumnDef::new(RemoteIdentities::SummaryHtml).text().null()) | |
| 50 | - .col(ColumnDef::new(RemoteIdentities::InboxUrl).text().not_null()) | |
| 51 | - .col(ColumnDef::new(RemoteIdentities::SharedInboxUrl).text().null()) | |
| 52 | - .col( | |
| 53 | - ColumnDef::new(RemoteIdentities::PublicKeyPem) | |
| 54 | - .text() | |
| 55 | - .not_null(), | |
| 56 | - ) | |
| 57 | - .col(ColumnDef::new(RemoteIdentities::IconUrl).text().null()) | |
| 58 | - .col( | |
| 59 | - ColumnDef::new(RemoteIdentities::ActorType) | |
| 60 | - .text() | |
| 61 | - .not_null() | |
| 62 | - .default("Person"), | |
| 63 | - ) | |
| 64 | - .col(ColumnDef::new(RemoteIdentities::RawJson).json_binary().null()) | |
| 65 | - .col( | |
| 66 | - ColumnDef::new(RemoteIdentities::LastFetchedAt) | |
| 67 | - .timestamp_with_time_zone() | |
| 68 | - .not_null(), | |
| 69 | - ) | |
| 70 | - .col( | |
| 71 | - ColumnDef::new(RemoteIdentities::DisabledAt) | |
| 72 | - .timestamp_with_time_zone() | |
| 73 | - .null(), | |
| 74 | - ) | |
| 75 | - .col( | |
| 76 | - ColumnDef::new(RemoteIdentities::CreatedAt) | |
| 77 | - .timestamp_with_time_zone() | |
| 78 | - .not_null(), | |
| 79 | - ) | |
| 80 | - .col( | |
| 81 | - ColumnDef::new(RemoteIdentities::UpdatedAt) | |
| 82 | - .timestamp_with_time_zone() | |
| 83 | - .not_null(), | |
| 84 | - ) | |
| 85 | - .to_owned(), | |
| 86 | - ) | |
| 87 | - .await?; | |
| 88 | - | |
| 89 | - manager | |
| 90 | - .create_index( | |
| 91 | - Index::create() | |
| 92 | - .table(RemoteIdentities::Table) | |
| 93 | - .name("uniq_remote_identities_handle") | |
| 94 | - .col(RemoteIdentities::Username) | |
| 95 | - .col(RemoteIdentities::Domain) | |
| 96 | - .unique() | |
| 97 | - .to_owned(), | |
| 98 | - ) | |
| 99 | - .await?; | |
| 100 | - | |
| 101 | - // ---- RFC 0005: per-user ActivityPub actor keys --------------- | |
| 102 | - manager | |
| 103 | - .alter_table( | |
| 104 | - Table::alter() | |
| 105 | - .table(Users::Table) | |
| 106 | - .add_column(ColumnDef::new(Users::ApPublicKeyPem).text().null()) | |
| 107 | - .to_owned(), | |
| 108 | - ) | |
| 109 | - .await?; | |
| 110 | - manager | |
| 111 | - .alter_table( | |
| 112 | - Table::alter() | |
| 113 | - .table(Users::Table) | |
| 114 | - .add_column(ColumnDef::new(Users::ApPrivateKeyPem).text().null()) | |
| 115 | - .to_owned(), | |
| 116 | - ) | |
| 117 | - .await?; | |
| 118 | - manager | |
| 119 | - .alter_table( | |
| 120 | - Table::alter() | |
| 121 | - .table(Users::Table) | |
| 122 | - .add_column( | |
| 123 | - ColumnDef::new(Users::ApDiscoverable) | |
| 124 | - .boolean() | |
| 125 | - .not_null() | |
| 126 | - .default(true), | |
| 127 | - ) | |
| 128 | - .to_owned(), | |
| 129 | - ) | |
| 130 | - .await?; | |
| 131 | - | |
| 132 | - // ---- RFC 0008: sibling FK columns for remote identities ------ | |
| 133 | - manager | |
| 134 | - .alter_table( | |
| 135 | - Table::alter() | |
| 136 | - .table(TeamMembers::Table) | |
| 137 | - .add_column(ColumnDef::new(TeamMembers::RemoteIdentityId).uuid().null()) | |
| 138 | - .to_owned(), | |
| 139 | - ) | |
| 140 | - .await?; | |
| 141 | - manager | |
| 142 | - .alter_table( | |
| 143 | - Table::alter() | |
| 144 | - .table(ResourceShares::Table) | |
| 145 | - .add_column( | |
| 146 | - ColumnDef::new(ResourceShares::SharedWithRemoteIdentityId) | |
| 147 | - .uuid() | |
| 148 | - .null(), | |
| 149 | - ) | |
| 150 | - .to_owned(), | |
| 151 | - ) | |
| 152 | - .await?; | |
| 153 | - manager | |
| 154 | - .alter_table( | |
| 155 | - Table::alter() | |
| 156 | - .table(AuditLogs::Table) | |
| 157 | - .add_column( | |
| 158 | - ColumnDef::new(AuditLogs::ActorRemoteIdentityId) | |
| 159 | - .uuid() | |
| 160 | - .null(), | |
| 161 | - ) | |
| 162 | - .to_owned(), | |
| 163 | - ) | |
| 164 | - .await?; | |
| 165 | - manager | |
| 166 | - .alter_table( | |
| 167 | - Table::alter() | |
| 168 | - .table(AuditLogs::Table) | |
| 169 | - .add_column( | |
| 170 | - ColumnDef::new(AuditLogs::TargetRemoteIdentityId) | |
| 171 | - .uuid() | |
| 172 | - .null(), | |
| 173 | - ) | |
| 174 | - .to_owned(), | |
| 175 | - ) | |
| 176 | - .await?; | |
| 177 | - | |
| 178 | - let conn = manager.get_connection(); | |
| 179 | - conn.execute_unprepared( | |
| 180 | - "CREATE INDEX team_members_remote_identity_idx \ | |
| 181 | - ON team_members (remote_identity_id) \ | |
| 182 | - WHERE remote_identity_id IS NOT NULL", | |
| 183 | - ) | |
| 184 | - .await?; | |
| 185 | - conn.execute_unprepared( | |
| 186 | - "CREATE INDEX resource_shares_remote_identity_idx \ | |
| 187 | - ON resource_shares (shared_with_remote_identity_id) \ | |
| 188 | - WHERE shared_with_remote_identity_id IS NOT NULL", | |
| 189 | - ) | |
| 190 | - .await?; | |
| 191 | - | |
| 192 | - // ---- RFC 0018: surrogate team_members.id PK + XOR ------------ | |
| 193 | - conn.execute_unprepared("ALTER TABLE team_members DROP CONSTRAINT team_members_pkey") | |
| 194 | - .await?; | |
| 195 | - conn.execute_unprepared("ALTER TABLE team_members ALTER COLUMN user_id DROP NOT NULL") | |
| 196 | - .await?; | |
| 197 | - conn.execute_unprepared( | |
| 198 | - "ALTER TABLE team_members ADD CONSTRAINT chk_team_members_principal \ | |
| 199 | - CHECK ((user_id IS NOT NULL)::int + (remote_identity_id IS NOT NULL)::int = 1)", | |
| 200 | - ) | |
| 201 | - .await?; | |
| 202 | - conn.execute_unprepared("CREATE EXTENSION IF NOT EXISTS pgcrypto") | |
| 203 | - .await?; | |
| 204 | - conn.execute_unprepared( | |
| 205 | - "ALTER TABLE team_members ADD COLUMN id UUID NOT NULL DEFAULT gen_random_uuid()", | |
| 206 | - ) | |
| 207 | - .await?; | |
| 208 | - conn.execute_unprepared("ALTER TABLE team_members ADD PRIMARY KEY (id)") | |
| 209 | - .await?; | |
| 210 | - conn.execute_unprepared( | |
| 211 | - "CREATE UNIQUE INDEX team_members_local_unique \ | |
| 212 | - ON team_members (team_id, user_id) \ | |
| 213 | - WHERE user_id IS NOT NULL", | |
| 214 | - ) | |
| 215 | - .await?; | |
| 216 | - conn.execute_unprepared( | |
| 217 | - "CREATE UNIQUE INDEX team_members_remote_unique \ | |
| 218 | - ON team_members (team_id, remote_identity_id) \ | |
| 219 | - WHERE remote_identity_id IS NOT NULL", | |
| 220 | - ) | |
| 221 | - .await?; | |
| 222 | - | |
| 223 | - Ok(()) | |
| 224 | - } | |
| 225 | - | |
| 226 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 227 | - let conn = manager.get_connection(); | |
| 228 | - | |
| 229 | - // ---- RFC 0018 reverse ---------------------------------------- | |
| 230 | - for sql in [ | |
| 231 | - "DROP INDEX IF EXISTS team_members_remote_unique", | |
| 232 | - "DROP INDEX IF EXISTS team_members_local_unique", | |
| 233 | - "ALTER TABLE team_members DROP CONSTRAINT IF EXISTS team_members_pkey", | |
| 234 | - "ALTER TABLE team_members DROP COLUMN IF EXISTS id", | |
| 235 | - "ALTER TABLE team_members DROP CONSTRAINT IF EXISTS chk_team_members_principal", | |
| 236 | - "ALTER TABLE team_members ALTER COLUMN user_id SET NOT NULL", | |
| 237 | - "ALTER TABLE team_members ADD PRIMARY KEY (team_id, user_id)", | |
| 238 | - ] { | |
| 239 | - conn.execute_unprepared(sql).await?; | |
| 240 | - } | |
| 241 | - | |
| 242 | - // ---- RFC 0008 reverse: drop partial indexes then columns ----- | |
| 243 | - for sql in [ | |
| 244 | - "DROP INDEX IF EXISTS resource_shares_remote_identity_idx", | |
| 245 | - "DROP INDEX IF EXISTS team_members_remote_identity_idx", | |
| 246 | - ] { | |
| 247 | - conn.execute_unprepared(sql).await?; | |
| 248 | - } | |
| 249 | - | |
| 250 | - manager | |
| 251 | - .alter_table( | |
| 252 | - Table::alter() | |
| 253 | - .table(AuditLogs::Table) | |
| 254 | - .drop_column(AuditLogs::TargetRemoteIdentityId) | |
| 255 | - .to_owned(), | |
| 256 | - ) | |
| 257 | - .await?; | |
| 258 | - manager | |
| 259 | - .alter_table( | |
| 260 | - Table::alter() | |
| 261 | - .table(AuditLogs::Table) | |
| 262 | - .drop_column(AuditLogs::ActorRemoteIdentityId) | |
| 263 | - .to_owned(), | |
| 264 | - ) | |
| 265 | - .await?; | |
| 266 | - manager | |
| 267 | - .alter_table( | |
| 268 | - Table::alter() | |
| 269 | - .table(ResourceShares::Table) | |
| 270 | - .drop_column(ResourceShares::SharedWithRemoteIdentityId) | |
| 271 | - .to_owned(), | |
| 272 | - ) | |
| 273 | - .await?; | |
| 274 | - manager | |
| 275 | - .alter_table( | |
| 276 | - Table::alter() | |
| 277 | - .table(TeamMembers::Table) | |
| 278 | - .drop_column(TeamMembers::RemoteIdentityId) | |
| 279 | - .to_owned(), | |
| 280 | - ) | |
| 281 | - .await?; | |
| 282 | - | |
| 283 | - // ---- RFC 0005 reverse ---------------------------------------- | |
| 284 | - manager | |
| 285 | - .alter_table( | |
| 286 | - Table::alter() | |
| 287 | - .table(Users::Table) | |
| 288 | - .drop_column(Users::ApDiscoverable) | |
| 289 | - .to_owned(), | |
| 290 | - ) | |
| 291 | - .await?; | |
| 292 | - manager | |
| 293 | - .alter_table( | |
| 294 | - Table::alter() | |
| 295 | - .table(Users::Table) | |
| 296 | - .drop_column(Users::ApPrivateKeyPem) | |
| 297 | - .to_owned(), | |
| 298 | - ) | |
| 299 | - .await?; | |
| 300 | - manager | |
| 301 | - .alter_table( | |
| 302 | - Table::alter() | |
| 303 | - .table(Users::Table) | |
| 304 | - .drop_column(Users::ApPublicKeyPem) | |
| 305 | - .to_owned(), | |
| 306 | - ) | |
| 307 | - .await?; | |
| 308 | - | |
| 309 | - // ---- RFC 0003 reverse ---------------------------------------- | |
| 310 | - manager | |
| 311 | - .drop_table(Table::drop().table(RemoteIdentities::Table).to_owned()) | |
| 312 | - .await?; | |
| 313 | - | |
| 314 | - Ok(()) | |
| 315 | - } | |
| 316 | -} | |
| 317 | - | |
| 318 | -#[derive(DeriveIden)] | |
| 319 | -enum RemoteIdentities { | |
| 320 | - Table, | |
| 321 | - Id, | |
| 322 | - ApId, | |
| 323 | - Username, | |
| 324 | - Domain, | |
| 325 | - DisplayName, | |
| 326 | - SummaryHtml, | |
| 327 | - InboxUrl, | |
| 328 | - SharedInboxUrl, | |
| 329 | - PublicKeyPem, | |
| 330 | - IconUrl, | |
| 331 | - ActorType, | |
| 332 | - RawJson, | |
| 333 | - LastFetchedAt, | |
| 334 | - DisabledAt, | |
| 335 | - CreatedAt, | |
| 336 | - UpdatedAt, | |
| 337 | -} | |
| 338 | - | |
| 339 | -#[derive(DeriveIden)] | |
| 340 | -enum Users { | |
| 341 | - Table, | |
| 342 | - ApPublicKeyPem, | |
| 343 | - ApPrivateKeyPem, | |
| 344 | - ApDiscoverable, | |
| 345 | -} | |
| 346 | - | |
| 347 | -#[derive(DeriveIden)] | |
| 348 | -enum TeamMembers { | |
| 349 | - Table, | |
| 350 | - RemoteIdentityId, | |
| 351 | -} | |
| 352 | - | |
| 353 | -#[derive(DeriveIden)] | |
| 354 | -enum ResourceShares { | |
| 355 | - Table, | |
| 356 | - SharedWithRemoteIdentityId, | |
| 357 | -} | |
| 358 | - | |
| 359 | -#[derive(DeriveIden)] | |
| 360 | -enum AuditLogs { | |
| 361 | - Table, | |
| 362 | - ActorRemoteIdentityId, | |
| 363 | - TargetRemoteIdentityId, | |
| 364 | -} |
A
crates/rustwarden-core/src/migrations/m20260305_000001_initial_schema.rs
+1281
-0
@@ -0,0 +1,1281 @@
| 1 | +use sea_orm_migration::prelude::*; | |
| 2 | + | |
| 3 | +/// Initial schema migration: creates the core users, roles, and sessions tables. | |
| 4 | +#[derive(DeriveMigrationName)] | |
| 5 | +pub struct Migration; | |
| 6 | + | |
| 7 | +#[async_trait::async_trait] | |
| 8 | +impl MigrationTrait for Migration { | |
| 9 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | + // --- 1. users --- | |
| 11 | + manager | |
| 12 | + .create_table( | |
| 13 | + Table::create() | |
| 14 | + .table(Users::Table) | |
| 15 | + .if_not_exists() | |
| 16 | + .col(ColumnDef::new(Users::Id).uuid().not_null().primary_key()) | |
| 17 | + .col( | |
| 18 | + ColumnDef::new(Users::Username) | |
| 19 | + .string() | |
| 20 | + .string_len(64) | |
| 21 | + .not_null() | |
| 22 | + .unique_key(), | |
| 23 | + ) | |
| 24 | + .col( | |
| 25 | + ColumnDef::new(Users::Email) | |
| 26 | + .string() | |
| 27 | + .string_len(255) | |
| 28 | + .not_null() | |
| 29 | + .unique_key(), | |
| 30 | + ) | |
| 31 | + .col( | |
| 32 | + ColumnDef::new(Users::PasswordHash) | |
| 33 | + .string() | |
| 34 | + .string_len(255) | |
| 35 | + .not_null(), | |
| 36 | + ) | |
| 37 | + .col( | |
| 38 | + ColumnDef::new(Users::MustChangePassword) | |
| 39 | + .boolean() | |
| 40 | + .not_null() | |
| 41 | + .default(false), | |
| 42 | + ) | |
| 43 | + .col( | |
| 44 | + ColumnDef::new(Users::EmailVerified) | |
| 45 | + .boolean() | |
| 46 | + .not_null() | |
| 47 | + .default(false), | |
| 48 | + ) | |
| 49 | + .col( | |
| 50 | + ColumnDef::new(Users::EmailVerifiedAt) | |
| 51 | + .timestamp_with_time_zone() | |
| 52 | + .null(), | |
| 53 | + ) | |
| 54 | + .col( | |
| 55 | + ColumnDef::new(Users::LastLoginAt) | |
| 56 | + .timestamp_with_time_zone() | |
| 57 | + .null(), | |
| 58 | + ) | |
| 59 | + .col( | |
| 60 | + ColumnDef::new(Users::LastLoginIp) | |
| 61 | + .string() | |
| 62 | + .string_len(45) | |
| 63 | + .null(), | |
| 64 | + ) | |
| 65 | + .col( | |
| 66 | + ColumnDef::new(Users::PasswordChangedAt) | |
| 67 | + .timestamp_with_time_zone() | |
| 68 | + .null(), | |
| 69 | + ) | |
| 70 | + .col( | |
| 71 | + ColumnDef::new(Users::CreatedAt) | |
| 72 | + .timestamp_with_time_zone() | |
| 73 | + .not_null(), | |
| 74 | + ) | |
| 75 | + .col( | |
| 76 | + ColumnDef::new(Users::UpdatedAt) | |
| 77 | + .timestamp_with_time_zone() | |
| 78 | + .not_null(), | |
| 79 | + ) | |
| 80 | + .to_owned(), | |
| 81 | + ) | |
| 82 | + .await?; | |
| 83 | + | |
| 84 | + manager | |
| 85 | + .create_index( | |
| 86 | + Index::create() | |
| 87 | + .if_not_exists() | |
| 88 | + .name("idx_users_email_verified") | |
| 89 | + .table(Users::Table) | |
| 90 | + .col(Users::EmailVerified) | |
| 91 | + .to_owned(), | |
| 92 | + ) | |
| 93 | + .await?; | |
| 94 | + | |
| 95 | + manager | |
| 96 | + .create_index( | |
| 97 | + Index::create() | |
| 98 | + .if_not_exists() | |
| 99 | + .name("idx_users_last_login_at") | |
| 100 | + .table(Users::Table) | |
| 101 | + .col(Users::LastLoginAt) | |
| 102 | + .to_owned(), | |
| 103 | + ) | |
| 104 | + .await?; | |
| 105 | + | |
| 106 | + manager | |
| 107 | + .create_index( | |
| 108 | + Index::create() | |
| 109 | + .if_not_exists() | |
| 110 | + .name("idx_users_password_changed_at") | |
| 111 | + .table(Users::Table) | |
| 112 | + .col(Users::PasswordChangedAt) | |
| 113 | + .to_owned(), | |
| 114 | + ) | |
| 115 | + .await?; | |
| 116 | + | |
| 117 | + // --- 2. roles --- | |
| 118 | + manager | |
| 119 | + .create_table( | |
| 120 | + Table::create() | |
| 121 | + .table(Roles::Table) | |
| 122 | + .if_not_exists() | |
| 123 | + .col(ColumnDef::new(Roles::Id).uuid().not_null().primary_key()) | |
| 124 | + .col( | |
| 125 | + ColumnDef::new(Roles::Name) | |
| 126 | + .string() | |
| 127 | + .string_len(64) | |
| 128 | + .not_null() | |
| 129 | + .unique_key(), | |
| 130 | + ) | |
| 131 | + .col(ColumnDef::new(Roles::Description).text().null()) | |
| 132 | + .col( | |
| 133 | + ColumnDef::new(Roles::CreatedAt) | |
| 134 | + .timestamp_with_time_zone() | |
| 135 | + .not_null(), | |
| 136 | + ) | |
| 137 | + .col( | |
| 138 | + ColumnDef::new(Roles::UpdatedAt) | |
| 139 | + .timestamp_with_time_zone() | |
| 140 | + .not_null(), | |
| 141 | + ) | |
| 142 | + .to_owned(), | |
| 143 | + ) | |
| 144 | + .await?; | |
| 145 | + | |
| 146 | + // --- 3. user_roles --- | |
| 147 | + manager | |
| 148 | + .create_table( | |
| 149 | + Table::create() | |
| 150 | + .table(UserRoles::Table) | |
| 151 | + .if_not_exists() | |
| 152 | + .col(ColumnDef::new(UserRoles::UserId).uuid().not_null()) | |
| 153 | + .col(ColumnDef::new(UserRoles::RoleId).uuid().not_null()) | |
| 154 | + .col( | |
| 155 | + ColumnDef::new(UserRoles::CreatedAt) | |
| 156 | + .timestamp_with_time_zone() | |
| 157 | + .not_null(), | |
| 158 | + ) | |
| 159 | + .primary_key( | |
| 160 | + Index::create() | |
| 161 | + .col(UserRoles::UserId) | |
| 162 | + .col(UserRoles::RoleId), | |
| 163 | + ) | |
| 164 | + .foreign_key( | |
| 165 | + ForeignKey::create() | |
| 166 | + .name("fk_user_roles_user_id") | |
| 167 | + .from(UserRoles::Table, UserRoles::UserId) | |
| 168 | + .to(Users::Table, Users::Id) | |
| 169 | + .on_delete(ForeignKeyAction::Cascade), | |
| 170 | + ) | |
| 171 | + .foreign_key( | |
| 172 | + ForeignKey::create() | |
| 173 | + .name("fk_user_roles_role_id") | |
| 174 | + .from(UserRoles::Table, UserRoles::RoleId) | |
| 175 | + .to(Roles::Table, Roles::Id) | |
| 176 | + .on_delete(ForeignKeyAction::Cascade), | |
| 177 | + ) | |
| 178 | + .to_owned(), | |
| 179 | + ) | |
| 180 | + .await?; | |
| 181 | + | |
| 182 | + // --- 4. refresh_token --- | |
| 183 | + manager | |
| 184 | + .create_table( | |
| 185 | + Table::create() | |
| 186 | + .table(RefreshToken::Table) | |
| 187 | + .if_not_exists() | |
| 188 | + .col( | |
| 189 | + ColumnDef::new(RefreshToken::Id) | |
| 190 | + .uuid() | |
| 191 | + .not_null() | |
| 192 | + .primary_key(), | |
| 193 | + ) | |
| 194 | + .col(ColumnDef::new(RefreshToken::UserId).uuid().not_null()) | |
| 195 | + .col( | |
| 196 | + ColumnDef::new(RefreshToken::Token) | |
| 197 | + .string() | |
| 198 | + .not_null() | |
| 199 | + .unique_key(), | |
| 200 | + ) | |
| 201 | + .col( | |
| 202 | + ColumnDef::new(RefreshToken::ExpiresAt) | |
| 203 | + .timestamp_with_time_zone() | |
| 204 | + .not_null(), | |
| 205 | + ) | |
| 206 | + .col( | |
| 207 | + ColumnDef::new(RefreshToken::CreatedAt) | |
| 208 | + .timestamp_with_time_zone() | |
| 209 | + .not_null(), | |
| 210 | + ) | |
| 211 | + .col( | |
| 212 | + ColumnDef::new(RefreshToken::Revoked) | |
| 213 | + .boolean() | |
| 214 | + .not_null() | |
| 215 | + .default(false), | |
| 216 | + ) | |
| 217 | + .col( | |
| 218 | + ColumnDef::new(RefreshToken::RevokedAt) | |
| 219 | + .timestamp_with_time_zone() | |
| 220 | + .null(), | |
| 221 | + ) | |
| 222 | + .col(ColumnDef::new(RefreshToken::ReplacedBy).uuid().null()) | |
| 223 | + .foreign_key( | |
| 224 | + ForeignKey::create() | |
| 225 | + .name("fk_refresh_token_user_id") | |
| 226 | + .from(RefreshToken::Table, RefreshToken::UserId) | |
| 227 | + .to(Users::Table, Users::Id) | |
| 228 | + .on_delete(ForeignKeyAction::Cascade), | |
| 229 | + ) | |
| 230 | + .foreign_key( | |
| 231 | + ForeignKey::create() | |
| 232 | + .name("fk_refresh_token_replaced_by") | |
| 233 | + .from(RefreshToken::Table, RefreshToken::ReplacedBy) | |
| 234 | + .to(RefreshToken::Table, RefreshToken::Id) | |
| 235 | + .on_delete(ForeignKeyAction::SetNull), | |
| 236 | + ) | |
| 237 | + .to_owned(), | |
| 238 | + ) | |
| 239 | + .await?; | |
| 240 | + | |
| 241 | + manager | |
| 242 | + .create_index( | |
| 243 | + Index::create() | |
| 244 | + .if_not_exists() | |
| 245 | + .name("idx_refresh_token_user_id") | |
| 246 | + .table(RefreshToken::Table) | |
| 247 | + .col(RefreshToken::UserId) | |
| 248 | + .to_owned(), | |
| 249 | + ) | |
| 250 | + .await?; | |
| 251 | + | |
| 252 | + manager | |
| 253 | + .create_index( | |
| 254 | + Index::create() | |
| 255 | + .if_not_exists() | |
| 256 | + .name("idx_refresh_token_token") | |
| 257 | + .table(RefreshToken::Table) | |
| 258 | + .col(RefreshToken::Token) | |
| 259 | + .to_owned(), | |
| 260 | + ) | |
| 261 | + .await?; | |
| 262 | + | |
| 263 | + manager | |
| 264 | + .create_index( | |
| 265 | + Index::create() | |
| 266 | + .if_not_exists() | |
| 267 | + .name("idx_refresh_token_expires_at") | |
| 268 | + .table(RefreshToken::Table) | |
| 269 | + .col(RefreshToken::ExpiresAt) | |
| 270 | + .to_owned(), | |
| 271 | + ) | |
| 272 | + .await?; | |
| 273 | + | |
| 274 | + // --- 5. jwt_blacklist --- | |
| 275 | + manager | |
| 276 | + .create_table( | |
| 277 | + Table::create() | |
| 278 | + .table(JwtBlacklist::Table) | |
| 279 | + .if_not_exists() | |
| 280 | + .col( | |
| 281 | + ColumnDef::new(JwtBlacklist::Id) | |
| 282 | + .uuid() | |
| 283 | + .not_null() | |
| 284 | + .primary_key(), | |
| 285 | + ) | |
| 286 | + .col( | |
| 287 | + ColumnDef::new(JwtBlacklist::TokenHash) | |
| 288 | + .string() | |
| 289 | + .not_null() | |
| 290 | + .unique_key(), | |
| 291 | + ) | |
| 292 | + .col(ColumnDef::new(JwtBlacklist::UserId).uuid().not_null()) | |
| 293 | + .col( | |
| 294 | + ColumnDef::new(JwtBlacklist::ExpiresAt) | |
| 295 | + .timestamp_with_time_zone() | |
| 296 | + .not_null(), | |
| 297 | + ) | |
| 298 | + .col( | |
| 299 | + ColumnDef::new(JwtBlacklist::RevokedAt) | |
| 300 | + .timestamp_with_time_zone() | |
| 301 | + .not_null(), | |
| 302 | + ) | |
| 303 | + .col( | |
| 304 | + ColumnDef::new(JwtBlacklist::CreatedAt) | |
| 305 | + .timestamp_with_time_zone() | |
| 306 | + .not_null(), | |
| 307 | + ) | |
| 308 | + .foreign_key( | |
| 309 | + ForeignKey::create() | |
| 310 | + .name("fk_jwt_blacklist_user_id") | |
| 311 | + .from(JwtBlacklist::Table, JwtBlacklist::UserId) | |
| 312 | + .to(Users::Table, Users::Id) | |
| 313 | + .on_delete(ForeignKeyAction::Cascade), | |
| 314 | + ) | |
| 315 | + .to_owned(), | |
| 316 | + ) | |
| 317 | + .await?; | |
| 318 | + | |
| 319 | + manager | |
| 320 | + .create_index( | |
| 321 | + Index::create() | |
| 322 | + .if_not_exists() | |
| 323 | + .name("idx_jwt_blacklist_token_hash") | |
| 324 | + .table(JwtBlacklist::Table) | |
| 325 | + .col(JwtBlacklist::TokenHash) | |
| 326 | + .unique() | |
| 327 | + .to_owned(), | |
| 328 | + ) | |
| 329 | + .await?; | |
| 330 | + | |
| 331 | + manager | |
| 332 | + .create_index( | |
| 333 | + Index::create() | |
| 334 | + .if_not_exists() | |
| 335 | + .name("idx_jwt_blacklist_expires_at") | |
| 336 | + .table(JwtBlacklist::Table) | |
| 337 | + .col(JwtBlacklist::ExpiresAt) | |
| 338 | + .to_owned(), | |
| 339 | + ) | |
| 340 | + .await?; | |
| 341 | + | |
| 342 | + manager | |
| 343 | + .create_index( | |
| 344 | + Index::create() | |
| 345 | + .if_not_exists() | |
| 346 | + .name("idx_jwt_blacklist_user_id") | |
| 347 | + .table(JwtBlacklist::Table) | |
| 348 | + .col(JwtBlacklist::UserId) | |
| 349 | + .to_owned(), | |
| 350 | + ) | |
| 351 | + .await?; | |
| 352 | + | |
| 353 | + // --- 6. user_preferences --- | |
| 354 | + manager | |
| 355 | + .create_table( | |
| 356 | + Table::create() | |
| 357 | + .table(UserPreferences::Table) | |
| 358 | + .if_not_exists() | |
| 359 | + .col( | |
| 360 | + ColumnDef::new(UserPreferences::Id) | |
| 361 | + .uuid() | |
| 362 | + .not_null() | |
| 363 | + .primary_key() | |
| 364 | + .default(Expr::cust("gen_random_uuid()")), | |
| 365 | + ) | |
| 366 | + .col( | |
| 367 | + ColumnDef::new(UserPreferences::UserId) | |
| 368 | + .uuid() | |
| 369 | + .not_null() | |
| 370 | + .unique_key(), | |
| 371 | + ) | |
| 372 | + .col( | |
| 373 | + ColumnDef::new(UserPreferences::Theme) | |
| 374 | + .string() | |
| 375 | + .string_len(10) | |
| 376 | + .not_null() | |
| 377 | + .default("light"), | |
| 378 | + ) | |
| 379 | + .col( | |
| 380 | + ColumnDef::new(UserPreferences::Language) | |
| 381 | + .string() | |
| 382 | + .string_len(5) | |
| 383 | + .not_null() | |
| 384 | + .default("fr"), | |
| 385 | + ) | |
| 386 | + .col( | |
| 387 | + ColumnDef::new(UserPreferences::CreatedAt) | |
| 388 | + .timestamp_with_time_zone() | |
| 389 | + .not_null() | |
| 390 | + .default(Expr::cust("NOW()")), | |
| 391 | + ) | |
| 392 | + .col( | |
| 393 | + ColumnDef::new(UserPreferences::UpdatedAt) | |
| 394 | + .timestamp_with_time_zone() | |
| 395 | + .not_null() | |
| 396 | + .default(Expr::cust("NOW()")), | |
| 397 | + ) | |
| 398 | + .foreign_key( | |
| 399 | + ForeignKey::create() | |
| 400 | + .name("fk_user_preferences_user_id") | |
| 401 | + .from(UserPreferences::Table, UserPreferences::UserId) | |
| 402 | + .to(Users::Table, Users::Id) | |
| 403 | + .on_delete(ForeignKeyAction::Cascade), | |
| 404 | + ) | |
| 405 | + .to_owned(), | |
| 406 | + ) | |
| 407 | + .await?; | |
| 408 | + | |
| 409 | + manager | |
| 410 | + .get_connection() | |
| 411 | + .execute_unprepared( | |
| 412 | + "ALTER TABLE user_preferences ADD CONSTRAINT check_theme CHECK (theme IN ('light', 'dark', 'auto'))", | |
| 413 | + ) | |
| 414 | + .await?; | |
| 415 | + | |
| 416 | + manager | |
| 417 | + .get_connection() | |
| 418 | + .execute_unprepared( | |
| 419 | + "ALTER TABLE user_preferences ADD CONSTRAINT check_language CHECK (language IN ('fr', 'en'))", | |
| 420 | + ) | |
| 421 | + .await?; | |
| 422 | + | |
| 423 | + manager | |
| 424 | + .create_index( | |
| 425 | + Index::create() | |
| 426 | + .if_not_exists() | |
| 427 | + .name("idx_user_preferences_user_id") | |
| 428 | + .table(UserPreferences::Table) | |
| 429 | + .col(UserPreferences::UserId) | |
| 430 | + .to_owned(), | |
| 431 | + ) | |
| 432 | + .await?; | |
| 433 | + | |
| 434 | + // --- 7. password_reset_tokens --- | |
| 435 | + manager | |
| 436 | + .create_table( | |
| 437 | + Table::create() | |
| 438 | + .table(PasswordResetTokens::Table) | |
| 439 | + .if_not_exists() | |
| 440 | + .col( | |
| 441 | + ColumnDef::new(PasswordResetTokens::Id) | |
| 442 | + .uuid() | |
| 443 | + .not_null() | |
| 444 | + .primary_key(), | |
| 445 | + ) | |
| 446 | + .col( | |
| 447 | + ColumnDef::new(PasswordResetTokens::UserId) | |
| 448 | + .uuid() | |
| 449 | + .not_null(), | |
| 450 | + ) | |
| 451 | + .col( | |
| 452 | + ColumnDef::new(PasswordResetTokens::TokenHash) | |
| 453 | + .string_len(64) | |
| 454 | + .not_null() | |
| 455 | + .unique_key(), | |
| 456 | + ) | |
| 457 | + .col( | |
| 458 | + ColumnDef::new(PasswordResetTokens::ExpiresAt) | |
| 459 | + .timestamp_with_time_zone() | |
| 460 | + .not_null(), | |
| 461 | + ) | |
| 462 | + .col( | |
| 463 | + ColumnDef::new(PasswordResetTokens::UsedAt) | |
| 464 | + .timestamp_with_time_zone() | |
| 465 | + .null(), | |
| 466 | + ) | |
| 467 | + .col( | |
| 468 | + ColumnDef::new(PasswordResetTokens::CreatedAt) | |
| 469 | + .timestamp_with_time_zone() | |
| 470 | + .not_null(), | |
| 471 | + ) | |
| 472 | + .foreign_key( | |
| 473 | + ForeignKey::create() | |
| 474 | + .name("fk_password_reset_tokens_user_id") | |
| 475 | + .from(PasswordResetTokens::Table, PasswordResetTokens::UserId) | |
| 476 | + .to(Users::Table, Users::Id) | |
| 477 | + .on_delete(ForeignKeyAction::Cascade), | |
| 478 | + ) | |
| 479 | + .to_owned(), | |
| 480 | + ) | |
| 481 | + .await?; | |
| 482 | + | |
| 483 | + manager | |
| 484 | + .create_index( | |
| 485 | + Index::create() | |
| 486 | + .if_not_exists() | |
| 487 | + .name("idx_password_reset_tokens_user_id") | |
| 488 | + .table(PasswordResetTokens::Table) | |
| 489 | + .col(PasswordResetTokens::UserId) | |
| 490 | + .to_owned(), | |
| 491 | + ) | |
| 492 | + .await?; | |
| 493 | + | |
| 494 | + manager | |
| 495 | + .create_index( | |
| 496 | + Index::create() | |
| 497 | + .if_not_exists() | |
| 498 | + .name("idx_password_reset_tokens_token_hash") | |
| 499 | + .table(PasswordResetTokens::Table) | |
| 500 | + .col(PasswordResetTokens::TokenHash) | |
| 501 | + .unique() | |
| 502 | + .to_owned(), | |
| 503 | + ) | |
| 504 | + .await?; | |
| 505 | + | |
| 506 | + manager | |
| 507 | + .create_index( | |
| 508 | + Index::create() | |
| 509 | + .if_not_exists() | |
| 510 | + .name("idx_password_reset_tokens_expires_at") | |
| 511 | + .table(PasswordResetTokens::Table) | |
| 512 | + .col(PasswordResetTokens::ExpiresAt) | |
| 513 | + .to_owned(), | |
| 514 | + ) | |
| 515 | + .await?; | |
| 516 | + | |
| 517 | + // --- 8. email_validation_tokens --- | |
| 518 | + manager | |
| 519 | + .create_table( | |
| 520 | + Table::create() | |
| 521 | + .table(EmailValidationTokens::Table) | |
| 522 | + .if_not_exists() | |
| 523 | + .col( | |
| 524 | + ColumnDef::new(EmailValidationTokens::Id) | |
| 525 | + .uuid() | |
| 526 | + .not_null() | |
| 527 | + .primary_key(), | |
| 528 | + ) | |
| 529 | + .col( | |
| 530 | + ColumnDef::new(EmailValidationTokens::UserId) | |
| 531 | + .uuid() | |
| 532 | + .not_null(), | |
| 533 | + ) | |
| 534 | + .col( | |
| 535 | + ColumnDef::new(EmailValidationTokens::TokenHash) | |
| 536 | + .string_len(64) | |
| 537 | + .not_null() | |
| 538 | + .unique_key(), | |
| 539 | + ) | |
| 540 | + .col( | |
| 541 | + ColumnDef::new(EmailValidationTokens::ExpiresAt) | |
| 542 | + .timestamp_with_time_zone() | |
| 543 | + .not_null(), | |
| 544 | + ) | |
| 545 | + .col( | |
| 546 | + ColumnDef::new(EmailValidationTokens::UsedAt) | |
| 547 | + .timestamp_with_time_zone() | |
| 548 | + .null(), | |
| 549 | + ) | |
| 550 | + .col( | |
| 551 | + ColumnDef::new(EmailValidationTokens::CreatedAt) | |
| 552 | + .timestamp_with_time_zone() | |
| 553 | + .not_null(), | |
| 554 | + ) | |
| 555 | + .foreign_key( | |
| 556 | + ForeignKey::create() | |
| 557 | + .name("fk_email_validation_tokens_user_id") | |
| 558 | + .from(EmailValidationTokens::Table, EmailValidationTokens::UserId) | |
| 559 | + .to(Users::Table, Users::Id) | |
| 560 | + .on_delete(ForeignKeyAction::Cascade), | |
| 561 | + ) | |
| 562 | + .to_owned(), | |
| 563 | + ) | |
| 564 | + .await?; | |
| 565 | + | |
| 566 | + manager | |
| 567 | + .create_index( | |
| 568 | + Index::create() | |
| 569 | + .if_not_exists() | |
| 570 | + .name("idx_email_validation_tokens_user_id") | |
| 571 | + .table(EmailValidationTokens::Table) | |
| 572 | + .col(EmailValidationTokens::UserId) | |
| 573 | + .to_owned(), | |
| 574 | + ) | |
| 575 | + .await?; | |
| 576 | + | |
| 577 | + manager | |
| 578 | + .create_index( | |
| 579 | + Index::create() | |
| 580 | + .if_not_exists() | |
| 581 | + .name("idx_email_validation_tokens_token_hash") | |
| 582 | + .table(EmailValidationTokens::Table) | |
| 583 | + .col(EmailValidationTokens::TokenHash) | |
| 584 | + .unique() | |
| 585 | + .to_owned(), | |
| 586 | + ) | |
| 587 | + .await?; | |
| 588 | + | |
| 589 | + manager | |
| 590 | + .create_index( | |
| 591 | + Index::create() | |
| 592 | + .if_not_exists() | |
| 593 | + .name("idx_email_validation_tokens_expires_at") | |
| 594 | + .table(EmailValidationTokens::Table) | |
| 595 | + .col(EmailValidationTokens::ExpiresAt) | |
| 596 | + .to_owned(), | |
| 597 | + ) | |
| 598 | + .await?; | |
| 599 | + | |
| 600 | + // --- 9. password_change_requests --- | |
| 601 | + manager | |
| 602 | + .create_table( | |
| 603 | + Table::create() | |
| 604 | + .table(PasswordChangeRequests::Table) | |
| 605 | + .if_not_exists() | |
| 606 | + .col( | |
| 607 | + ColumnDef::new(PasswordChangeRequests::Id) | |
| 608 | + .uuid() | |
| 609 | + .not_null() | |
| 610 | + .primary_key(), | |
| 611 | + ) | |
| 612 | + .col( | |
| 613 | + ColumnDef::new(PasswordChangeRequests::UserId) | |
| 614 | + .uuid() | |
| 615 | + .not_null(), | |
| 616 | + ) | |
| 617 | + .col( | |
| 618 | + ColumnDef::new(PasswordChangeRequests::NewPasswordHash) | |
| 619 | + .text() | |
| 620 | + .not_null(), | |
| 621 | + ) | |
| 622 | + .col( | |
| 623 | + ColumnDef::new(PasswordChangeRequests::ConfirmationToken) | |
| 624 | + .string_len(64) | |
| 625 | + .not_null() | |
| 626 | + .unique_key(), | |
| 627 | + ) | |
| 628 | + .col( | |
| 629 | + ColumnDef::new(PasswordChangeRequests::ExpiresAt) | |
| 630 | + .timestamp_with_time_zone() | |
| 631 | + .not_null(), | |
| 632 | + ) | |
| 633 | + .col( | |
| 634 | + ColumnDef::new(PasswordChangeRequests::CreatedAt) | |
| 635 | + .timestamp_with_time_zone() | |
| 636 | + .not_null(), | |
| 637 | + ) | |
| 638 | + .col( | |
| 639 | + ColumnDef::new(PasswordChangeRequests::ConfirmedAt) | |
| 640 | + .timestamp_with_time_zone() | |
| 641 | + .null(), | |
| 642 | + ) | |
| 643 | + .col( | |
| 644 | + ColumnDef::new(PasswordChangeRequests::EmailSentAt) | |
| 645 | + .timestamp_with_time_zone() | |
| 646 | + .null(), | |
| 647 | + ) | |
| 648 | + .col( | |
| 649 | + ColumnDef::new(PasswordChangeRequests::RetryCount) | |
| 650 | + .integer() | |
| 651 | + .not_null() | |
| 652 | + .default(0), | |
| 653 | + ) | |
| 654 | + .foreign_key( | |
| 655 | + ForeignKey::create() | |
| 656 | + .name("fk_password_change_requests_user_id") | |
| 657 | + .from( | |
| 658 | + PasswordChangeRequests::Table, | |
| 659 | + PasswordChangeRequests::UserId, | |
| 660 | + ) | |
| 661 | + .to(Users::Table, Users::Id) | |
| 662 | + .on_delete(ForeignKeyAction::Cascade), | |
| 663 | + ) | |
| 664 | + .to_owned(), | |
| 665 | + ) | |
| 666 | + .await?; | |
| 667 | + | |
| 668 | + manager | |
| 669 | + .create_index( | |
| 670 | + Index::create() | |
| 671 | + .if_not_exists() | |
| 672 | + .name("idx_password_change_requests_user_id") | |
| 673 | + .table(PasswordChangeRequests::Table) | |
| 674 | + .col(PasswordChangeRequests::UserId) | |
| 675 | + .to_owned(), | |
| 676 | + ) | |
| 677 | + .await?; | |
| 678 | + | |
| 679 | + manager | |
| 680 | + .create_index( | |
| 681 | + Index::create() | |
| 682 | + .if_not_exists() | |
| 683 | + .name("idx_password_change_requests_confirmation_token") | |
| 684 | + .table(PasswordChangeRequests::Table) | |
| 685 | + .col(PasswordChangeRequests::ConfirmationToken) | |
| 686 | + .unique() | |
| 687 | + .to_owned(), | |
| 688 | + ) | |
| 689 | + .await?; | |
| 690 | + | |
| 691 | + manager | |
| 692 | + .create_index( | |
| 693 | + Index::create() | |
| 694 | + .if_not_exists() | |
| 695 | + .name("idx_password_change_requests_expires_at") | |
| 696 | + .table(PasswordChangeRequests::Table) | |
| 697 | + .col(PasswordChangeRequests::ExpiresAt) | |
| 698 | + .to_owned(), | |
| 699 | + ) | |
| 700 | + .await?; | |
| 701 | + | |
| 702 | + manager | |
| 703 | + .get_connection() | |
| 704 | + .execute_unprepared( | |
| 705 | + r#" | |
| 706 | + CREATE UNIQUE INDEX IF NOT EXISTS idx_password_change_requests_user_id_pending | |
| 707 | + ON password_change_requests (user_id) | |
| 708 | + WHERE confirmed_at IS NULL | |
| 709 | + "#, | |
| 710 | + ) | |
| 711 | + .await?; | |
| 712 | + | |
| 713 | + // --- 10. email_queue --- | |
| 714 | + manager | |
| 715 | + .create_table( | |
| 716 | + Table::create() | |
| 717 | + .table(EmailQueue::Table) | |
| 718 | + .if_not_exists() | |
| 719 | + .col( | |
| 720 | + ColumnDef::new(EmailQueue::Id) | |
| 721 | + .uuid() | |
| 722 | + .not_null() | |
| 723 | + .primary_key(), | |
| 724 | + ) | |
| 725 | + .col( | |
| 726 | + ColumnDef::new(EmailQueue::ToEmail) | |
| 727 | + .string_len(255) | |
| 728 | + .not_null(), | |
| 729 | + ) | |
| 730 | + .col(ColumnDef::new(EmailQueue::ToName).string_len(255).null()) | |
| 731 | + .col( | |
| 732 | + ColumnDef::new(EmailQueue::FromEmail) | |
| 733 | + .string_len(255) | |
| 734 | + .not_null(), | |
| 735 | + ) | |
| 736 | + .col(ColumnDef::new(EmailQueue::FromName).string_len(255).null()) | |
| 737 | + .col( | |
| 738 | + ColumnDef::new(EmailQueue::Subject) | |
| 739 | + .string_len(500) | |
| 740 | + .not_null(), | |
| 741 | + ) | |
| 742 | + .col(ColumnDef::new(EmailQueue::BodyHtml).text().not_null()) | |
| 743 | + .col(ColumnDef::new(EmailQueue::BodyText).text().not_null()) | |
| 744 | + .col( | |
| 745 | + ColumnDef::new(EmailQueue::EmailType) | |
| 746 | + .string_len(50) | |
| 747 | + .not_null(), | |
| 748 | + ) | |
| 749 | + .col( | |
| 750 | + ColumnDef::new(EmailQueue::Status) | |
| 751 | + .string_len(20) | |
| 752 | + .not_null() | |
| 753 | + .default("pending"), | |
| 754 | + ) | |
| 755 | + .col( | |
| 756 | + ColumnDef::new(EmailQueue::Attempts) | |
| 757 | + .integer() | |
| 758 | + .not_null() | |
| 759 | + .default(0), | |
| 760 | + ) | |
| 761 | + .col( | |
| 762 | + ColumnDef::new(EmailQueue::LastAttemptAt) | |
| 763 | + .timestamp_with_time_zone() | |
| 764 | + .null(), | |
| 765 | + ) | |
| 766 | + .col( | |
| 767 | + ColumnDef::new(EmailQueue::SentAt) | |
| 768 | + .timestamp_with_time_zone() | |
| 769 | + .null(), | |
| 770 | + ) | |
| 771 | + .col(ColumnDef::new(EmailQueue::ErrorMessage).text().null()) | |
| 772 | + .col(ColumnDef::new(EmailQueue::SmtpCode).string_len(10).null()) | |
| 773 | + .col(ColumnDef::new(EmailQueue::SmtpResponse).text().null()) | |
| 774 | + .col( | |
| 775 | + ColumnDef::new(EmailQueue::CreatedAt) | |
| 776 | + .timestamp_with_time_zone() | |
| 777 | + .not_null(), | |
| 778 | + ) | |
| 779 | + .col( | |
| 780 | + ColumnDef::new(EmailQueue::UpdatedAt) | |
| 781 | + .timestamp_with_time_zone() | |
| 782 | + .not_null(), | |
| 783 | + ) | |
| 784 | + .to_owned(), | |
| 785 | + ) | |
| 786 | + .await?; | |
| 787 | + | |
| 788 | + manager | |
| 789 | + .create_index( | |
| 790 | + Index::create() | |
| 791 | + .if_not_exists() | |
| 792 | + .name("idx_email_queue_status") | |
| 793 | + .table(EmailQueue::Table) | |
| 794 | + .col(EmailQueue::Status) | |
| 795 | + .to_owned(), | |
| 796 | + ) | |
| 797 | + .await?; | |
| 798 | + | |
| 799 | + manager | |
| 800 | + .create_index( | |
| 801 | + Index::create() | |
| 802 | + .if_not_exists() | |
| 803 | + .name("idx_email_queue_created_at") | |
| 804 | + .table(EmailQueue::Table) | |
| 805 | + .col(EmailQueue::CreatedAt) | |
| 806 | + .to_owned(), | |
| 807 | + ) | |
| 808 | + .await?; | |
| 809 | + | |
| 810 | + manager | |
| 811 | + .create_index( | |
| 812 | + Index::create() | |
| 813 | + .if_not_exists() | |
| 814 | + .name("idx_email_queue_email_type") | |
| 815 | + .table(EmailQueue::Table) | |
| 816 | + .col(EmailQueue::EmailType) | |
| 817 | + .to_owned(), | |
| 818 | + ) | |
| 819 | + .await?; | |
| 820 | + | |
| 821 | + manager | |
| 822 | + .get_connection() | |
| 823 | + .execute_unprepared( | |
| 824 | + r#" | |
| 825 | + ALTER TABLE email_queue | |
| 826 | + ADD CONSTRAINT chk_email_queue_email_type | |
| 827 | + CHECK (email_type IN ( | |
| 828 | + 'password_reset', | |
| 829 | + 'email_validation', | |
| 830 | + 'password_changed', | |
| 831 | + 'account_modified', | |
| 832 | + 'welcome', | |
| 833 | + 'admin_new_user', | |
| 834 | + 'notification' | |
| 835 | + )) | |
| 836 | + "#, | |
| 837 | + ) | |
| 838 | + .await?; | |
| 839 | + | |
| 840 | + manager | |
| 841 | + .get_connection() | |
| 842 | + .execute_unprepared( | |
| 843 | + r#" | |
| 844 | + ALTER TABLE email_queue | |
| 845 | + ADD CONSTRAINT chk_email_queue_status | |
| 846 | + CHECK (status IN ('pending', 'sent', 'failed')) | |
| 847 | + "#, | |
| 848 | + ) | |
| 849 | + .await?; | |
| 850 | + | |
| 851 | + // --- 11. email_delivery_status --- | |
| 852 | + manager | |
| 853 | + .create_table( | |
| 854 | + Table::create() | |
| 855 | + .table(EmailDeliveryStatus::Table) | |
| 856 | + .if_not_exists() | |
| 857 | + .col( | |
| 858 | + ColumnDef::new(EmailDeliveryStatus::Id) | |
| 859 | + .uuid() | |
| 860 | + .not_null() | |
| 861 | + .primary_key(), | |
| 862 | + ) | |
| 863 | + .col( | |
| 864 | + ColumnDef::new(EmailDeliveryStatus::EmailType) | |
| 865 | + .string_len(50) | |
| 866 | + .not_null(), | |
| 867 | + ) | |
| 868 | + .col( | |
| 869 | + ColumnDef::new(EmailDeliveryStatus::Recipient) | |
| 870 | + .string_len(255) | |
| 871 | + .not_null(), | |
| 872 | + ) | |
| 873 | + .col( | |
| 874 | + ColumnDef::new(EmailDeliveryStatus::Template) | |
| 875 | + .string_len(255) | |
| 876 | + .not_null(), | |
| 877 | + ) | |
| 878 | + .col( | |
| 879 | + ColumnDef::new(EmailDeliveryStatus::State) | |
| 880 | + .string_len(20) | |
| 881 | + .not_null() | |
| 882 | + .default("queued"), | |
| 883 | + ) | |
| 884 | + .col( | |
| 885 | + ColumnDef::new(EmailDeliveryStatus::SmtpCode) | |
| 886 | + .string_len(10) | |
| 887 | + .null(), | |
| 888 | + ) | |
| 889 | + .col( | |
| 890 | + ColumnDef::new(EmailDeliveryStatus::SmtpResponse) | |
| 891 | + .text() | |
| 892 | + .null(), | |
| 893 | + ) | |
| 894 | + .col( | |
| 895 | + ColumnDef::new(EmailDeliveryStatus::AttemptCount) | |
| 896 | + .integer() | |
| 897 | + .not_null() | |
| 898 | + .default(0), | |
| 899 | + ) | |
| 900 | + .col( | |
| 901 | + ColumnDef::new(EmailDeliveryStatus::LastAttemptAt) | |
| 902 | + .timestamp_with_time_zone() | |
| 903 | + .null(), | |
| 904 | + ) | |
| 905 | + .col( | |
| 906 | + ColumnDef::new(EmailDeliveryStatus::LastStateAt) | |
| 907 | + .timestamp_with_time_zone() | |
| 908 | + .not_null(), | |
| 909 | + ) | |
| 910 | + .col( | |
| 911 | + ColumnDef::new(EmailDeliveryStatus::CorrelationId) | |
| 912 | + .uuid() | |
| 913 | + .null(), | |
| 914 | + ) | |
| 915 | + .col( | |
| 916 | + ColumnDef::new(EmailDeliveryStatus::CreatedAt) | |
| 917 | + .timestamp_with_time_zone() | |
| 918 | + .not_null(), | |
| 919 | + ) | |
| 920 | + .col( | |
| 921 | + ColumnDef::new(EmailDeliveryStatus::UpdatedAt) | |
| 922 | + .timestamp_with_time_zone() | |
| 923 | + .not_null(), | |
| 924 | + ) | |
| 925 | + .to_owned(), | |
| 926 | + ) | |
| 927 | + .await?; | |
| 928 | + | |
| 929 | + manager | |
| 930 | + .create_index( | |
| 931 | + Index::create() | |
| 932 | + .if_not_exists() | |
| 933 | + .name("idx_email_delivery_status_type") | |
| 934 | + .table(EmailDeliveryStatus::Table) | |
| 935 | + .col(EmailDeliveryStatus::EmailType) | |
| 936 | + .to_owned(), | |
| 937 | + ) | |
| 938 | + .await?; | |
| 939 | + | |
| 940 | + manager | |
| 941 | + .create_index( | |
| 942 | + Index::create() | |
| 943 | + .if_not_exists() | |
| 944 | + .name("idx_email_delivery_status_state") | |
| 945 | + .table(EmailDeliveryStatus::Table) | |
| 946 | + .col(EmailDeliveryStatus::State) | |
| 947 | + .to_owned(), | |
| 948 | + ) | |
| 949 | + .await?; | |
| 950 | + | |
| 951 | + manager | |
| 952 | + .create_index( | |
| 953 | + Index::create() | |
| 954 | + .if_not_exists() | |
| 955 | + .name("idx_email_delivery_status_recipient") | |
| 956 | + .table(EmailDeliveryStatus::Table) | |
| 957 | + .col(EmailDeliveryStatus::Recipient) | |
| 958 | + .to_owned(), | |
| 959 | + ) | |
| 960 | + .await?; | |
| 961 | + | |
| 962 | + manager | |
| 963 | + .create_index( | |
| 964 | + Index::create() | |
| 965 | + .if_not_exists() | |
| 966 | + .name("idx_email_delivery_status_last_state_at") | |
| 967 | + .table(EmailDeliveryStatus::Table) | |
| 968 | + .col(EmailDeliveryStatus::LastStateAt) | |
| 969 | + .to_owned(), | |
| 970 | + ) | |
| 971 | + .await?; | |
| 972 | + | |
| 973 | + // --- Section 12: audit_logs table --- | |
| 974 | + manager | |
| 975 | + .create_table( | |
| 976 | + Table::create() | |
| 977 | + .table(AuditLogs::Table) | |
| 978 | + .if_not_exists() | |
| 979 | + .col( | |
| 980 | + ColumnDef::new(AuditLogs::Id) | |
| 981 | + .uuid() | |
| 982 | + .not_null() | |
| 983 | + .primary_key(), | |
| 984 | + ) | |
| 985 | + .col(ColumnDef::new(AuditLogs::UserId).uuid()) | |
| 986 | + .col(ColumnDef::new(AuditLogs::Action).string_len(100).not_null()) | |
| 987 | + .col(ColumnDef::new(AuditLogs::TargetUserId).uuid()) | |
| 988 | + .col( | |
| 989 | + ColumnDef::new(AuditLogs::Level) | |
| 990 | + .string_len(10) | |
| 991 | + .not_null() | |
| 992 | + .default("INFO"), | |
| 993 | + ) | |
| 994 | + .col(ColumnDef::new(AuditLogs::Message).text().not_null()) | |
| 995 | + .col( | |
| 996 | + ColumnDef::new(AuditLogs::CreatedAt) | |
| 997 | + .timestamp_with_time_zone() | |
| 998 | + .not_null() | |
| 999 | + .default(Expr::current_timestamp()), | |
| 1000 | + ) | |
| 1001 | + .foreign_key( | |
| 1002 | + ForeignKey::create() | |
| 1003 | + .name("fk_audit_logs_user") | |
| 1004 | + .from(AuditLogs::Table, AuditLogs::UserId) | |
| 1005 | + .to(Users::Table, Users::Id) | |
| 1006 | + .on_delete(ForeignKeyAction::SetNull), | |
| 1007 | + ) | |
| 1008 | + .foreign_key( | |
| 1009 | + ForeignKey::create() | |
| 1010 | + .name("fk_audit_logs_target_user") | |
| 1011 | + .from(AuditLogs::Table, AuditLogs::TargetUserId) | |
| 1012 | + .to(Users::Table, Users::Id) | |
| 1013 | + .on_delete(ForeignKeyAction::SetNull), | |
| 1014 | + ) | |
| 1015 | + .to_owned(), | |
| 1016 | + ) | |
| 1017 | + .await?; | |
| 1018 | + | |
| 1019 | + manager | |
| 1020 | + .create_index( | |
| 1021 | + Index::create() | |
| 1022 | + .if_not_exists() | |
| 1023 | + .name("idx_audit_logs_user_id") | |
| 1024 | + .table(AuditLogs::Table) | |
| 1025 | + .col(AuditLogs::UserId) | |
| 1026 | + .to_owned(), | |
| 1027 | + ) | |
| 1028 | + .await?; | |
| 1029 | + | |
| 1030 | + manager | |
| 1031 | + .create_index( | |
| 1032 | + Index::create() | |
| 1033 | + .if_not_exists() | |
| 1034 | + .name("idx_audit_logs_action") | |
| 1035 | + .table(AuditLogs::Table) | |
| 1036 | + .col(AuditLogs::Action) | |
| 1037 | + .to_owned(), | |
| 1038 | + ) | |
| 1039 | + .await?; | |
| 1040 | + | |
| 1041 | + manager | |
| 1042 | + .create_index( | |
| 1043 | + Index::create() | |
| 1044 | + .if_not_exists() | |
| 1045 | + .name("idx_audit_logs_created_at") | |
| 1046 | + .table(AuditLogs::Table) | |
| 1047 | + .col(AuditLogs::CreatedAt) | |
| 1048 | + .to_owned(), | |
| 1049 | + ) | |
| 1050 | + .await?; | |
| 1051 | + | |
| 1052 | + // --- Seed default roles --- | |
| 1053 | + manager | |
| 1054 | + .get_connection() | |
| 1055 | + .execute_unprepared( | |
| 1056 | + r#" | |
| 1057 | + INSERT INTO roles (id, name, description, created_at, updated_at) | |
| 1058 | + VALUES | |
| 1059 | + (gen_random_uuid(), 'admin', 'Administrator role', NOW(), NOW()), | |
| 1060 | + (gen_random_uuid(), 'user', 'Standard user role', NOW(), NOW()) | |
| 1061 | + ON CONFLICT (name) DO NOTHING | |
| 1062 | + "#, | |
| 1063 | + ) | |
| 1064 | + .await?; | |
| 1065 | + | |
| 1066 | + Ok(()) | |
| 1067 | + } | |
| 1068 | + | |
| 1069 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 1070 | + manager | |
| 1071 | + .drop_table(Table::drop().table(AuditLogs::Table).to_owned()) | |
| 1072 | + .await?; | |
| 1073 | + | |
| 1074 | + manager | |
| 1075 | + .drop_table(Table::drop().table(EmailDeliveryStatus::Table).to_owned()) | |
| 1076 | + .await?; | |
| 1077 | + | |
| 1078 | + manager | |
| 1079 | + .drop_table(Table::drop().table(EmailQueue::Table).to_owned()) | |
| 1080 | + .await?; | |
| 1081 | + | |
| 1082 | + manager | |
| 1083 | + .drop_table( | |
| 1084 | + Table::drop() | |
| 1085 | + .table(PasswordChangeRequests::Table) | |
| 1086 | + .to_owned(), | |
| 1087 | + ) | |
| 1088 | + .await?; | |
| 1089 | + | |
| 1090 | + manager | |
| 1091 | + .drop_table(Table::drop().table(EmailValidationTokens::Table).to_owned()) | |
| 1092 | + .await?; | |
| 1093 | + | |
| 1094 | + manager | |
| 1095 | + .drop_table(Table::drop().table(PasswordResetTokens::Table).to_owned()) | |
| 1096 | + .await?; | |
| 1097 | + | |
| 1098 | + manager | |
| 1099 | + .drop_table(Table::drop().table(UserPreferences::Table).to_owned()) | |
| 1100 | + .await?; | |
| 1101 | + | |
| 1102 | + manager | |
| 1103 | + .drop_table(Table::drop().table(JwtBlacklist::Table).to_owned()) | |
| 1104 | + .await?; | |
| 1105 | + | |
| 1106 | + manager | |
| 1107 | + .drop_table(Table::drop().table(RefreshToken::Table).to_owned()) | |
| 1108 | + .await?; | |
| 1109 | + | |
| 1110 | + manager | |
| 1111 | + .drop_table(Table::drop().table(UserRoles::Table).to_owned()) | |
| 1112 | + .await?; | |
| 1113 | + | |
| 1114 | + manager | |
| 1115 | + .drop_table(Table::drop().table(Roles::Table).to_owned()) | |
| 1116 | + .await?; | |
| 1117 | + | |
| 1118 | + manager | |
| 1119 | + .drop_table(Table::drop().table(Users::Table).to_owned()) | |
| 1120 | + .await | |
| 1121 | + } | |
| 1122 | +} | |
| 1123 | + | |
| 1124 | +#[derive(DeriveIden)] | |
| 1125 | +enum Users { | |
| 1126 | + Table, | |
| 1127 | + Id, | |
| 1128 | + Username, | |
| 1129 | + Email, | |
| 1130 | + PasswordHash, | |
| 1131 | + MustChangePassword, | |
| 1132 | + EmailVerified, | |
| 1133 | + EmailVerifiedAt, | |
| 1134 | + LastLoginAt, | |
| 1135 | + LastLoginIp, | |
| 1136 | + PasswordChangedAt, | |
| 1137 | + CreatedAt, | |
| 1138 | + UpdatedAt, | |
| 1139 | +} | |
| 1140 | + | |
| 1141 | +#[derive(DeriveIden)] | |
| 1142 | +enum Roles { | |
| 1143 | + Table, | |
| 1144 | + Id, | |
| 1145 | + Name, | |
| 1146 | + Description, | |
| 1147 | + CreatedAt, | |
| 1148 | + UpdatedAt, | |
| 1149 | +} | |
| 1150 | + | |
| 1151 | +#[derive(DeriveIden)] | |
| 1152 | +enum UserRoles { | |
| 1153 | + Table, | |
| 1154 | + UserId, | |
| 1155 | + RoleId, | |
| 1156 | + CreatedAt, | |
| 1157 | +} | |
| 1158 | + | |
| 1159 | +#[derive(DeriveIden)] | |
| 1160 | +enum RefreshToken { | |
| 1161 | + Table, | |
| 1162 | + Id, | |
| 1163 | + UserId, | |
| 1164 | + Token, | |
| 1165 | + ExpiresAt, | |
| 1166 | + CreatedAt, | |
| 1167 | + Revoked, | |
| 1168 | + RevokedAt, | |
| 1169 | + ReplacedBy, | |
| 1170 | +} | |
| 1171 | + | |
| 1172 | +#[derive(DeriveIden)] | |
| 1173 | +enum JwtBlacklist { | |
| 1174 | + Table, | |
| 1175 | + Id, | |
| 1176 | + TokenHash, | |
| 1177 | + UserId, | |
| 1178 | + ExpiresAt, | |
| 1179 | + RevokedAt, | |
| 1180 | + CreatedAt, | |
| 1181 | +} | |
| 1182 | + | |
| 1183 | +#[derive(DeriveIden)] | |
| 1184 | +enum UserPreferences { | |
| 1185 | + Table, | |
| 1186 | + Id, | |
| 1187 | + UserId, | |
| 1188 | + Theme, | |
| 1189 | + Language, | |
| 1190 | + CreatedAt, | |
| 1191 | + UpdatedAt, | |
| 1192 | +} | |
| 1193 | + | |
| 1194 | +#[derive(DeriveIden)] | |
| 1195 | +enum PasswordResetTokens { | |
| 1196 | + Table, | |
| 1197 | + Id, | |
| 1198 | + UserId, | |
| 1199 | + TokenHash, | |
| 1200 | + ExpiresAt, | |
| 1201 | + UsedAt, | |
| 1202 | + CreatedAt, | |
| 1203 | +} | |
| 1204 | + | |
| 1205 | +#[derive(DeriveIden)] | |
| 1206 | +enum EmailValidationTokens { | |
| 1207 | + Table, | |
| 1208 | + Id, | |
| 1209 | + UserId, | |
| 1210 | + TokenHash, | |
| 1211 | + ExpiresAt, | |
| 1212 | + UsedAt, | |
| 1213 | + CreatedAt, | |
| 1214 | +} | |
| 1215 | + | |
| 1216 | +#[derive(DeriveIden)] | |
| 1217 | +enum PasswordChangeRequests { | |
| 1218 | + Table, | |
| 1219 | + Id, | |
| 1220 | + UserId, | |
| 1221 | + NewPasswordHash, | |
| 1222 | + ConfirmationToken, | |
| 1223 | + ExpiresAt, | |
| 1224 | + CreatedAt, | |
| 1225 | + ConfirmedAt, | |
| 1226 | + EmailSentAt, | |
| 1227 | + RetryCount, | |
| 1228 | +} | |
| 1229 | + | |
| 1230 | +#[derive(DeriveIden)] | |
| 1231 | +enum EmailQueue { | |
| 1232 | + Table, | |
| 1233 | + Id, | |
| 1234 | + ToEmail, | |
| 1235 | + ToName, | |
| 1236 | + FromEmail, | |
| 1237 | + FromName, | |
| 1238 | + Subject, | |
| 1239 | + BodyHtml, | |
| 1240 | + BodyText, | |
| 1241 | + EmailType, | |
| 1242 | + Status, | |
| 1243 | + Attempts, | |
| 1244 | + LastAttemptAt, | |
| 1245 | + SentAt, | |
| 1246 | + ErrorMessage, | |
| 1247 | + SmtpCode, | |
| 1248 | + SmtpResponse, | |
| 1249 | + CreatedAt, | |
| 1250 | + UpdatedAt, | |
| 1251 | +} | |
| 1252 | + | |
| 1253 | +#[derive(DeriveIden)] | |
| 1254 | +enum EmailDeliveryStatus { | |
| 1255 | + Table, | |
| 1256 | + Id, | |
| 1257 | + EmailType, | |
| 1258 | + Recipient, | |
| 1259 | + Template, | |
| 1260 | + State, | |
| 1261 | + SmtpCode, | |
| 1262 | + SmtpResponse, | |
| 1263 | + AttemptCount, | |
| 1264 | + LastAttemptAt, | |
| 1265 | + LastStateAt, | |
| 1266 | + CorrelationId, | |
| 1267 | + CreatedAt, | |
| 1268 | + UpdatedAt, | |
| 1269 | +} | |
| 1270 | + | |
| 1271 | +#[derive(DeriveIden)] | |
| 1272 | +enum AuditLogs { | |
| 1273 | + Table, | |
| 1274 | + Id, | |
| 1275 | + UserId, | |
| 1276 | + Action, | |
| 1277 | + TargetUserId, | |
| 1278 | + Level, | |
| 1279 | + Message, | |
| 1280 | + CreatedAt, | |
| 1281 | +} |
A
crates/rustwarden-core/src/migrations/m20260306_000002_create_app_settings_table.rs
+83
-0
@@ -0,0 +1,83 @@
| 1 | +use sea_orm_migration::prelude::*; | |
| 2 | + | |
| 3 | +/// Migration: creates the `app_settings` key-value table. | |
| 4 | +#[derive(DeriveMigrationName)] | |
| 5 | +pub struct Migration; | |
| 6 | + | |
| 7 | +#[async_trait::async_trait] | |
| 8 | +impl MigrationTrait for Migration { | |
| 9 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | + manager | |
| 11 | + .create_table( | |
| 12 | + Table::create() | |
| 13 | + .table(AppSettings::Table) | |
| 14 | + .if_not_exists() | |
| 15 | + .col( | |
| 16 | + ColumnDef::new(AppSettings::Id) | |
| 17 | + .uuid() | |
| 18 | + .not_null() | |
| 19 | + .primary_key(), | |
| 20 | + ) | |
| 21 | + .col( | |
| 22 | + ColumnDef::new(AppSettings::Key) | |
| 23 | + .string() | |
| 24 | + .string_len(100) | |
| 25 | + .not_null() | |
| 26 | + .unique_key(), | |
| 27 | + ) | |
| 28 | + .col(ColumnDef::new(AppSettings::Value).text().not_null()) | |
| 29 | + .col(ColumnDef::new(AppSettings::Description).text().null()) | |
| 30 | + .col(ColumnDef::new(AppSettings::UpdatedBy).uuid().null()) | |
| 31 | + .col( | |
| 32 | + ColumnDef::new(AppSettings::UpdatedAt) | |
| 33 | + .timestamp_with_time_zone() | |
| 34 | + .not_null(), | |
| 35 | + ) | |
| 36 | + .foreign_key( | |
| 37 | + ForeignKey::create() | |
| 38 | + .name("fk_app_settings_updated_by") | |
| 39 | + .from(AppSettings::Table, AppSettings::UpdatedBy) | |
| 40 | + .to(Users::Table, Users::Id) | |
| 41 | + .on_delete(ForeignKeyAction::SetNull), | |
| 42 | + ) | |
| 43 | + .to_owned(), | |
| 44 | + ) | |
| 45 | + .await?; | |
| 46 | + | |
| 47 | + manager | |
| 48 | + .create_index( | |
| 49 | + Index::create() | |
| 50 | + .if_not_exists() | |
| 51 | + .name("idx_app_settings_key") | |
| 52 | + .table(AppSettings::Table) | |
| 53 | + .col(AppSettings::Key) | |
| 54 | + .to_owned(), | |
| 55 | + ) | |
| 56 | + .await?; | |
| 57 | + | |
| 58 | + Ok(()) | |
| 59 | + } | |
| 60 | + | |
| 61 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 62 | + manager | |
| 63 | + .drop_table(Table::drop().table(AppSettings::Table).to_owned()) | |
| 64 | + .await | |
| 65 | + } | |
| 66 | +} | |
| 67 | + | |
| 68 | +#[derive(DeriveIden)] | |
| 69 | +enum AppSettings { | |
| 70 | + Table, | |
| 71 | + Id, | |
| 72 | + Key, | |
| 73 | + Value, | |
| 74 | + Description, | |
| 75 | + UpdatedBy, | |
| 76 | + UpdatedAt, | |
| 77 | +} | |
| 78 | + | |
| 79 | +#[derive(DeriveIden)] | |
| 80 | +enum Users { | |
| 81 | + Table, | |
| 82 | + Id, | |
| 83 | +} |
A
crates/rustwarden-core/src/migrations/m20260309_000003_create_permissions_tables.rs
+176
-0
@@ -0,0 +1,176 @@
| 1 | +use sea_orm_migration::prelude::*; | |
| 2 | + | |
| 3 | +/// Migration: creates the `permissions`, `roles`, and `role_permissions` tables. | |
| 4 | +#[derive(DeriveMigrationName)] | |
| 5 | +pub struct Migration; | |
| 6 | + | |
| 7 | +#[async_trait::async_trait] | |
| 8 | +impl MigrationTrait for Migration { | |
| 9 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | + // --- 1. permissions table --- | |
| 11 | + manager | |
| 12 | + .create_table( | |
| 13 | + Table::create() | |
| 14 | + .table(Permissions::Table) | |
| 15 | + .if_not_exists() | |
| 16 | + .col( | |
| 17 | + ColumnDef::new(Permissions::Id) | |
| 18 | + .uuid() | |
| 19 | + .not_null() | |
| 20 | + .primary_key(), | |
| 21 | + ) | |
| 22 | + .col( | |
| 23 | + ColumnDef::new(Permissions::Name) | |
| 24 | + .string() | |
| 25 | + .string_len(100) | |
| 26 | + .not_null() | |
| 27 | + .unique_key(), | |
| 28 | + ) | |
| 29 | + .col(ColumnDef::new(Permissions::Description).text().null()) | |
| 30 | + .col( | |
| 31 | + ColumnDef::new(Permissions::Category) | |
| 32 | + .string() | |
| 33 | + .string_len(50) | |
| 34 | + .not_null(), | |
| 35 | + ) | |
| 36 | + .col( | |
| 37 | + ColumnDef::new(Permissions::CreatedAt) | |
| 38 | + .timestamp_with_time_zone() | |
| 39 | + .not_null(), | |
| 40 | + ) | |
| 41 | + .to_owned(), | |
| 42 | + ) | |
| 43 | + .await?; | |
| 44 | + | |
| 45 | + // --- 2. role_permissions junction table --- | |
| 46 | + manager | |
| 47 | + .create_table( | |
| 48 | + Table::create() | |
| 49 | + .table(RolePermissions::Table) | |
| 50 | + .if_not_exists() | |
| 51 | + .col(ColumnDef::new(RolePermissions::RoleId).uuid().not_null()) | |
| 52 | + .col( | |
| 53 | + ColumnDef::new(RolePermissions::PermissionId) | |
| 54 | + .uuid() | |
| 55 | + .not_null(), | |
| 56 | + ) | |
| 57 | + .primary_key( | |
| 58 | + Index::create() | |
| 59 | + .col(RolePermissions::RoleId) | |
| 60 | + .col(RolePermissions::PermissionId), | |
| 61 | + ) | |
| 62 | + .foreign_key( | |
| 63 | + ForeignKey::create() | |
| 64 | + .name("fk_role_permissions_role_id") | |
| 65 | + .from(RolePermissions::Table, RolePermissions::RoleId) | |
| 66 | + .to(Roles::Table, Roles::Id) | |
| 67 | + .on_delete(ForeignKeyAction::Cascade), | |
| 68 | + ) | |
| 69 | + .foreign_key( | |
| 70 | + ForeignKey::create() | |
| 71 | + .name("fk_role_permissions_permission_id") | |
| 72 | + .from(RolePermissions::Table, RolePermissions::PermissionId) | |
| 73 | + .to(Permissions::Table, Permissions::Id) | |
| 74 | + .on_delete(ForeignKeyAction::Cascade), | |
| 75 | + ) | |
| 76 | + .to_owned(), | |
| 77 | + ) | |
| 78 | + .await?; | |
| 79 | + | |
| 80 | + // --- 3. Indexes --- | |
| 81 | + manager | |
| 82 | + .create_index( | |
| 83 | + Index::create() | |
| 84 | + .if_not_exists() | |
| 85 | + .name("idx_permissions_category") | |
| 86 | + .table(Permissions::Table) | |
| 87 | + .col(Permissions::Category) | |
| 88 | + .to_owned(), | |
| 89 | + ) | |
| 90 | + .await?; | |
| 91 | + | |
| 92 | + manager | |
| 93 | + .create_index( | |
| 94 | + Index::create() | |
| 95 | + .if_not_exists() | |
| 96 | + .name("idx_role_permissions_role_id") | |
| 97 | + .table(RolePermissions::Table) | |
| 98 | + .col(RolePermissions::RoleId) | |
| 99 | + .to_owned(), | |
| 100 | + ) | |
| 101 | + .await?; | |
| 102 | + | |
| 103 | + // --- 4. Seed default permissions --- | |
| 104 | + manager | |
| 105 | + .get_connection() | |
| 106 | + .execute_unprepared( | |
| 107 | + r#" | |
| 108 | + INSERT INTO permissions (id, name, description, category, created_at) VALUES | |
| 109 | + (gen_random_uuid(), 'users.list', 'View the list of users', 'users', NOW()), | |
| 110 | + (gen_random_uuid(), 'users.create', 'Create new users', 'users', NOW()), | |
| 111 | + (gen_random_uuid(), 'users.edit', 'Edit existing users', 'users', NOW()), | |
| 112 | + (gen_random_uuid(), 'users.delete', 'Delete users', 'users', NOW()), | |
| 113 | + (gen_random_uuid(), 'users.reset_password', 'Reset user passwords', 'users', NOW()), | |
| 114 | + (gen_random_uuid(), 'roles.list', 'View the list of roles', 'roles', NOW()), | |
| 115 | + (gen_random_uuid(), 'roles.create', 'Create new roles', 'roles', NOW()), | |
| 116 | + (gen_random_uuid(), 'roles.edit', 'Edit existing roles', 'roles', NOW()), | |
| 117 | + (gen_random_uuid(), 'roles.delete', 'Delete roles', 'roles', NOW()), | |
| 118 | + (gen_random_uuid(), 'settings.view', 'View application settings', 'settings', NOW()), | |
| 119 | + (gen_random_uuid(), 'settings.edit', 'Edit application settings', 'settings', NOW()), | |
| 120 | + (gen_random_uuid(), 'audit.view', 'View audit logs', 'audit', NOW()) | |
| 121 | + ON CONFLICT (name) DO NOTHING | |
| 122 | + "#, | |
| 123 | + ) | |
| 124 | + .await?; | |
| 125 | + | |
| 126 | + // --- 5. Assign all permissions to the admin role --- | |
| 127 | + manager | |
| 128 | + .get_connection() | |
| 129 | + .execute_unprepared( | |
| 130 | + r#" | |
| 131 | + INSERT INTO role_permissions (role_id, permission_id) | |
| 132 | + SELECT r.id, p.id | |
| 133 | + FROM roles r | |
| 134 | + CROSS JOIN permissions p | |
| 135 | + WHERE r.name = 'admin' | |
| 136 | + ON CONFLICT DO NOTHING | |
| 137 | + "#, | |
| 138 | + ) | |
| 139 | + .await?; | |
| 140 | + | |
| 141 | + Ok(()) | |
| 142 | + } | |
| 143 | + | |
| 144 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 145 | + manager | |
| 146 | + .drop_table(Table::drop().table(RolePermissions::Table).to_owned()) | |
| 147 | + .await?; | |
| 148 | + manager | |
| 149 | + .drop_table(Table::drop().table(Permissions::Table).to_owned()) | |
| 150 | + .await?; | |
| 151 | + Ok(()) | |
| 152 | + } | |
| 153 | +} | |
| 154 | + | |
| 155 | +#[derive(DeriveIden)] | |
| 156 | +enum Permissions { | |
| 157 | + Table, | |
| 158 | + Id, | |
| 159 | + Name, | |
| 160 | + Description, | |
| 161 | + Category, | |
| 162 | + CreatedAt, | |
| 163 | +} | |
| 164 | + | |
| 165 | +#[derive(DeriveIden)] | |
| 166 | +enum RolePermissions { | |
| 167 | + Table, | |
| 168 | + RoleId, | |
| 169 | + PermissionId, | |
| 170 | +} | |
| 171 | + | |
| 172 | +#[derive(DeriveIden)] | |
| 173 | +enum Roles { | |
| 174 | + Table, | |
| 175 | + Id, | |
| 176 | +} |
A
crates/rustwarden-core/src/migrations/m20260309_000004_create_resources_tables.rs
+212
-0
@@ -0,0 +1,212 @@
| 1 | +use sea_orm_migration::prelude::*; | |
| 2 | + | |
| 3 | +/// Migration: creates the `resources` and `resource_shares` tables. | |
| 4 | +#[derive(DeriveMigrationName)] | |
| 5 | +pub struct Migration; | |
| 6 | + | |
| 7 | +#[async_trait::async_trait] | |
| 8 | +impl MigrationTrait for Migration { | |
| 9 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | + // --- 1. resources table --- | |
| 11 | + manager | |
| 12 | + .create_table( | |
| 13 | + Table::create() | |
| 14 | + .table(Resources::Table) | |
| 15 | + .if_not_exists() | |
| 16 | + .col( | |
| 17 | + ColumnDef::new(Resources::Id) | |
| 18 | + .uuid() | |
| 19 | + .not_null() | |
| 20 | + .primary_key(), | |
| 21 | + ) | |
| 22 | + .col( | |
| 23 | + ColumnDef::new(Resources::ResourceType) | |
| 24 | + .string() | |
| 25 | + .string_len(100) | |
| 26 | + .not_null(), | |
| 27 | + ) | |
| 28 | + .col(ColumnDef::new(Resources::ResourceId).uuid().not_null()) | |
| 29 | + .col(ColumnDef::new(Resources::OwnerId).uuid().not_null()) | |
| 30 | + .col( | |
| 31 | + ColumnDef::new(Resources::IsPublic) | |
| 32 | + .boolean() | |
| 33 | + .not_null() | |
| 34 | + .default(false), | |
| 35 | + ) | |
| 36 | + .col( | |
| 37 | + ColumnDef::new(Resources::CreatedAt) | |
| 38 | + .timestamp_with_time_zone() | |
| 39 | + .not_null(), | |
| 40 | + ) | |
| 41 | + .foreign_key( | |
| 42 | + ForeignKey::create() | |
| 43 | + .name("fk_resources_owner_id") | |
| 44 | + .from(Resources::Table, Resources::OwnerId) | |
| 45 | + .to(Users::Table, Users::Id) | |
| 46 | + .on_delete(ForeignKeyAction::Cascade), | |
| 47 | + ) | |
| 48 | + .to_owned(), | |
| 49 | + ) | |
| 50 | + .await?; | |
| 51 | + | |
| 52 | + // Unique constraint: one registry entry per (type, id) | |
| 53 | + manager | |
| 54 | + .create_index( | |
| 55 | + Index::create() | |
| 56 | + .if_not_exists() | |
| 57 | + .name("idx_resources_type_id_unique") | |
| 58 | + .table(Resources::Table) | |
| 59 | + .col(Resources::ResourceType) | |
| 60 | + .col(Resources::ResourceId) | |
| 61 | + .unique() | |
| 62 | + .to_owned(), | |
| 63 | + ) | |
| 64 | + .await?; | |
| 65 | + | |
| 66 | + manager | |
| 67 | + .create_index( | |
| 68 | + Index::create() | |
| 69 | + .if_not_exists() | |
| 70 | + .name("idx_resources_owner_id") | |
| 71 | + .table(Resources::Table) | |
| 72 | + .col(Resources::OwnerId) | |
| 73 | + .to_owned(), | |
| 74 | + ) | |
| 75 | + .await?; | |
| 76 | + | |
| 77 | + manager | |
| 78 | + .create_index( | |
| 79 | + Index::create() | |
| 80 | + .if_not_exists() | |
| 81 | + .name("idx_resources_type") | |
| 82 | + .table(Resources::Table) | |
| 83 | + .col(Resources::ResourceType) | |
| 84 | + .to_owned(), | |
| 85 | + ) | |
| 86 | + .await?; | |
| 87 | + | |
| 88 | + // --- 2. resource_shares table --- | |
| 89 | + manager | |
| 90 | + .create_table( | |
| 91 | + Table::create() | |
| 92 | + .table(ResourceShares::Table) | |
| 93 | + .if_not_exists() | |
| 94 | + .col( | |
| 95 | + ColumnDef::new(ResourceShares::Id) | |
| 96 | + .uuid() | |
| 97 | + .not_null() | |
| 98 | + .primary_key(), | |
| 99 | + ) | |
| 100 | + .col(ColumnDef::new(ResourceShares::ResourceId).uuid().not_null()) | |
| 101 | + .col( | |
| 102 | + ColumnDef::new(ResourceShares::SharedWithUserId) | |
| 103 | + .uuid() | |
| 104 | + .not_null(), | |
| 105 | + ) | |
| 106 | + .col( | |
| 107 | + ColumnDef::new(ResourceShares::PermissionLevel) | |
| 108 | + .string() | |
| 109 | + .string_len(20) | |
| 110 | + .not_null(), | |
| 111 | + ) | |
| 112 | + .col( | |
| 113 | + ColumnDef::new(ResourceShares::SharedByUserId) | |
| 114 | + .uuid() | |
| 115 | + .not_null(), | |
| 116 | + ) | |
| 117 | + .col( | |
| 118 | + ColumnDef::new(ResourceShares::CreatedAt) | |
| 119 | + .timestamp_with_time_zone() | |
| 120 | + .not_null(), | |
| 121 | + ) | |
| 122 | + .foreign_key( | |
| 123 | + ForeignKey::create() | |
| 124 | + .name("fk_resource_shares_resource_id") | |
| 125 | + .from(ResourceShares::Table, ResourceShares::ResourceId) | |
| 126 | + .to(Resources::Table, Resources::Id) | |
| 127 | + .on_delete(ForeignKeyAction::Cascade), | |
| 128 | + ) | |
| 129 | + .foreign_key( | |
| 130 | + ForeignKey::create() | |
| 131 | + .name("fk_resource_shares_shared_with") | |
| 132 | + .from(ResourceShares::Table, ResourceShares::SharedWithUserId) | |
| 133 | + .to(Users::Table, Users::Id) | |
| 134 | + .on_delete(ForeignKeyAction::Cascade), | |
| 135 | + ) | |
| 136 | + .foreign_key( | |
| 137 | + ForeignKey::create() | |
| 138 | + .name("fk_resource_shares_shared_by") | |
| 139 | + .from(ResourceShares::Table, ResourceShares::SharedByUserId) | |
| 140 | + .to(Users::Table, Users::Id) | |
| 141 | + .on_delete(ForeignKeyAction::Cascade), | |
| 142 | + ) | |
| 143 | + .to_owned(), | |
| 144 | + ) | |
| 145 | + .await?; | |
| 146 | + | |
| 147 | + // One share per (resource, user) pair | |
| 148 | + manager | |
| 149 | + .create_index( | |
| 150 | + Index::create() | |
| 151 | + .if_not_exists() | |
| 152 | + .name("idx_resource_shares_resource_user_unique") | |
| 153 | + .table(ResourceShares::Table) | |
| 154 | + .col(ResourceShares::ResourceId) | |
| 155 | + .col(ResourceShares::SharedWithUserId) | |
| 156 | + .unique() | |
| 157 | + .to_owned(), | |
| 158 | + ) | |
| 159 | + .await?; | |
| 160 | + | |
| 161 | + manager | |
| 162 | + .create_index( | |
| 163 | + Index::create() | |
| 164 | + .if_not_exists() | |
| 165 | + .name("idx_resource_shares_shared_with") | |
| 166 | + .table(ResourceShares::Table) | |
| 167 | + .col(ResourceShares::SharedWithUserId) | |
| 168 | + .to_owned(), | |
| 169 | + ) | |
| 170 | + .await?; | |
| 171 | + | |
| 172 | + Ok(()) | |
| 173 | + } | |
| 174 | + | |
| 175 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 176 | + manager | |
| 177 | + .drop_table(Table::drop().table(ResourceShares::Table).to_owned()) | |
| 178 | + .await?; | |
| 179 | + manager | |
| 180 | + .drop_table(Table::drop().table(Resources::Table).to_owned()) | |
| 181 | + .await?; | |
| 182 | + Ok(()) | |
| 183 | + } | |
| 184 | +} | |
| 185 | + | |
| 186 | +#[derive(DeriveIden)] | |
| 187 | +enum Resources { | |
| 188 | + Table, | |
| 189 | + Id, | |
| 190 | + ResourceType, | |
| 191 | + ResourceId, | |
| 192 | + OwnerId, | |
| 193 | + IsPublic, | |
| 194 | + CreatedAt, | |
| 195 | +} | |
| 196 | + | |
| 197 | +#[derive(DeriveIden)] | |
| 198 | +enum ResourceShares { | |
| 199 | + Table, | |
| 200 | + Id, | |
| 201 | + ResourceId, | |
| 202 | + SharedWithUserId, | |
| 203 | + PermissionLevel, | |
| 204 | + SharedByUserId, | |
| 205 | + CreatedAt, | |
| 206 | +} | |
| 207 | + | |
| 208 | +#[derive(DeriveIden)] | |
| 209 | +enum Users { | |
| 210 | + Table, | |
| 211 | + Id, | |
| 212 | +} |
A
crates/rustwarden-core/src/migrations/m20260310_000005_create_oauth_accounts_table.rs
+126
-0
@@ -0,0 +1,126 @@
| 1 | +use sea_orm_migration::prelude::*; | |
| 2 | + | |
| 3 | +/// Migration: creates the `oauth_accounts` table for linked OAuth identities. | |
| 4 | +#[derive(DeriveMigrationName)] | |
| 5 | +pub struct Migration; | |
| 6 | + | |
| 7 | +#[async_trait::async_trait] | |
| 8 | +impl MigrationTrait for Migration { | |
| 9 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | + manager | |
| 11 | + .create_table( | |
| 12 | + Table::create() | |
| 13 | + .table(OauthAccounts::Table) | |
| 14 | + .if_not_exists() | |
| 15 | + .col( | |
| 16 | + ColumnDef::new(OauthAccounts::Id) | |
| 17 | + .uuid() | |
| 18 | + .not_null() | |
| 19 | + .primary_key(), | |
| 20 | + ) | |
| 21 | + .col(ColumnDef::new(OauthAccounts::UserId).uuid().not_null()) | |
| 22 | + .col( | |
| 23 | + ColumnDef::new(OauthAccounts::Provider) | |
| 24 | + .string() | |
| 25 | + .string_len(50) | |
| 26 | + .not_null(), | |
| 27 | + ) | |
| 28 | + .col( | |
| 29 | + ColumnDef::new(OauthAccounts::ProviderUserId) | |
| 30 | + .string() | |
| 31 | + .string_len(255) | |
| 32 | + .not_null(), | |
| 33 | + ) | |
| 34 | + .col( | |
| 35 | + ColumnDef::new(OauthAccounts::Email) | |
| 36 | + .string() | |
| 37 | + .string_len(255) | |
| 38 | + .null(), | |
| 39 | + ) | |
| 40 | + .col( | |
| 41 | + ColumnDef::new(OauthAccounts::DisplayName) | |
| 42 | + .string() | |
| 43 | + .string_len(255) | |
| 44 | + .null(), | |
| 45 | + ) | |
| 46 | + .col( | |
| 47 | + ColumnDef::new(OauthAccounts::AvatarUrl) | |
| 48 | + .string() | |
| 49 | + .string_len(1024) | |
| 50 | + .null(), | |
| 51 | + ) | |
| 52 | + .col( | |
| 53 | + ColumnDef::new(OauthAccounts::CreatedAt) | |
| 54 | + .timestamp_with_time_zone() | |
| 55 | + .not_null() | |
| 56 | + .default(Expr::current_timestamp()), | |
| 57 | + ) | |
| 58 | + .col( | |
| 59 | + ColumnDef::new(OauthAccounts::UpdatedAt) | |
| 60 | + .timestamp_with_time_zone() | |
| 61 | + .not_null() | |
| 62 | + .default(Expr::current_timestamp()), | |
| 63 | + ) | |
| 64 | + .foreign_key( | |
| 65 | + ForeignKey::create() | |
| 66 | + .name("fk_oauth_accounts_user_id") | |
| 67 | + .from(OauthAccounts::Table, OauthAccounts::UserId) | |
| 68 | + .to(Users::Table, Users::Id) | |
| 69 | + .on_delete(ForeignKeyAction::Cascade), | |
| 70 | + ) | |
| 71 | + .to_owned(), | |
| 72 | + ) | |
| 73 | + .await?; | |
| 74 | + | |
| 75 | + // Unique constraint: one provider account per user per provider | |
| 76 | + manager | |
| 77 | + .create_index( | |
| 78 | + Index::create() | |
| 79 | + .name("idx_oauth_accounts_provider_user") | |
| 80 | + .table(OauthAccounts::Table) | |
| 81 | + .col(OauthAccounts::Provider) | |
| 82 | + .col(OauthAccounts::ProviderUserId) | |
| 83 | + .unique() | |
| 84 | + .to_owned(), | |
| 85 | + ) | |
| 86 | + .await?; | |
| 87 | + | |
| 88 | + manager | |
| 89 | + .create_index( | |
| 90 | + Index::create() | |
| 91 | + .name("idx_oauth_accounts_user_id") | |
| 92 | + .table(OauthAccounts::Table) | |
| 93 | + .col(OauthAccounts::UserId) | |
| 94 | + .to_owned(), | |
| 95 | + ) | |
| 96 | + .await?; | |
| 97 | + | |
| 98 | + Ok(()) | |
| 99 | + } | |
| 100 | + | |
| 101 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 102 | + manager | |
| 103 | + .drop_table(Table::drop().table(OauthAccounts::Table).to_owned()) | |
| 104 | + .await | |
| 105 | + } | |
| 106 | +} | |
| 107 | + | |
| 108 | +#[derive(Iden)] | |
| 109 | +enum OauthAccounts { | |
| 110 | + Table, | |
| 111 | + Id, | |
| 112 | + UserId, | |
| 113 | + Provider, | |
| 114 | + ProviderUserId, | |
| 115 | + Email, | |
| 116 | + DisplayName, | |
| 117 | + AvatarUrl, | |
| 118 | + CreatedAt, | |
| 119 | + UpdatedAt, | |
| 120 | +} | |
| 121 | + | |
| 122 | +#[derive(Iden)] | |
| 123 | +enum Users { | |
| 124 | + Table, | |
| 125 | + Id, | |
| 126 | +} |
A
crates/rustwarden-core/src/migrations/m20260409_000006_create_totp_tables.rs
+184
-0
@@ -0,0 +1,184 @@
| 1 | +use sea_orm_migration::prelude::*; | |
| 2 | + | |
| 3 | +/// Migration: creates the TOTP secrets and backup-codes tables for 2FA. | |
| 4 | +#[derive(DeriveMigrationName)] | |
| 5 | +pub struct Migration; | |
| 6 | + | |
| 7 | +#[async_trait::async_trait] | |
| 8 | +impl MigrationTrait for Migration { | |
| 9 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | + // Table: user_totp (TOTP 2FA data per user) | |
| 11 | + manager | |
| 12 | + .create_table( | |
| 13 | + Table::create() | |
| 14 | + .table(UserTotp::Table) | |
| 15 | + .if_not_exists() | |
| 16 | + .col(ColumnDef::new(UserTotp::Id).uuid().not_null().primary_key()) | |
| 17 | + .col( | |
| 18 | + ColumnDef::new(UserTotp::UserId) | |
| 19 | + .uuid() | |
| 20 | + .not_null() | |
| 21 | + .unique_key(), | |
| 22 | + ) | |
| 23 | + .col(ColumnDef::new(UserTotp::SecretEncrypted).text().not_null()) | |
| 24 | + .col( | |
| 25 | + ColumnDef::new(UserTotp::Enabled) | |
| 26 | + .boolean() | |
| 27 | + .not_null() | |
| 28 | + .default(false), | |
| 29 | + ) | |
| 30 | + .col(ColumnDef::new(UserTotp::BackupCodesEncrypted).text().null()) | |
| 31 | + .col( | |
| 32 | + ColumnDef::new(UserTotp::BackupCodesRemaining) | |
| 33 | + .integer() | |
| 34 | + .not_null() | |
| 35 | + .default(0), | |
| 36 | + ) | |
| 37 | + .col( | |
| 38 | + ColumnDef::new(UserTotp::EnabledAt) | |
| 39 | + .timestamp_with_time_zone() | |
| 40 | + .null(), | |
| 41 | + ) | |
| 42 | + .col( | |
| 43 | + ColumnDef::new(UserTotp::CreatedAt) | |
| 44 | + .timestamp_with_time_zone() | |
| 45 | + .not_null() | |
| 46 | + .default(Expr::current_timestamp()), | |
| 47 | + ) | |
| 48 | + .col( | |
| 49 | + ColumnDef::new(UserTotp::UpdatedAt) | |
| 50 | + .timestamp_with_time_zone() | |
| 51 | + .not_null() | |
| 52 | + .default(Expr::current_timestamp()), | |
| 53 | + ) | |
| 54 | + .foreign_key( | |
| 55 | + ForeignKey::create() | |
| 56 | + .name("fk_user_totp_user_id") | |
| 57 | + .from(UserTotp::Table, UserTotp::UserId) | |
| 58 | + .to(Users::Table, Users::Id) | |
| 59 | + .on_delete(ForeignKeyAction::Cascade), | |
| 60 | + ) | |
| 61 | + .to_owned(), | |
| 62 | + ) | |
| 63 | + .await?; | |
| 64 | + | |
| 65 | + // Table: totp_challenges (ephemeral login 2FA challenges) | |
| 66 | + manager | |
| 67 | + .create_table( | |
| 68 | + Table::create() | |
| 69 | + .table(TotpChallenges::Table) | |
| 70 | + .if_not_exists() | |
| 71 | + .col( | |
| 72 | + ColumnDef::new(TotpChallenges::Id) | |
| 73 | + .uuid() | |
| 74 | + .not_null() | |
| 75 | + .primary_key(), | |
| 76 | + ) | |
| 77 | + .col(ColumnDef::new(TotpChallenges::UserId).uuid().not_null()) | |
| 78 | + .col( | |
| 79 | + ColumnDef::new(TotpChallenges::ChallengeToken) | |
| 80 | + .string() | |
| 81 | + .string_len(128) | |
| 82 | + .not_null() | |
| 83 | + .unique_key(), | |
| 84 | + ) | |
| 85 | + .col( | |
| 86 | + ColumnDef::new(TotpChallenges::Attempts) | |
| 87 | + .integer() | |
| 88 | + .not_null() | |
| 89 | + .default(0), | |
| 90 | + ) | |
| 91 | + .col( | |
| 92 | + ColumnDef::new(TotpChallenges::MaxAttempts) | |
| 93 | + .integer() | |
| 94 | + .not_null() | |
| 95 | + .default(5), | |
| 96 | + ) | |
| 97 | + .col( | |
| 98 | + ColumnDef::new(TotpChallenges::ExpiresAt) | |
| 99 | + .timestamp_with_time_zone() | |
| 100 | + .not_null(), | |
| 101 | + ) | |
| 102 | + .col( | |
| 103 | + ColumnDef::new(TotpChallenges::CreatedAt) | |
| 104 | + .timestamp_with_time_zone() | |
| 105 | + .not_null() | |
| 106 | + .default(Expr::current_timestamp()), | |
| 107 | + ) | |
| 108 | + .foreign_key( | |
| 109 | + ForeignKey::create() | |
| 110 | + .name("fk_totp_challenges_user_id") | |
| 111 | + .from(TotpChallenges::Table, TotpChallenges::UserId) | |
| 112 | + .to(Users::Table, Users::Id) | |
| 113 | + .on_delete(ForeignKeyAction::Cascade), | |
| 114 | + ) | |
| 115 | + .to_owned(), | |
| 116 | + ) | |
| 117 | + .await?; | |
| 118 | + | |
| 119 | + // Index on challenge_token for fast lookup | |
| 120 | + manager | |
| 121 | + .create_index( | |
| 122 | + Index::create() | |
| 123 | + .name("idx_totp_challenges_token") | |
| 124 | + .table(TotpChallenges::Table) | |
| 125 | + .col(TotpChallenges::ChallengeToken) | |
| 126 | + .to_owned(), | |
| 127 | + ) | |
| 128 | + .await?; | |
| 129 | + | |
| 130 | + // Index on expires_at for cleanup queries | |
| 131 | + manager | |
| 132 | + .create_index( | |
| 133 | + Index::create() | |
| 134 | + .name("idx_totp_challenges_expires") | |
| 135 | + .table(TotpChallenges::Table) | |
| 136 | + .col(TotpChallenges::ExpiresAt) | |
| 137 | + .to_owned(), | |
| 138 | + ) | |
| 139 | + .await?; | |
| 140 | + | |
| 141 | + Ok(()) | |
| 142 | + } | |
| 143 | + | |
| 144 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 145 | + manager | |
| 146 | + .drop_table(Table::drop().table(TotpChallenges::Table).to_owned()) | |
| 147 | + .await?; | |
| 148 | + manager | |
| 149 | + .drop_table(Table::drop().table(UserTotp::Table).to_owned()) | |
| 150 | + .await | |
| 151 | + } | |
| 152 | +} | |
| 153 | + | |
| 154 | +#[derive(Iden)] | |
| 155 | +enum UserTotp { | |
| 156 | + Table, | |
| 157 | + Id, | |
| 158 | + UserId, | |
| 159 | + SecretEncrypted, | |
| 160 | + Enabled, | |
| 161 | + BackupCodesEncrypted, | |
| 162 | + BackupCodesRemaining, | |
| 163 | + EnabledAt, | |
| 164 | + CreatedAt, | |
| 165 | + UpdatedAt, | |
| 166 | +} | |
| 167 | + | |
| 168 | +#[derive(Iden)] | |
| 169 | +enum TotpChallenges { | |
| 170 | + Table, | |
| 171 | + Id, | |
| 172 | + UserId, | |
| 173 | + ChallengeToken, | |
| 174 | + Attempts, | |
| 175 | + MaxAttempts, | |
| 176 | + ExpiresAt, | |
| 177 | + CreatedAt, | |
| 178 | +} | |
| 179 | + | |
| 180 | +#[derive(Iden)] | |
| 181 | +enum Users { | |
| 182 | + Table, | |
| 183 | + Id, | |
| 184 | +} |
A
crates/rustwarden-core/src/migrations/m20260422_000007_create_personal_access_tokens.rs
+147
-0
@@ -0,0 +1,147 @@
| 1 | +use sea_orm_migration::prelude::*; | |
| 2 | + | |
| 3 | +/// Migration: creates the `personal_access_tokens` table. | |
| 4 | +#[derive(DeriveMigrationName)] | |
| 5 | +pub struct Migration; | |
| 6 | + | |
| 7 | +#[async_trait::async_trait] | |
| 8 | +impl MigrationTrait for Migration { | |
| 9 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | + // Table: personal_access_tokens (long-lived API tokens with scopes) | |
| 11 | + // | |
| 12 | + // Complements refresh_token: whereas refresh_token implements JWT | |
| 13 | + // rotation (one-use, detects reuse), PATs are static secrets consumed | |
| 14 | + // unchanged across calls (e.g. git push over HTTPS). Hash-only storage, | |
| 15 | + // nullable expiration, and per-user unique names. | |
| 16 | + manager | |
| 17 | + .create_table( | |
| 18 | + Table::create() | |
| 19 | + .table(PersonalAccessTokens::Table) | |
| 20 | + .if_not_exists() | |
| 21 | + .col( | |
| 22 | + ColumnDef::new(PersonalAccessTokens::Id) | |
| 23 | + .uuid() | |
| 24 | + .not_null() | |
| 25 | + .primary_key(), | |
| 26 | + ) | |
| 27 | + .col( | |
| 28 | + ColumnDef::new(PersonalAccessTokens::UserId) | |
| 29 | + .uuid() | |
| 30 | + .not_null(), | |
| 31 | + ) | |
| 32 | + .col( | |
| 33 | + ColumnDef::new(PersonalAccessTokens::Name) | |
| 34 | + .string() | |
| 35 | + .string_len(128) | |
| 36 | + .not_null(), | |
| 37 | + ) | |
| 38 | + .col( | |
| 39 | + ColumnDef::new(PersonalAccessTokens::TokenHash) | |
| 40 | + .string() | |
| 41 | + .string_len(64) // SHA-256 hex | |
| 42 | + .not_null() | |
| 43 | + .unique_key(), | |
| 44 | + ) | |
| 45 | + .col( | |
| 46 | + ColumnDef::new(PersonalAccessTokens::Scopes) | |
| 47 | + .array(sea_query::ColumnType::Text) | |
| 48 | + .not_null(), | |
| 49 | + ) | |
| 50 | + .col( | |
| 51 | + ColumnDef::new(PersonalAccessTokens::ExpiresAt) | |
| 52 | + .timestamp_with_time_zone() | |
| 53 | + .null(), | |
| 54 | + ) | |
| 55 | + .col( | |
| 56 | + ColumnDef::new(PersonalAccessTokens::LastUsedAt) | |
| 57 | + .timestamp_with_time_zone() | |
| 58 | + .null(), | |
| 59 | + ) | |
| 60 | + .col( | |
| 61 | + ColumnDef::new(PersonalAccessTokens::RevokedAt) | |
| 62 | + .timestamp_with_time_zone() | |
| 63 | + .null(), | |
| 64 | + ) | |
| 65 | + .col( | |
| 66 | + ColumnDef::new(PersonalAccessTokens::CreatedAt) | |
| 67 | + .timestamp_with_time_zone() | |
| 68 | + .not_null() | |
| 69 | + .default(Expr::current_timestamp()), | |
| 70 | + ) | |
| 71 | + .foreign_key( | |
| 72 | + ForeignKey::create() | |
| 73 | + .name("fk_personal_access_tokens_user_id") | |
| 74 | + .from(PersonalAccessTokens::Table, PersonalAccessTokens::UserId) | |
| 75 | + .to(Users::Table, Users::Id) | |
| 76 | + .on_delete(ForeignKeyAction::Cascade), | |
| 77 | + ) | |
| 78 | + .to_owned(), | |
| 79 | + ) | |
| 80 | + .await?; | |
| 81 | + | |
| 82 | + // One token name per user (UI uniqueness guarantee). | |
| 83 | + manager | |
| 84 | + .create_index( | |
| 85 | + Index::create() | |
| 86 | + .name("idx_pat_name_per_user") | |
| 87 | + .table(PersonalAccessTokens::Table) | |
| 88 | + .col(PersonalAccessTokens::UserId) | |
| 89 | + .col(PersonalAccessTokens::Name) | |
| 90 | + .unique() | |
| 91 | + .to_owned(), | |
| 92 | + ) | |
| 93 | + .await?; | |
| 94 | + | |
| 95 | + // Fast lookup for `verify(token)` — token_hash is already unique, but | |
| 96 | + // an explicit btree index documents intent and lets EXPLAIN stay stable | |
| 97 | + // across rewrites. | |
| 98 | + manager | |
| 99 | + .create_index( | |
| 100 | + Index::create() | |
| 101 | + .name("idx_pat_token_hash") | |
| 102 | + .table(PersonalAccessTokens::Table) | |
| 103 | + .col(PersonalAccessTokens::TokenHash) | |
| 104 | + .to_owned(), | |
| 105 | + ) | |
| 106 | + .await?; | |
| 107 | + | |
| 108 | + // Supports the cleanup job `WHERE expires_at < now()`. | |
| 109 | + manager | |
| 110 | + .create_index( | |
| 111 | + Index::create() | |
| 112 | + .name("idx_pat_expires_at") | |
| 113 | + .table(PersonalAccessTokens::Table) | |
| 114 | + .col(PersonalAccessTokens::ExpiresAt) | |
| 115 | + .to_owned(), | |
| 116 | + ) | |
| 117 | + .await?; | |
| 118 | + | |
| 119 | + Ok(()) | |
| 120 | + } | |
| 121 | + | |
| 122 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 123 | + manager | |
| 124 | + .drop_table(Table::drop().table(PersonalAccessTokens::Table).to_owned()) | |
| 125 | + .await | |
| 126 | + } | |
| 127 | +} | |
| 128 | + | |
| 129 | +#[derive(Iden)] | |
| 130 | +enum PersonalAccessTokens { | |
| 131 | + Table, | |
| 132 | + Id, | |
| 133 | + UserId, | |
| 134 | + Name, | |
| 135 | + TokenHash, | |
| 136 | + Scopes, | |
| 137 | + ExpiresAt, | |
| 138 | + LastUsedAt, | |
| 139 | + RevokedAt, | |
| 140 | + CreatedAt, | |
| 141 | +} | |
| 142 | + | |
| 143 | +#[derive(Iden)] | |
| 144 | +enum Users { | |
| 145 | + Table, | |
| 146 | + Id, | |
| 147 | +} |
A
crates/rustwarden-core/src/migrations/m20260423_000008_create_teams.rs
+277
-0
@@ -0,0 +1,277 @@
| 1 | +use sea_orm_migration::prelude::*; | |
| 2 | + | |
| 3 | +/// Migration: creates the `teams` and `team_members` tables. | |
| 4 | +#[derive(DeriveMigrationName)] | |
| 5 | +pub struct Migration; | |
| 6 | + | |
| 7 | +#[async_trait::async_trait] | |
| 8 | +impl MigrationTrait for Migration { | |
| 9 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 10 | + // ----------------------------------------------------------------- | |
| 11 | + // 1. teams — named groups of users with an owner | |
| 12 | + // ----------------------------------------------------------------- | |
| 13 | + manager | |
| 14 | + .create_table( | |
| 15 | + Table::create() | |
| 16 | + .table(Teams::Table) | |
| 17 | + .if_not_exists() | |
| 18 | + .col(ColumnDef::new(Teams::Id).uuid().not_null().primary_key()) | |
| 19 | + .col( | |
| 20 | + ColumnDef::new(Teams::Slug) | |
| 21 | + .string() | |
| 22 | + .string_len(64) | |
| 23 | + .not_null() | |
| 24 | + .unique_key(), | |
| 25 | + ) | |
| 26 | + .col( | |
| 27 | + ColumnDef::new(Teams::Name) | |
| 28 | + .string() | |
| 29 | + .string_len(128) | |
| 30 | + .not_null(), | |
| 31 | + ) | |
| 32 | + .col(ColumnDef::new(Teams::OwnerId).uuid().not_null()) | |
| 33 | + .col( | |
| 34 | + ColumnDef::new(Teams::CreatedAt) | |
| 35 | + .timestamp_with_time_zone() | |
| 36 | + .not_null() | |
| 37 | + .default(Expr::current_timestamp()), | |
| 38 | + ) | |
| 39 | + .foreign_key( | |
| 40 | + ForeignKey::create() | |
| 41 | + .name("fk_teams_owner_id") | |
| 42 | + .from(Teams::Table, Teams::OwnerId) | |
| 43 | + .to(Users::Table, Users::Id) | |
| 44 | + .on_delete(ForeignKeyAction::Cascade), | |
| 45 | + ) | |
| 46 | + .to_owned(), | |
| 47 | + ) | |
| 48 | + .await?; | |
| 49 | + | |
| 50 | + // ----------------------------------------------------------------- | |
| 51 | + // 2. team_members — N:N with role (owner/admin/member) | |
| 52 | + // ----------------------------------------------------------------- | |
| 53 | + manager | |
| 54 | + .create_table( | |
| 55 | + Table::create() | |
| 56 | + .table(TeamMembers::Table) | |
| 57 | + .if_not_exists() | |
| 58 | + .col(ColumnDef::new(TeamMembers::TeamId).uuid().not_null()) | |
| 59 | + .col(ColumnDef::new(TeamMembers::UserId).uuid().not_null()) | |
| 60 | + .col( | |
| 61 | + ColumnDef::new(TeamMembers::Role) | |
| 62 | + .string() | |
| 63 | + .string_len(16) | |
| 64 | + .not_null() | |
| 65 | + .default("member"), | |
| 66 | + ) | |
| 67 | + .col( | |
| 68 | + ColumnDef::new(TeamMembers::JoinedAt) | |
| 69 | + .timestamp_with_time_zone() | |
| 70 | + .not_null() | |
| 71 | + .default(Expr::current_timestamp()), | |
| 72 | + ) | |
| 73 | + .primary_key( | |
| 74 | + Index::create() | |
| 75 | + .col(TeamMembers::TeamId) | |
| 76 | + .col(TeamMembers::UserId), | |
| 77 | + ) | |
| 78 | + .foreign_key( | |
| 79 | + ForeignKey::create() | |
| 80 | + .name("fk_team_members_team_id") | |
| 81 | + .from(TeamMembers::Table, TeamMembers::TeamId) | |
| 82 | + .to(Teams::Table, Teams::Id) | |
| 83 | + .on_delete(ForeignKeyAction::Cascade), | |
| 84 | + ) | |
| 85 | + .foreign_key( | |
| 86 | + ForeignKey::create() | |
| 87 | + .name("fk_team_members_user_id") | |
| 88 | + .from(TeamMembers::Table, TeamMembers::UserId) | |
| 89 | + .to(Users::Table, Users::Id) | |
| 90 | + .on_delete(ForeignKeyAction::Cascade), | |
| 91 | + ) | |
| 92 | + .to_owned(), | |
| 93 | + ) | |
| 94 | + .await?; | |
| 95 | + | |
| 96 | + // Index: list teams of a user in constant time. | |
| 97 | + manager | |
| 98 | + .create_index( | |
| 99 | + Index::create() | |
| 100 | + .name("idx_team_members_user") | |
| 101 | + .table(TeamMembers::Table) | |
| 102 | + .col(TeamMembers::UserId) | |
| 103 | + .to_owned(), | |
| 104 | + ) | |
| 105 | + .await?; | |
| 106 | + | |
| 107 | + // ----------------------------------------------------------------- | |
| 108 | + // 3. resource_shares — extend to accept team principals | |
| 109 | + // | |
| 110 | + // Strategy (option B of the evolution plan): keep the existing | |
| 111 | + // `shared_with_user_id` column (now nullable) and add a companion | |
| 112 | + // `shared_with_team_id`. A DB-level CHECK constraint enforces that | |
| 113 | + // exactly one is set — keeps schema-level referential integrity | |
| 114 | + // instead of a polymorphic ID + discriminator string. | |
| 115 | + // ----------------------------------------------------------------- | |
| 116 | + manager | |
| 117 | + .get_connection() | |
| 118 | + .execute_unprepared( | |
| 119 | + "ALTER TABLE resource_shares ALTER COLUMN shared_with_user_id DROP NOT NULL", | |
| 120 | + ) | |
| 121 | + .await?; | |
| 122 | + | |
| 123 | + manager | |
| 124 | + .alter_table( | |
| 125 | + Table::alter() | |
| 126 | + .table(ResourceShares::Table) | |
| 127 | + .add_column( | |
| 128 | + ColumnDef::new(ResourceShares::SharedWithTeamId) | |
| 129 | + .uuid() | |
| 130 | + .null(), | |
| 131 | + ) | |
| 132 | + .to_owned(), | |
| 133 | + ) | |
| 134 | + .await?; | |
| 135 | + | |
| 136 | + manager | |
| 137 | + .get_connection() | |
| 138 | + .execute_unprepared( | |
| 139 | + "ALTER TABLE resource_shares \ | |
| 140 | + ADD CONSTRAINT fk_resource_shares_shared_with_team \ | |
| 141 | + FOREIGN KEY (shared_with_team_id) REFERENCES teams(id) ON DELETE CASCADE", | |
| 142 | + ) | |
| 143 | + .await?; | |
| 144 | + | |
| 145 | + // Exactly one of (user, team) must be set. | |
| 146 | + manager | |
| 147 | + .get_connection() | |
| 148 | + .execute_unprepared( | |
| 149 | + "ALTER TABLE resource_shares \ | |
| 150 | + ADD CONSTRAINT chk_resource_shares_principal \ | |
| 151 | + CHECK ((shared_with_user_id IS NOT NULL)::int + \ | |
| 152 | + (shared_with_team_id IS NOT NULL)::int = 1)", | |
| 153 | + ) | |
| 154 | + .await?; | |
| 155 | + | |
| 156 | + // The existing unique (resource_id, shared_with_user_id) index becomes | |
| 157 | + // partial — only enforce uniqueness when the user column is set. | |
| 158 | + // We drop & recreate instead of altering to keep this cross-backend-friendly. | |
| 159 | + manager | |
| 160 | + .get_connection() | |
| 161 | + .execute_unprepared("DROP INDEX IF EXISTS idx_resource_shares_resource_user_unique") | |
| 162 | + .await?; | |
| 163 | + | |
| 164 | + manager | |
| 165 | + .get_connection() | |
| 166 | + .execute_unprepared( | |
| 167 | + "CREATE UNIQUE INDEX idx_resource_shares_resource_user_unique \ | |
| 168 | + ON resource_shares (resource_id, shared_with_user_id) \ | |
| 169 | + WHERE shared_with_user_id IS NOT NULL", | |
| 170 | + ) | |
| 171 | + .await?; | |
| 172 | + | |
| 173 | + manager | |
| 174 | + .get_connection() | |
| 175 | + .execute_unprepared( | |
| 176 | + "CREATE UNIQUE INDEX idx_resource_shares_resource_team_unique \ | |
| 177 | + ON resource_shares (resource_id, shared_with_team_id) \ | |
| 178 | + WHERE shared_with_team_id IS NOT NULL", | |
| 179 | + ) | |
| 180 | + .await?; | |
| 181 | + | |
| 182 | + manager | |
| 183 | + .get_connection() | |
| 184 | + .execute_unprepared( | |
| 185 | + "CREATE INDEX idx_resource_shares_team ON resource_shares (shared_with_team_id) \ | |
| 186 | + WHERE shared_with_team_id IS NOT NULL", | |
| 187 | + ) | |
| 188 | + .await?; | |
| 189 | + | |
| 190 | + Ok(()) | |
| 191 | + } | |
| 192 | + | |
| 193 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 194 | + manager | |
| 195 | + .get_connection() | |
| 196 | + .execute_unprepared("DROP INDEX IF EXISTS idx_resource_shares_team") | |
| 197 | + .await?; | |
| 198 | + manager | |
| 199 | + .get_connection() | |
| 200 | + .execute_unprepared("DROP INDEX IF EXISTS idx_resource_shares_resource_team_unique") | |
| 201 | + .await?; | |
| 202 | + manager | |
| 203 | + .get_connection() | |
| 204 | + .execute_unprepared("DROP INDEX IF EXISTS idx_resource_shares_resource_user_unique") | |
| 205 | + .await?; | |
| 206 | + manager | |
| 207 | + .get_connection() | |
| 208 | + .execute_unprepared( | |
| 209 | + "CREATE UNIQUE INDEX idx_resource_shares_resource_user_unique \ | |
| 210 | + ON resource_shares (resource_id, shared_with_user_id)", | |
| 211 | + ) | |
| 212 | + .await?; | |
| 213 | + manager | |
| 214 | + .get_connection() | |
| 215 | + .execute_unprepared( | |
| 216 | + "ALTER TABLE resource_shares DROP CONSTRAINT IF EXISTS chk_resource_shares_principal", | |
| 217 | + ) | |
| 218 | + .await?; | |
| 219 | + manager | |
| 220 | + .get_connection() | |
| 221 | + .execute_unprepared( | |
| 222 | + "ALTER TABLE resource_shares DROP CONSTRAINT IF EXISTS fk_resource_shares_shared_with_team", | |
| 223 | + ) | |
| 224 | + .await?; | |
| 225 | + manager | |
| 226 | + .alter_table( | |
| 227 | + Table::alter() | |
| 228 | + .table(ResourceShares::Table) | |
| 229 | + .drop_column(ResourceShares::SharedWithTeamId) | |
| 230 | + .to_owned(), | |
| 231 | + ) | |
| 232 | + .await?; | |
| 233 | + manager | |
| 234 | + .get_connection() | |
| 235 | + .execute_unprepared( | |
| 236 | + "ALTER TABLE resource_shares ALTER COLUMN shared_with_user_id SET NOT NULL", | |
| 237 | + ) | |
| 238 | + .await?; | |
| 239 | + manager | |
| 240 | + .drop_table(Table::drop().table(TeamMembers::Table).to_owned()) | |
| 241 | + .await?; | |
| 242 | + manager | |
| 243 | + .drop_table(Table::drop().table(Teams::Table).to_owned()) | |
| 244 | + .await | |
| 245 | + } | |
| 246 | +} | |
| 247 | + | |
| 248 | +#[derive(Iden)] | |
| 249 | +enum Teams { | |
| 250 | + Table, | |
| 251 | + Id, | |
| 252 | + Slug, | |
| 253 | + Name, | |
| 254 | + OwnerId, | |
| 255 | + CreatedAt, | |
| 256 | +} | |
| 257 | + | |
| 258 | +#[derive(Iden)] | |
| 259 | +enum TeamMembers { | |
| 260 | + Table, | |
| 261 | + TeamId, | |
| 262 | + UserId, | |
| 263 | + Role, | |
| 264 | + JoinedAt, | |
| 265 | +} | |
| 266 | + | |
| 267 | +#[derive(Iden)] | |
| 268 | +enum ResourceShares { | |
| 269 | + Table, | |
| 270 | + SharedWithTeamId, | |
| 271 | +} | |
| 272 | + | |
| 273 | +#[derive(Iden)] | |
| 274 | +enum Users { | |
| 275 | + Table, | |
| 276 | + Id, | |
| 277 | +} |
A
crates/rustwarden-core/src/migrations/m20260510_000009_fediverse_interop.rs
+364
-0
@@ -0,0 +1,364 @@
| 1 | +//! Migration `m20260510_000009_fediverse_interop`. | |
| 2 | +//! | |
| 3 | +//! Fused migration that defines the fediverse-aware surface of the | |
| 4 | +//! core schema. Replaces the legacy granular migrations: | |
| 5 | +//! | |
| 6 | +//! - `m20260510_000009_remote_identities` — `remote_identities` table (RFC 0003) | |
| 7 | +//! - `m20260510_000011_extend_users_with_actor_keys` — `users.ap_*` columns (RFC 0005) | |
| 8 | +//! - `m20260510_000014_identity_sibling_fks` — sibling FK columns on | |
| 9 | +//! `team_members`, `resource_shares`, `audit_logs` + their partial | |
| 10 | +//! indexes (RFC 0008) | |
| 11 | +//! - `m20260510_000016_relax_team_members_pk` — surrogate `team_members.id` | |
| 12 | +//! PK, XOR check on `(user_id, remote_identity_id)`, partial | |
| 13 | +//! uniqueness indexes (RFC 0018) | |
| 14 | +//! | |
| 15 | +//! Pure-protocol fediverse tables (`fediverse_trusted_domains`, | |
| 16 | +//! `fediverse_outbound_queue`, `fediverse_auth_challenges`, | |
| 17 | +//! `fediverse_pending_invites`) live in the dedicated | |
| 18 | +//! `rustwarden-fediverse` migrator instead. | |
| 19 | + | |
| 20 | +use sea_orm_migration::prelude::*; | |
| 21 | +use sea_orm_migration::sea_orm::ConnectionTrait; | |
| 22 | + | |
| 23 | +#[derive(DeriveMigrationName)] | |
| 24 | +pub struct Migration; | |
| 25 | + | |
| 26 | +#[async_trait::async_trait] | |
| 27 | +impl MigrationTrait for Migration { | |
| 28 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 29 | + // ---- RFC 0003: cache for remote ActivityPub actors ----------- | |
| 30 | + manager | |
| 31 | + .create_table( | |
| 32 | + Table::create() | |
| 33 | + .table(RemoteIdentities::Table) | |
| 34 | + .col( | |
| 35 | + ColumnDef::new(RemoteIdentities::Id) | |
| 36 | + .uuid() | |
| 37 | + .not_null() | |
| 38 | + .primary_key(), | |
| 39 | + ) | |
| 40 | + .col( | |
| 41 | + ColumnDef::new(RemoteIdentities::ApId) | |
| 42 | + .text() | |
| 43 | + .not_null() | |
| 44 | + .unique_key(), | |
| 45 | + ) | |
| 46 | + .col(ColumnDef::new(RemoteIdentities::Username).text().not_null()) | |
| 47 | + .col(ColumnDef::new(RemoteIdentities::Domain).text().not_null()) | |
| 48 | + .col(ColumnDef::new(RemoteIdentities::DisplayName).text().null()) | |
| 49 | + .col(ColumnDef::new(RemoteIdentities::SummaryHtml).text().null()) | |
| 50 | + .col(ColumnDef::new(RemoteIdentities::InboxUrl).text().not_null()) | |
| 51 | + .col(ColumnDef::new(RemoteIdentities::SharedInboxUrl).text().null()) | |
| 52 | + .col( | |
| 53 | + ColumnDef::new(RemoteIdentities::PublicKeyPem) | |
| 54 | + .text() | |
| 55 | + .not_null(), | |
| 56 | + ) | |
| 57 | + .col(ColumnDef::new(RemoteIdentities::IconUrl).text().null()) | |
| 58 | + .col( | |
| 59 | + ColumnDef::new(RemoteIdentities::ActorType) | |
| 60 | + .text() | |
| 61 | + .not_null() | |
| 62 | + .default("Person"), | |
| 63 | + ) | |
| 64 | + .col(ColumnDef::new(RemoteIdentities::RawJson).json_binary().null()) | |
| 65 | + .col( | |
| 66 | + ColumnDef::new(RemoteIdentities::LastFetchedAt) | |
| 67 | + .timestamp_with_time_zone() | |
| 68 | + .not_null(), | |
| 69 | + ) | |
| 70 | + .col( | |
| 71 | + ColumnDef::new(RemoteIdentities::DisabledAt) | |
| 72 | + .timestamp_with_time_zone() | |
| 73 | + .null(), | |
| 74 | + ) | |
| 75 | + .col( | |
| 76 | + ColumnDef::new(RemoteIdentities::CreatedAt) | |
| 77 | + .timestamp_with_time_zone() | |
| 78 | + .not_null(), | |
| 79 | + ) | |
| 80 | + .col( | |
| 81 | + ColumnDef::new(RemoteIdentities::UpdatedAt) | |
| 82 | + .timestamp_with_time_zone() | |
| 83 | + .not_null(), | |
| 84 | + ) | |
| 85 | + .to_owned(), | |
| 86 | + ) | |
| 87 | + .await?; | |
| 88 | + | |
| 89 | + manager | |
| 90 | + .create_index( | |
| 91 | + Index::create() | |
| 92 | + .table(RemoteIdentities::Table) | |
| 93 | + .name("uniq_remote_identities_handle") | |
| 94 | + .col(RemoteIdentities::Username) | |
| 95 | + .col(RemoteIdentities::Domain) | |
| 96 | + .unique() | |
| 97 | + .to_owned(), | |
| 98 | + ) | |
| 99 | + .await?; | |
| 100 | + | |
| 101 | + // ---- RFC 0005: per-user ActivityPub actor keys --------------- | |
| 102 | + manager | |
| 103 | + .alter_table( | |
| 104 | + Table::alter() | |
| 105 | + .table(Users::Table) | |
| 106 | + .add_column(ColumnDef::new(Users::ApPublicKeyPem).text().null()) | |
| 107 | + .to_owned(), | |
| 108 | + ) | |
| 109 | + .await?; | |
| 110 | + manager | |
| 111 | + .alter_table( | |
| 112 | + Table::alter() | |
| 113 | + .table(Users::Table) | |
| 114 | + .add_column(ColumnDef::new(Users::ApPrivateKeyPem).text().null()) | |
| 115 | + .to_owned(), | |
| 116 | + ) | |
| 117 | + .await?; | |
| 118 | + manager | |
| 119 | + .alter_table( | |
| 120 | + Table::alter() | |
| 121 | + .table(Users::Table) | |
| 122 | + .add_column( | |
| 123 | + ColumnDef::new(Users::ApDiscoverable) | |
| 124 | + .boolean() | |
| 125 | + .not_null() | |
| 126 | + .default(true), | |
| 127 | + ) | |
| 128 | + .to_owned(), | |
| 129 | + ) | |
| 130 | + .await?; | |
| 131 | + | |
| 132 | + // ---- RFC 0008: sibling FK columns for remote identities ------ | |
| 133 | + manager | |
| 134 | + .alter_table( | |
| 135 | + Table::alter() | |
| 136 | + .table(TeamMembers::Table) | |
| 137 | + .add_column(ColumnDef::new(TeamMembers::RemoteIdentityId).uuid().null()) | |
| 138 | + .to_owned(), | |
| 139 | + ) | |
| 140 | + .await?; | |
| 141 | + manager | |
| 142 | + .alter_table( | |
| 143 | + Table::alter() | |
| 144 | + .table(ResourceShares::Table) | |
| 145 | + .add_column( | |
| 146 | + ColumnDef::new(ResourceShares::SharedWithRemoteIdentityId) | |
| 147 | + .uuid() | |
| 148 | + .null(), | |
| 149 | + ) | |
| 150 | + .to_owned(), | |
| 151 | + ) | |
| 152 | + .await?; | |
| 153 | + manager | |
| 154 | + .alter_table( | |
| 155 | + Table::alter() | |
| 156 | + .table(AuditLogs::Table) | |
| 157 | + .add_column( | |
| 158 | + ColumnDef::new(AuditLogs::ActorRemoteIdentityId) | |
| 159 | + .uuid() | |
| 160 | + .null(), | |
| 161 | + ) | |
| 162 | + .to_owned(), | |
| 163 | + ) | |
| 164 | + .await?; | |
| 165 | + manager | |
| 166 | + .alter_table( | |
| 167 | + Table::alter() | |
| 168 | + .table(AuditLogs::Table) | |
| 169 | + .add_column( | |
| 170 | + ColumnDef::new(AuditLogs::TargetRemoteIdentityId) | |
| 171 | + .uuid() | |
| 172 | + .null(), | |
| 173 | + ) | |
| 174 | + .to_owned(), | |
| 175 | + ) | |
| 176 | + .await?; | |
| 177 | + | |
| 178 | + let conn = manager.get_connection(); | |
| 179 | + conn.execute_unprepared( | |
| 180 | + "CREATE INDEX team_members_remote_identity_idx \ | |
| 181 | + ON team_members (remote_identity_id) \ | |
| 182 | + WHERE remote_identity_id IS NOT NULL", | |
| 183 | + ) | |
| 184 | + .await?; | |
| 185 | + conn.execute_unprepared( | |
| 186 | + "CREATE INDEX resource_shares_remote_identity_idx \ | |
| 187 | + ON resource_shares (shared_with_remote_identity_id) \ | |
| 188 | + WHERE shared_with_remote_identity_id IS NOT NULL", | |
| 189 | + ) | |
| 190 | + .await?; | |
| 191 | + | |
| 192 | + // ---- RFC 0018: surrogate team_members.id PK + XOR ------------ | |
| 193 | + conn.execute_unprepared("ALTER TABLE team_members DROP CONSTRAINT team_members_pkey") | |
| 194 | + .await?; | |
| 195 | + conn.execute_unprepared("ALTER TABLE team_members ALTER COLUMN user_id DROP NOT NULL") | |
| 196 | + .await?; | |
| 197 | + conn.execute_unprepared( | |
| 198 | + "ALTER TABLE team_members ADD CONSTRAINT chk_team_members_principal \ | |
| 199 | + CHECK ((user_id IS NOT NULL)::int + (remote_identity_id IS NOT NULL)::int = 1)", | |
| 200 | + ) | |
| 201 | + .await?; | |
| 202 | + conn.execute_unprepared("CREATE EXTENSION IF NOT EXISTS pgcrypto") | |
| 203 | + .await?; | |
| 204 | + conn.execute_unprepared( | |
| 205 | + "ALTER TABLE team_members ADD COLUMN id UUID NOT NULL DEFAULT gen_random_uuid()", | |
| 206 | + ) | |
| 207 | + .await?; | |
| 208 | + conn.execute_unprepared("ALTER TABLE team_members ADD PRIMARY KEY (id)") | |
| 209 | + .await?; | |
| 210 | + conn.execute_unprepared( | |
| 211 | + "CREATE UNIQUE INDEX team_members_local_unique \ | |
| 212 | + ON team_members (team_id, user_id) \ | |
| 213 | + WHERE user_id IS NOT NULL", | |
| 214 | + ) | |
| 215 | + .await?; | |
| 216 | + conn.execute_unprepared( | |
| 217 | + "CREATE UNIQUE INDEX team_members_remote_unique \ | |
| 218 | + ON team_members (team_id, remote_identity_id) \ | |
| 219 | + WHERE remote_identity_id IS NOT NULL", | |
| 220 | + ) | |
| 221 | + .await?; | |
| 222 | + | |
| 223 | + Ok(()) | |
| 224 | + } | |
| 225 | + | |
| 226 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 227 | + let conn = manager.get_connection(); | |
| 228 | + | |
| 229 | + // ---- RFC 0018 reverse ---------------------------------------- | |
| 230 | + for sql in [ | |
| 231 | + "DROP INDEX IF EXISTS team_members_remote_unique", | |
| 232 | + "DROP INDEX IF EXISTS team_members_local_unique", | |
| 233 | + "ALTER TABLE team_members DROP CONSTRAINT IF EXISTS team_members_pkey", | |
| 234 | + "ALTER TABLE team_members DROP COLUMN IF EXISTS id", | |
| 235 | + "ALTER TABLE team_members DROP CONSTRAINT IF EXISTS chk_team_members_principal", | |
| 236 | + "ALTER TABLE team_members ALTER COLUMN user_id SET NOT NULL", | |
| 237 | + "ALTER TABLE team_members ADD PRIMARY KEY (team_id, user_id)", | |
| 238 | + ] { | |
| 239 | + conn.execute_unprepared(sql).await?; | |
| 240 | + } | |
| 241 | + | |
| 242 | + // ---- RFC 0008 reverse: drop partial indexes then columns ----- | |
| 243 | + for sql in [ | |
| 244 | + "DROP INDEX IF EXISTS resource_shares_remote_identity_idx", | |
| 245 | + "DROP INDEX IF EXISTS team_members_remote_identity_idx", | |
| 246 | + ] { | |
| 247 | + conn.execute_unprepared(sql).await?; | |
| 248 | + } | |
| 249 | + | |
| 250 | + manager | |
| 251 | + .alter_table( | |
| 252 | + Table::alter() | |
| 253 | + .table(AuditLogs::Table) | |
| 254 | + .drop_column(AuditLogs::TargetRemoteIdentityId) | |
| 255 | + .to_owned(), | |
| 256 | + ) | |
| 257 | + .await?; | |
| 258 | + manager | |
| 259 | + .alter_table( | |
| 260 | + Table::alter() | |
| 261 | + .table(AuditLogs::Table) | |
| 262 | + .drop_column(AuditLogs::ActorRemoteIdentityId) | |
| 263 | + .to_owned(), | |
| 264 | + ) | |
| 265 | + .await?; | |
| 266 | + manager | |
| 267 | + .alter_table( | |
| 268 | + Table::alter() | |
| 269 | + .table(ResourceShares::Table) | |
| 270 | + .drop_column(ResourceShares::SharedWithRemoteIdentityId) | |
| 271 | + .to_owned(), | |
| 272 | + ) | |
| 273 | + .await?; | |
| 274 | + manager | |
| 275 | + .alter_table( | |
| 276 | + Table::alter() | |
| 277 | + .table(TeamMembers::Table) | |
| 278 | + .drop_column(TeamMembers::RemoteIdentityId) | |
| 279 | + .to_owned(), | |
| 280 | + ) | |
| 281 | + .await?; | |
| 282 | + | |
| 283 | + // ---- RFC 0005 reverse ---------------------------------------- | |
| 284 | + manager | |
| 285 | + .alter_table( | |
| 286 | + Table::alter() | |
| 287 | + .table(Users::Table) | |
| 288 | + .drop_column(Users::ApDiscoverable) | |
| 289 | + .to_owned(), | |
| 290 | + ) | |
| 291 | + .await?; | |
| 292 | + manager | |
| 293 | + .alter_table( | |
| 294 | + Table::alter() | |
| 295 | + .table(Users::Table) | |
| 296 | + .drop_column(Users::ApPrivateKeyPem) | |
| 297 | + .to_owned(), | |
| 298 | + ) | |
| 299 | + .await?; | |
| 300 | + manager | |
| 301 | + .alter_table( | |
| 302 | + Table::alter() | |
| 303 | + .table(Users::Table) | |
| 304 | + .drop_column(Users::ApPublicKeyPem) | |
| 305 | + .to_owned(), | |
| 306 | + ) | |
| 307 | + .await?; | |
| 308 | + | |
| 309 | + // ---- RFC 0003 reverse ---------------------------------------- | |
| 310 | + manager | |
| 311 | + .drop_table(Table::drop().table(RemoteIdentities::Table).to_owned()) | |
| 312 | + .await?; | |
| 313 | + | |
| 314 | + Ok(()) | |
| 315 | + } | |
| 316 | +} | |
| 317 | + | |
| 318 | +#[derive(DeriveIden)] | |
| 319 | +enum RemoteIdentities { | |
| 320 | + Table, | |
| 321 | + Id, | |
| 322 | + ApId, | |
| 323 | + Username, | |
| 324 | + Domain, | |
| 325 | + DisplayName, | |
| 326 | + SummaryHtml, | |
| 327 | + InboxUrl, | |
| 328 | + SharedInboxUrl, | |
| 329 | + PublicKeyPem, | |
| 330 | + IconUrl, | |
| 331 | + ActorType, | |
| 332 | + RawJson, | |
| 333 | + LastFetchedAt, | |
| 334 | + DisabledAt, | |
| 335 | + CreatedAt, | |
| 336 | + UpdatedAt, | |
| 337 | +} | |
| 338 | + | |
| 339 | +#[derive(DeriveIden)] | |
| 340 | +enum Users { | |
| 341 | + Table, | |
| 342 | + ApPublicKeyPem, | |
| 343 | + ApPrivateKeyPem, | |
| 344 | + ApDiscoverable, | |
| 345 | +} | |
| 346 | + | |
| 347 | +#[derive(DeriveIden)] | |
| 348 | +enum TeamMembers { | |
| 349 | + Table, | |
| 350 | + RemoteIdentityId, | |
| 351 | +} | |
| 352 | + | |
| 353 | +#[derive(DeriveIden)] | |
| 354 | +enum ResourceShares { | |
| 355 | + Table, | |
| 356 | + SharedWithRemoteIdentityId, | |
| 357 | +} | |
| 358 | + | |
| 359 | +#[derive(DeriveIden)] | |
| 360 | +enum AuditLogs { | |
| 361 | + Table, | |
| 362 | + ActorRemoteIdentityId, | |
| 363 | + TargetRemoteIdentityId, | |
| 364 | +} |
M
tools/rustwarden-cli/src/start_project.rs
+41
-16
@@ -5,7 +5,9 @@ use console::style;
| 5 | 5 | use dialoguer::{Confirm, Input, Select}; |
| 6 | 6 | use rand::Rng; |
| 7 | 7 | |
| 8 | -use crate::templates::{self, OAuthProviderConfig, OAuthWizardConfig, ProjectConfig, SmtpConfig}; | |
| 8 | +use crate::templates::{ | |
| 9 | + self, FediverseMode, OAuthProviderConfig, OAuthWizardConfig, ProjectConfig, SmtpConfig, | |
| 10 | +}; | |
| 9 | 11 | use crate::version; |
| 10 | 12 | |
| 11 | 13 | pub fn run() -> Result<(), Box<dyn std::error::Error>> { |
@@ -29,7 +31,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 29 | 31 | handle_existing_dir(&project_dir)?; |
| 30 | 32 | |
| 31 | 33 | let with_ui = ask_ui_mode()?; |
| 32 | - let (with_fediverse, fediverse_domain) = ask_fediverse_mode()?; | |
| 34 | + let (fediverse_mode, fediverse_domain) = ask_fediverse_mode()?; | |
| 33 | 35 | let auto_migrate = ask_auto_migrate()?; |
| 34 | 36 | let port = ask_port()?; |
| 35 | 37 |
@@ -52,7 +54,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 52 | 54 | app_display_name: app_display_name.clone(), |
| 53 | 55 | description: description.clone(), |
| 54 | 56 | with_ui, |
| 55 | - with_fediverse, | |
| 57 | + fediverse_mode, | |
| 56 | 58 | fediverse_domain, |
| 57 | 59 | auto_migrate, |
| 58 | 60 | port, |
@@ -76,8 +78,15 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 76 | 78 | if with_ui { |
| 77 | 79 | copy_crate(&rustwarden_root, &project_dir, "rustwarden-ui")?; |
| 78 | 80 | } |
| 79 | - if with_fediverse { | |
| 80 | - copy_crate(&rustwarden_root, &project_dir, "rustwarden-fediverse")?; | |
| 81 | + match cfg.fediverse_mode { | |
| 82 | + FediverseMode::None => {} | |
| 83 | + FediverseMode::Headless => { | |
| 84 | + copy_crate(&rustwarden_root, &project_dir, "rustwarden-fediverse")?; | |
| 85 | + } | |
| 86 | + FediverseMode::WithReferenceUi => { | |
| 87 | + copy_crate(&rustwarden_root, &project_dir, "rustwarden-fediverse")?; | |
| 88 | + copy_crate(&rustwarden_root, &project_dir, "rustwarden-fediverse-ui")?; | |
| 89 | + } | |
| 81 | 90 | } |
| 82 | 91 | |
| 83 | 92 | copy_resources(&rustwarden_root, &project_dir, with_ui)?; |
@@ -207,22 +216,38 @@ fn handle_existing_dir(project_dir: &Path) -> Result<(), Box<dyn std::error::Err
| 207 | 216 | Ok(()) |
| 208 | 217 | } |
| 209 | 218 | |
| 210 | -/// Demande si le projet doit etre genere avec la couche federation | |
| 211 | -/// ActivityPub (crate `rustwarden-fediverse`). Si oui, demande aussi | |
| 212 | -/// le domaine federe a placer dans `FEDIVERSE_DOMAIN`. | |
| 219 | +/// Demande comment le projet consommateur doit consommer la federation | |
| 220 | +/// ActivityPub : pas du tout, protocole headless seul, ou protocole + | |
| 221 | +/// UI HTML de reference. | |
| 213 | 222 | /// |
| 214 | -/// Retourne `(with_fediverse, Some(domain))` ou `(false, None)`. | |
| 215 | -fn ask_fediverse_mode() -> Result<(bool, Option<String>), Box<dyn std::error::Error>> { | |
| 216 | - let enable = Confirm::new() | |
| 223 | +/// Si un mode federe est selectionne, demande aussi le domaine federe a | |
| 224 | +/// placer dans `FEDIVERSE_DOMAIN`. | |
| 225 | +/// | |
| 226 | +/// Retourne `(FediverseMode, Some(domain))` quand la federation est | |
| 227 | +/// activee, ou `(FediverseMode::None, None)` sinon. | |
| 228 | +fn ask_fediverse_mode() -> Result<(FediverseMode, Option<String>), Box<dyn std::error::Error>> { | |
| 229 | + let options = vec![ | |
| 230 | + "Pas de federation", | |
| 231 | + "Federation headless (protocole AP seul, vous implementez votre propre UI)", | |
| 232 | + "Federation avec UI de reference (protocole AP + interface HTML par defaut) [recommande]", | |
| 233 | + ]; | |
| 234 | + let selection = Select::new() | |
| 217 | 235 | .with_prompt(format!( |
| 218 | 236 | "{}", |
| 219 | - style("Activer la federation ActivityPub (RFC 0001+, WebFinger, AP-native auth, invitations cross-instance) ?").cyan() | |
| 237 | + style("Federation ActivityPub (RFC 0001+, WebFinger, AP-native auth, invitations cross-instance)").cyan() | |
| 220 | 238 | )) |
| 221 | - .default(false) | |
| 239 | + .items(&options) | |
| 240 | + .default(2) | |
| 222 | 241 | .interact()?; |
| 223 | 242 | |
| 224 | - if !enable { | |
| 225 | - return Ok((false, None)); | |
| 243 | + let mode = match selection { | |
| 244 | + 0 => FediverseMode::None, | |
| 245 | + 1 => FediverseMode::Headless, | |
| 246 | + _ => FediverseMode::WithReferenceUi, | |
| 247 | + }; | |
| 248 | + | |
| 249 | + if matches!(mode, FediverseMode::None) { | |
| 250 | + return Ok((mode, None)); | |
| 226 | 251 | } |
| 227 | 252 | |
| 228 | 253 | let domain: String = Input::new() |
@@ -260,7 +285,7 @@ fn ask_fediverse_mode() -> Result<(bool, Option<String>), Box<dyn std::error::Er
| 260 | 285 | }) |
| 261 | 286 | .interact_text()?; |
| 262 | 287 | |
| 263 | - Ok((true, Some(domain.trim().to_string()))) | |
| 288 | + Ok((mode, Some(domain.trim().to_string()))) | |
| 264 | 289 | } |
| 265 | 290 | |
| 266 | 291 | fn ask_ui_mode() -> Result<bool, Box<dyn std::error::Error>> { |
M
tools/rustwarden-cli/src/templates.rs
+127
-42
@@ -29,18 +29,38 @@ pub struct OAuthWizardConfig {
| 29 | 29 | pub providers: Vec<OAuthProviderConfig>, |
| 30 | 30 | } |
| 31 | 31 | |
| 32 | +/// How the generated project consumes the federation layer. | |
| 33 | +/// | |
| 34 | +/// - [`FediverseMode::None`] — no federation crate copied, no feature | |
| 35 | +/// declared, no wiring injected in `main.rs`. | |
| 36 | +/// - [`FediverseMode::Headless`] — copies `rustwarden-fediverse` only; | |
| 37 | +/// the consumer project implements its own UI on top of the protocol | |
| 38 | +/// crate. Adds the `fediverse` feature. | |
| 39 | +/// - [`FediverseMode::WithReferenceUi`] — copies both | |
| 40 | +/// `rustwarden-fediverse` and `rustwarden-fediverse-ui`. Adds the | |
| 41 | +/// `fediverse` feature plus a `fediverse-ui` feature that implies | |
| 42 | +/// `fediverse` and pulls in the reference HTML UI. | |
| 43 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 44 | +pub enum FediverseMode { | |
| 45 | + /// No federation support. | |
| 46 | + None, | |
| 47 | + /// Bare ActivityPub protocol; the consumer ships its own UI. | |
| 48 | + Headless, | |
| 49 | + /// Protocol plus the reference HTML UI crate. | |
| 50 | + WithReferenceUi, | |
| 51 | +} | |
| 52 | + | |
| 32 | 53 | pub struct ProjectConfig { |
| 33 | 54 | pub name: String, |
| 34 | 55 | pub app_display_name: String, |
| 35 | 56 | pub description: String, |
| 36 | 57 | pub with_ui: bool, |
| 37 | - /// Active la federation ActivityPub via le crate `rustwarden-fediverse`. | |
| 38 | - /// Si `true` : copie le crate, ajoute la feature `fediverse` au Cargo.toml, | |
| 39 | - /// injecte le wiring dans `main.rs` et seede les variables `FEDIVERSE_*`. | |
| 40 | - pub with_fediverse: bool, | |
| 41 | - /// Domaine federe declare au boot quand `with_fediverse = true` | |
| 42 | - /// (`FEDIVERSE_DOMAIN`). Rester `None` quand la federation est | |
| 43 | - /// desactivee. | |
| 58 | + /// Selects the federation layer to wire into the generated project. | |
| 59 | + /// Drives crate copy, `Cargo.toml` features, `main.rs` wiring and | |
| 60 | + /// the `FEDIVERSE_*` env section. | |
| 61 | + pub fediverse_mode: FediverseMode, | |
| 62 | + /// Federated domain declared at boot when `fediverse_mode != None` | |
| 63 | + /// (`FEDIVERSE_DOMAIN`). Stays `None` when federation is disabled. | |
| 44 | 64 | pub fediverse_domain: Option<String>, |
| 45 | 65 | pub auto_migrate: bool, |
| 46 | 66 | pub port: u16, |
@@ -55,6 +75,23 @@ pub struct ProjectConfig {
| 55 | 75 | pub oauth: Option<OAuthWizardConfig>, |
| 56 | 76 | } |
| 57 | 77 | |
| 78 | +impl ProjectConfig { | |
| 79 | + /// Internal helper: `true` when any federation crate must be copied | |
| 80 | + /// and any `FEDIVERSE_*` env section must be emitted. | |
| 81 | + pub fn has_fediverse(&self) -> bool { | |
| 82 | + matches!( | |
| 83 | + self.fediverse_mode, | |
| 84 | + FediverseMode::Headless | FediverseMode::WithReferenceUi | |
| 85 | + ) | |
| 86 | + } | |
| 87 | + | |
| 88 | + /// Internal helper: `true` when the reference UI crate must be | |
| 89 | + /// copied and the `fediverse-ui` feature emitted. | |
| 90 | + pub fn has_fediverse_ui(&self) -> bool { | |
| 91 | + matches!(self.fediverse_mode, FediverseMode::WithReferenceUi) | |
| 92 | + } | |
| 93 | +} | |
| 94 | + | |
| 58 | 95 | pub fn cargo_toml(cfg: &ProjectConfig) -> String { |
| 59 | 96 | let ui_member = if cfg.with_ui { |
| 60 | 97 | ", \"crates/rustwarden-ui\"" |
@@ -62,40 +99,67 @@ pub fn cargo_toml(cfg: &ProjectConfig) -> String {
| 62 | 99 | "" |
| 63 | 100 | }; |
| 64 | 101 | |
| 65 | - let fediverse_member = if cfg.with_fediverse { | |
| 102 | + let fediverse_member = if cfg.has_fediverse() { | |
| 66 | 103 | ", \"crates/rustwarden-fediverse\"" |
| 67 | 104 | } else { |
| 68 | 105 | "" |
| 69 | 106 | }; |
| 70 | 107 | |
| 108 | + let fediverse_ui_member = if cfg.has_fediverse_ui() { | |
| 109 | + ", \"crates/rustwarden-fediverse-ui\"" | |
| 110 | + } else { | |
| 111 | + "" | |
| 112 | + }; | |
| 113 | + | |
| 71 | 114 | let ui_dep = if cfg.with_ui { |
| 72 | 115 | "\nrustwarden-ui = { path = \"crates/rustwarden-ui\" }" |
| 73 | 116 | } else { |
| 74 | 117 | "" |
| 75 | 118 | }; |
| 76 | 119 | |
| 77 | - let fediverse_dep = if cfg.with_fediverse { | |
| 120 | + let fediverse_dep = if cfg.has_fediverse() { | |
| 78 | 121 | "\nrustwarden-fediverse = { path = \"crates/rustwarden-fediverse\", optional = true }" |
| 79 | 122 | } else { |
| 80 | 123 | "" |
| 81 | 124 | }; |
| 82 | 125 | |
| 83 | - let features_section = if cfg.with_fediverse { | |
| 84 | - let ui_feature = if cfg.with_ui { | |
| 85 | - ", \"rustwarden-ui/fediverse\"" | |
| 86 | - } else { | |
| 87 | - "" | |
| 88 | - }; | |
| 89 | - format!( | |
| 90 | - "\n[features]\ndefault = [\"fediverse\"]\n# Federation ActivityPub (RFC 0001+).\n# Activable a la compilation. Cf. README §Federation ActivityPub.\nfediverse = [\"dep:rustwarden-fediverse\"{ui_feature}]\n" | |
| 91 | - ) | |
| 126 | + let fediverse_ui_dep = if cfg.has_fediverse_ui() { | |
| 127 | + "\nrustwarden-fediverse-ui = { path = \"crates/rustwarden-fediverse-ui\", optional = true }" | |
| 92 | 128 | } else { |
| 93 | - String::new() | |
| 129 | + "" | |
| 130 | + }; | |
| 131 | + | |
| 132 | + // Features section: stays empty when federation is disabled. When | |
| 133 | + // enabled, `default = []` so consumers must opt-in explicitly with | |
| 134 | + // `--features fediverse` (or `fediverse-ui`, which implies it). | |
| 135 | + let features_section = match cfg.fediverse_mode { | |
| 136 | + FediverseMode::None => String::new(), | |
| 137 | + FediverseMode::Headless => { | |
| 138 | + "\n[features]\n\ | |
| 139 | + default = []\n\ | |
| 140 | + # Active le protocole ActivityPub headless (RFC 0001+).\n\ | |
| 141 | + # Compile et lie la crate `rustwarden-fediverse` (services, routes,\n\ | |
| 142 | + # signatures HTTP) sans aucune UI.\n\ | |
| 143 | + fediverse = [\"dep:rustwarden-fediverse\"]\n" | |
| 144 | + .to_string() | |
| 145 | + } | |
| 146 | + FediverseMode::WithReferenceUi => { | |
| 147 | + "\n[features]\n\ | |
| 148 | + default = []\n\ | |
| 149 | + # Active le protocole ActivityPub headless (RFC 0001+).\n\ | |
| 150 | + # Compile et lie la crate `rustwarden-fediverse` (services, routes,\n\ | |
| 151 | + # signatures HTTP) sans aucune UI.\n\ | |
| 152 | + fediverse = [\"dep:rustwarden-fediverse\"]\n\ | |
| 153 | + # Active l'UI HTML de reference pour la federation (handlers, templates,\n\ | |
| 154 | + # carte dashboard). Implique automatiquement `fediverse`.\n\ | |
| 155 | + fediverse-ui = [\"fediverse\", \"dep:rustwarden-fediverse-ui\"]\n" | |
| 156 | + .to_string() | |
| 157 | + } | |
| 94 | 158 | }; |
| 95 | 159 | |
| 96 | 160 | format!( |
| 97 | 161 | r#"[workspace] |
| 98 | -members = ["crates/rustwarden-core"{ui_member}{fediverse_member}] | |
| 162 | +members = ["crates/rustwarden-core"{ui_member}{fediverse_member}{fediverse_ui_member}] | |
| 99 | 163 | resolver = "2" |
| 100 | 164 | |
| 101 | 165 | [workspace.lints.clippy] |
@@ -112,7 +176,7 @@ name = "{name}"
| 112 | 176 | path = "src/main.rs" |
| 113 | 177 | {features_section} |
| 114 | 178 | [dependencies] |
| 115 | -rustwarden-core = {{ path = "crates/rustwarden-core" }}{ui_dep}{fediverse_dep} | |
| 179 | +rustwarden-core = {{ path = "crates/rustwarden-core" }}{ui_dep}{fediverse_dep}{fediverse_ui_dep} | |
| 116 | 180 | axum = {{ version = "0.8", features = ["macros"] }} |
| 117 | 181 | tokio = {{ version = "1", features = ["full"] }} |
| 118 | 182 | sea-orm = {{ version = "2.0.0-rc.20", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] }} |
@@ -128,8 +192,10 @@ empty_line_after_doc_comments = "allow"
| 128 | 192 | description = cfg.description, |
| 129 | 193 | ui_member = ui_member, |
| 130 | 194 | fediverse_member = fediverse_member, |
| 195 | + fediverse_ui_member = fediverse_ui_member, | |
| 131 | 196 | ui_dep = ui_dep, |
| 132 | 197 | fediverse_dep = fediverse_dep, |
| 198 | + fediverse_ui_dep = fediverse_ui_dep, | |
| 133 | 199 | features_section = features_section, |
| 134 | 200 | ) |
| 135 | 201 | } |
@@ -159,13 +225,32 @@ pub fn main_rs(cfg: &ProjectConfig) -> String {
| 159 | 225 | "" |
| 160 | 226 | }; |
| 161 | 227 | |
| 162 | - // Wiring conditionnel de la federation ActivityPub. Casse la chaine | |
| 163 | - // du builder en 3 etapes pour permettre l'ouverture precoce de la | |
| 164 | - // connexion DB requise par le router fediverse. | |
| 165 | - let (builder_open, fediverse_block, builder_close) = if cfg.with_fediverse { | |
| 166 | - ( | |
| 167 | - "let builder = RustwardenApp::builder()\n", | |
| 168 | - r#" | |
| 228 | + // Wiring conditionnel de la federation ActivityPub. Quand la | |
| 229 | + // federation est activee, on casse la chaine du builder en plusieurs | |
| 230 | + // etapes pour permettre l'ouverture precoce de la connexion DB | |
| 231 | + // requise par le router fediverse, puis on injecte un second bloc | |
| 232 | + // `cfg(feature = "fediverse-ui")` quand l'UI de reference est | |
| 233 | + // demandee. | |
| 234 | + let (builder_open, fediverse_block, fediverse_ui_block, builder_close) = | |
| 235 | + if cfg.has_fediverse() { | |
| 236 | + let fediverse_ui = if cfg.has_fediverse_ui() { | |
| 237 | + r#" | |
| 238 | + // Reference HTML UI for the federation. Opt-in via the | |
| 239 | + // `fediverse-ui` feature, which itself implies `fediverse`. Mounts | |
| 240 | + // the UI router and injects the federation card into the admin | |
| 241 | + // dashboard. | |
| 242 | + #[cfg(feature = "fediverse-ui")] | |
| 243 | + let builder = builder | |
| 244 | + .merge_routes(rustwarden_fediverse_ui::default_routes()) | |
| 245 | + .add_dashboard_card(rustwarden_fediverse_ui::dashboard_card()); | |
| 246 | +"# | |
| 247 | + } else { | |
| 248 | + "" | |
| 249 | + }; | |
| 250 | + | |
| 251 | + ( | |
| 252 | + "let builder = RustwardenApp::builder()\n", | |
| 253 | + r#" | |
| 169 | 254 | #[cfg(feature = "fediverse")] |
| 170 | 255 | let builder = { |
| 171 | 256 | use std::sync::Arc; |
@@ -190,20 +275,20 @@ pub fn main_rs(cfg: &ProjectConfig) -> String {
| 190 | 275 | .register_revocation_checker(checker) |
| 191 | 276 | .merge_routes(fediverse_router) |
| 192 | 277 | }; |
| 193 | - | |
| 194 | - builder | |
| 195 | 278 | "#, |
| 196 | - " .merge_routes(app_routes())\n .build()\n .await?\n .run()\n .await\n", | |
| 197 | - ) | |
| 198 | - } else { | |
| 199 | - ( | |
| 200 | - " RustwardenApp::builder()\n", | |
| 201 | - "", | |
| 202 | - " .merge_routes(app_routes())\n .build()\n .await?\n .run()\n .await\n", | |
| 203 | - ) | |
| 204 | - }; | |
| 279 | + fediverse_ui, | |
| 280 | + "\n builder\n .merge_routes(app_routes())\n .build()\n .await?\n .run()\n .await\n", | |
| 281 | + ) | |
| 282 | + } else { | |
| 283 | + ( | |
| 284 | + " RustwardenApp::builder()\n", | |
| 285 | + "", | |
| 286 | + "", | |
| 287 | + " .merge_routes(app_routes())\n .build()\n .await?\n .run()\n .await\n", | |
| 288 | + ) | |
| 289 | + }; | |
| 205 | 290 | |
| 206 | - let fediverse_use = if cfg.with_fediverse { | |
| 291 | + let fediverse_use = if cfg.has_fediverse() { | |
| 207 | 292 | "#[allow(unused_imports)]\nuse rustwarden_core::middleware::revocation::RemoteIdentityRevocationChecker;\n" |
| 208 | 293 | } else { |
| 209 | 294 | "" |
@@ -217,7 +302,7 @@ use sea_orm::DatabaseConnection;
| 217 | 302 | #[tokio::main] |
| 218 | 303 | async fn main() -> anyhow::Result<()> {{ |
| 219 | 304 | {builder_open} .from_env() |
| 220 | -{ui_line}{migrate_line}{email_line}{address_line}{fediverse_block}{builder_close}}} | |
| 305 | +{ui_line}{migrate_line}{email_line}{address_line}{fediverse_block}{fediverse_ui_block}{builder_close}}} | |
| 221 | 306 | |
| 222 | 307 | fn app_routes() -> Router<DatabaseConnection> {{ |
| 223 | 308 | Router::new() |
@@ -311,7 +396,7 @@ OAUTH_ENCRYPTION_KEY={encryption_key}
| 311 | 396 | } |
| 312 | 397 | |
| 313 | 398 | fn fediverse_env_section(cfg: &ProjectConfig) -> String { |
| 314 | - if cfg.with_fediverse { | |
| 399 | + if cfg.has_fediverse() { | |
| 315 | 400 | let domain = cfg |
| 316 | 401 | .fediverse_domain |
| 317 | 402 | .clone() |
M
tools/rustwarden-cli/src/update_project.rs
+34
-6
@@ -7,11 +7,15 @@ use dialoguer::{Confirm, Input};
| 7 | 7 | use crate::version; |
| 8 | 8 | |
| 9 | 9 | /// Fichiers et dossiers du framework qui sont remplaces lors de la mise a jour. |
| 10 | -/// `rustwarden-core` est toujours synchronise. `rustwarden-ui` et | |
| 11 | -/// `rustwarden-fediverse` ne sont synchronises que s'ils sont presents | |
| 12 | -/// dans le projet consommateur (detection en debut de `run()`). | |
| 13 | -const FRAMEWORK_CRATES: &[&str] = | |
| 14 | - &["rustwarden-core", "rustwarden-ui", "rustwarden-fediverse"]; | |
| 10 | +/// `rustwarden-core` est toujours synchronise. Les autres crates ne sont | |
| 11 | +/// synchronisees que si elles sont presentes dans le projet consommateur | |
| 12 | +/// (detection en debut de `run()`). | |
| 13 | +const FRAMEWORK_CRATES: &[&str] = &[ | |
| 14 | + "rustwarden-core", | |
| 15 | + "rustwarden-ui", | |
| 16 | + "rustwarden-fediverse", | |
| 17 | + "rustwarden-fediverse-ui", | |
| 18 | +]; | |
| 15 | 19 | |
| 16 | 20 | /// Fichiers du projet consommateur qu'on ne touche JAMAIS. |
| 17 | 21 | const PROTECTED_FILES: &[&str] = &[ |
@@ -46,9 +50,16 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 46 | 50 | |
| 47 | 51 | let has_ui = project_dir.join("crates/rustwarden-ui").exists(); |
| 48 | 52 | let has_fediverse = project_dir.join("crates/rustwarden-fediverse").exists(); |
| 53 | + let has_fediverse_ui = project_dir.join("crates/rustwarden-fediverse-ui").exists(); | |
| 49 | 54 | let custom_templates = detect_custom_templates(&project_dir, &rustwarden_root, has_ui); |
| 50 | 55 | |
| 51 | - print_update_plan(&project_dir, has_ui, has_fediverse, &custom_templates); | |
| 56 | + print_update_plan( | |
| 57 | + &project_dir, | |
| 58 | + has_ui, | |
| 59 | + has_fediverse, | |
| 60 | + has_fediverse_ui, | |
| 61 | + &custom_templates, | |
| 62 | + ); | |
| 52 | 63 | |
| 53 | 64 | if !Confirm::new() |
| 54 | 65 | .with_prompt(format!("{}", style("Proceder a la mise a jour ?").cyan())) |
@@ -68,6 +79,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| 68 | 79 | &project_dir, |
| 69 | 80 | has_ui, |
| 70 | 81 | has_fediverse, |
| 82 | + has_fediverse_ui, | |
| 71 | 83 | &custom_templates, |
| 72 | 84 | )?; |
| 73 | 85 |
@@ -222,6 +234,7 @@ fn print_update_plan(
| 222 | 234 | project_dir: &Path, |
| 223 | 235 | has_ui: bool, |
| 224 | 236 | has_fediverse: bool, |
| 237 | + has_fediverse_ui: bool, | |
| 225 | 238 | custom_templates: &[String], |
| 226 | 239 | ) { |
| 227 | 240 | println!("\n{}", style("── Plan de mise a jour ──").cyan().bold()); |
@@ -236,6 +249,9 @@ fn print_update_plan(
| 236 | 249 | if has_fediverse { |
| 237 | 250 | println!(" - crates/rustwarden-fediverse/"); |
| 238 | 251 | } |
| 252 | + if has_fediverse_ui { | |
| 253 | + println!(" - crates/rustwarden-fediverse-ui/"); | |
| 254 | + } | |
| 239 | 255 | |
| 240 | 256 | println!( |
| 241 | 257 | "\n {} :", |
@@ -409,6 +425,7 @@ fn update_crates(
| 409 | 425 | project_dir: &Path, |
| 410 | 426 | has_ui: bool, |
| 411 | 427 | has_fediverse: bool, |
| 428 | + has_fediverse_ui: bool, | |
| 412 | 429 | custom_templates: &[String], |
| 413 | 430 | ) -> Result<SyncStats, Box<dyn std::error::Error>> { |
| 414 | 431 | println!("\n{}", style("── Mise a jour des crates ──").cyan().bold()); |
@@ -446,6 +463,17 @@ fn update_crates(
| 446 | 463 | )?; |
| 447 | 464 | } |
| 448 | 465 | |
| 466 | + // rustwarden-fediverse-ui : uniquement si le projet l'utilise | |
| 467 | + if has_fediverse_ui { | |
| 468 | + sync_crate( | |
| 469 | + rustwarden_root, | |
| 470 | + project_dir, | |
| 471 | + "rustwarden-fediverse-ui", | |
| 472 | + &[], | |
| 473 | + &mut stats, | |
| 474 | + )?; | |
| 475 | + } | |
| 476 | + | |
| 449 | 477 | Ok(stats) |
| 450 | 478 | } |
| 451 | 479 |
GitRust