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