iron-cms 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/iron.css +97 -68
  3. data/app/assets/tailwind/iron/components/card.css +28 -26
  4. data/app/assets/tailwind/iron/components/selection-list.css +4 -0
  5. data/app/controllers/concerns/iron/api/cursor_pagination.rb +51 -0
  6. data/app/controllers/concerns/iron/api/locale_resolution.rb +23 -0
  7. data/app/controllers/concerns/iron/api/token_authentication.rb +29 -0
  8. data/app/controllers/concerns/iron/authorization.rb +4 -0
  9. data/app/controllers/iron/api/base_controller.rb +18 -0
  10. data/app/controllers/iron/api/content_controller.rb +75 -0
  11. data/app/controllers/iron/api/openapi_controller.rb +22 -0
  12. data/app/controllers/iron/api/search_controller.rb +15 -0
  13. data/app/controllers/iron/api/uploads_controller.rb +17 -0
  14. data/app/controllers/iron/passwords_controller.rb +8 -7
  15. data/app/controllers/iron/sessions/transfers_controller.rb +4 -2
  16. data/app/controllers/iron/sessions_controller.rb +4 -2
  17. data/app/controllers/iron/settings/integrations_controller.rb +34 -0
  18. data/app/controllers/iron/users/emails_controller.rb +5 -1
  19. data/app/controllers/iron/users/passwords_controller.rb +1 -1
  20. data/app/controllers/iron/users_controller.rb +9 -4
  21. data/app/javascript/iron/application.js +1 -1
  22. data/app/javascript/iron/controllers/index.js +2 -2
  23. data/app/javascript/iron/controllers/sortable_list_controller.js +1 -1
  24. data/app/mailers/iron/passwords_mailer.rb +2 -1
  25. data/app/models/iron/api/openapi_spec.rb +426 -0
  26. data/app/models/iron/current.rb +8 -0
  27. data/app/models/iron/entry/content_assignable.rb +53 -0
  28. data/app/models/iron/entry/deep_validation.rb +5 -1
  29. data/app/models/iron/entry/presentable.rb +1 -4
  30. data/app/models/iron/entry/searchable.rb +1 -2
  31. data/app/models/iron/entry.rb +1 -1
  32. data/app/models/iron/field.rb +27 -1
  33. data/app/models/iron/field_definition.rb +3 -1
  34. data/app/models/iron/fields/block.rb +24 -12
  35. data/app/models/iron/fields/block_list.rb +48 -7
  36. data/app/models/iron/fields/boolean.rb +4 -10
  37. data/app/models/iron/fields/date.rb +11 -10
  38. data/app/models/iron/fields/file.rb +11 -8
  39. data/app/models/iron/fields/number.rb +4 -10
  40. data/app/models/iron/fields/reference.rb +22 -5
  41. data/app/models/iron/fields/reference_list.rb +22 -11
  42. data/app/models/iron/fields/rich_text_area.rb +4 -24
  43. data/app/models/iron/fields/text_area.rb +4 -10
  44. data/app/models/iron/fields/text_field.rb +4 -8
  45. data/app/models/iron/first_run.rb +13 -3
  46. data/app/models/iron/integration.rb +15 -0
  47. data/app/models/iron/{user → person}/transferable.rb +1 -1
  48. data/app/models/iron/person.rb +19 -0
  49. data/app/models/iron/seed.rb +11 -5
  50. data/app/models/iron/user/role.rb +7 -1
  51. data/app/models/iron/user.rb +10 -14
  52. data/app/views/iron/api/_entry.json.jbuilder +25 -0
  53. data/app/views/iron/api/_pagination.json.jbuilder +3 -0
  54. data/app/views/iron/api/content/index.json.jbuilder +4 -0
  55. data/app/views/iron/api/content/show.json.jbuilder +1 -0
  56. data/app/views/iron/api/fields/_block.json.jbuilder +19 -0
  57. data/app/views/iron/api/fields/_block_list.json.jbuilder +21 -0
  58. data/app/views/iron/api/fields/_boolean.json.jbuilder +1 -0
  59. data/app/views/iron/api/fields/_date.json.jbuilder +1 -0
  60. data/app/views/iron/api/fields/_file.json.jbuilder +12 -0
  61. data/app/views/iron/api/fields/_number.json.jbuilder +1 -0
  62. data/app/views/iron/api/fields/_reference.json.jbuilder +8 -0
  63. data/app/views/iron/api/fields/_reference_list.json.jbuilder +6 -0
  64. data/app/views/iron/api/fields/_rich_text_area.json.jbuilder +4 -0
  65. data/app/views/iron/api/fields/_text_area.json.jbuilder +1 -0
  66. data/app/views/iron/api/fields/_text_field.json.jbuilder +1 -0
  67. data/app/views/iron/api/search/show.json.jbuilder +4 -0
  68. data/app/views/iron/passwords_mailer/reset.html.erb +1 -1
  69. data/app/views/iron/passwords_mailer/reset.text.erb +1 -1
  70. data/app/views/iron/settings/integrations/_integration.html.erb +29 -0
  71. data/app/views/iron/settings/integrations/_new_integration_dialog.html.erb +43 -0
  72. data/app/views/iron/settings/integrations/index.html.erb +63 -0
  73. data/app/views/iron/settings/show.html.erb +18 -0
  74. data/app/views/iron/users/_change_role_dialog.html.erb +1 -1
  75. data/app/views/iron/users/_transfer.html.erb +1 -1
  76. data/app/views/iron/users/_user.html.erb +1 -1
  77. data/app/views/iron/users/index.html.erb +1 -1
  78. data/app/views/iron/users/show.html.erb +3 -3
  79. data/config/importmap.rb +2 -2
  80. data/config/locales/en.yml +41 -0
  81. data/config/locales/it.yml +41 -0
  82. data/config/routes.rb +15 -0
  83. data/db/migrate/20260215222130_create_iron_people.rb +11 -0
  84. data/db/migrate/20260215222227_add_authenticatable_to_iron_users.rb +33 -0
  85. data/db/migrate/20260215222735_remove_legacy_auth_from_iron_users.rb +7 -0
  86. data/db/migrate/20260221231832_create_iron_integrations.rb +14 -0
  87. data/lib/iron/engine.rb +1 -0
  88. data/lib/iron/version.rb +1 -1
  89. data/lib/tasks/iron_tasks.rake +23 -0
  90. metadata +52 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6e038165d19792d4996c7ec0dba52cf3050c9047819f0857274a9d3adb5f190
4
- data.tar.gz: 1d6624acb03d0789ed4f7f984b3578237474a179610b4ab20632e89fda7a2e63
3
+ metadata.gz: bb7869fda3b54d6d01a68d88308c0b958709d4875da820646ca523623a78cce5
4
+ data.tar.gz: 108a5025a605c9cf7ccd3296c72b3565c8a69fee07d59139db064510aecdd581
5
5
  SHA512:
6
- metadata.gz: 91535cb2aa2fa21cdfe6b819b231ea70a447ec88145b0123564f84fa47c3e3c1ad4214a446ca59c4f1423496d644c6fb5207e292cc7d686f3bc5ef37ffbf37e2
7
- data.tar.gz: 63802328dc8911c7109a661daaf8742461459b3e0b929e73163736c0f86861b7cab9115f3ccb85802b20d0e6983baafc7d5c3a782b2408bf91ea50cffe83a010
6
+ metadata.gz: 5d6bad5f973d173ac571fe0cdeb976f282fee23f2407dde15c2b66f6b5875f1b2812e91dc0436871adc5183f044ed926fe2172835d3c06d17c53425825c575d4
7
+ data.tar.gz: 9a8f935b36a195dac33234696888f8736d293e46432442a1ca61fdb28fe460c6115e23bbfd21d7deff1925b4d79a21d2e150b8aa7be36d3357c83e265802bdd5
@@ -2958,6 +2958,9 @@
2958
2958
  }
2959
2959
  }
2960
2960
  }
2961
+ .bg-amber-100 {
2962
+ background-color: var(--color-amber-100);
2963
+ }
2961
2964
  .bg-gray-100 {
2962
2965
  background-color: var(--color-gray-100);
2963
2966
  }
@@ -3341,6 +3344,9 @@
3341
3344
  --tw-tracking: var(--tracking-widest);
3342
3345
  letter-spacing: var(--tracking-widest);
3343
3346
  }
3347
+ .text-amber-600 {
3348
+ color: var(--color-amber-600);
3349
+ }
3344
3350
  .text-current {
3345
3351
  color: currentcolor;
3346
3352
  }
@@ -4212,6 +4218,14 @@
4212
4218
  border-top-color: var(--color-stone-300);
4213
4219
  }
4214
4220
  }
4221
+ .dark\:bg-amber-900\/50 {
4222
+ @media (prefers-color-scheme: dark) {
4223
+ background-color: color-mix(in srgb, oklch(41.4% 0.112 45.904) 50%, transparent);
4224
+ @supports (color: color-mix(in lab, red, red)) {
4225
+ background-color: color-mix(in oklab, var(--color-amber-900) 50%, transparent);
4226
+ }
4227
+ }
4228
+ }
4215
4229
  .dark\:bg-green-500\/10 {
4216
4230
  @media (prefers-color-scheme: dark) {
4217
4231
  background-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 10%, transparent);
@@ -4339,6 +4353,11 @@
4339
4353
  --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
4340
4354
  }
4341
4355
  }
4356
+ .dark\:text-amber-400 {
4357
+ @media (prefers-color-scheme: dark) {
4358
+ color: var(--color-amber-400);
4359
+ }
4360
+ }
4342
4361
  .dark\:text-green-400 {
4343
4362
  @media (prefers-color-scheme: dark) {
4344
4363
  color: var(--color-green-400);
@@ -4764,87 +4783,89 @@
4764
4783
  }
4765
4784
  }
4766
4785
  }
4767
- .card {
4768
- background-color: var(--color-white);
4769
- @media (prefers-color-scheme: dark) {
4770
- background-color: color-mix(in srgb, oklch(26.8% 0.007 34.298) 50%, transparent);
4771
- @supports (color: color-mix(in lab, red, red)) {
4772
- background-color: color-mix(in oklab, var(--color-stone-800) 50%, transparent);
4786
+ @layer components {
4787
+ .card {
4788
+ background-color: var(--color-white);
4789
+ @media (prefers-color-scheme: dark) {
4790
+ background-color: color-mix(in srgb, oklch(26.8% 0.007 34.298) 50%, transparent);
4791
+ @supports (color: color-mix(in lab, red, red)) {
4792
+ background-color: color-mix(in oklab, var(--color-stone-800) 50%, transparent);
4793
+ }
4773
4794
  }
4774
- }
4775
- overflow: hidden;
4776
- border-radius: var(--radius-xl);
4777
- --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
4778
- --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
4779
- box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
4780
- --tw-ring-color: color-mix(in oklab, var(--color-stone-900) 5%, transparent);
4781
- @media (prefers-color-scheme: dark) {
4782
- --tw-ring-color: color-mix(in srgb, #fff 10%, transparent);
4783
- @supports (color: color-mix(in lab, red, red)) {
4784
- --tw-ring-color: color-mix(in oklab, var(--color-white) 10%, transparent);
4795
+ overflow: hidden;
4796
+ border-radius: var(--radius-xl);
4797
+ --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
4798
+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
4799
+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
4800
+ --tw-ring-color: color-mix(in oklab, var(--color-stone-900) 5%, transparent);
4801
+ @media (prefers-color-scheme: dark) {
4802
+ --tw-ring-color: color-mix(in srgb, #fff 10%, transparent);
4803
+ @supports (color: color-mix(in lab, red, red)) {
4804
+ --tw-ring-color: color-mix(in oklab, var(--color-white) 10%, transparent);
4805
+ }
4785
4806
  }
4786
4807
  }
4787
- }
4788
- .card-list {
4789
- :where(& > :not(:last-child)) {
4790
- --tw-divide-y-reverse: 0;
4791
- border-bottom-style: var(--tw-border-style);
4792
- border-top-style: var(--tw-border-style);
4793
- border-top-width: calc(1px * var(--tw-divide-y-reverse));
4794
- border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
4808
+ .card-list {
4809
+ :where(& > :not(:last-child)) {
4810
+ --tw-divide-y-reverse: 0;
4811
+ border-bottom-style: var(--tw-border-style);
4812
+ border-top-style: var(--tw-border-style);
4813
+ border-top-width: calc(1px * var(--tw-divide-y-reverse));
4814
+ border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
4815
+ }
4816
+ :where(& > :not(:last-child)) {
4817
+ border-color: var(--color-stone-100);
4818
+ }
4819
+ @media (prefers-color-scheme: dark) {
4820
+ :where(& > :not(:last-child)) {
4821
+ border-color: color-mix(in srgb, oklch(37.4% 0.01 67.558) 30%, transparent);
4822
+ @supports (color: color-mix(in lab, red, red)) {
4823
+ border-color: color-mix(in oklab, var(--color-stone-700) 30%, transparent);
4824
+ }
4825
+ }
4826
+ }
4795
4827
  }
4796
- :where(& > :not(:last-child)) {
4797
- border-color: var(--color-stone-100);
4828
+ .card:not(.card-list):has(.card-link) {
4829
+ position: relative;
4830
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
4831
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
4832
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
4798
4833
  }
4799
- @media (prefers-color-scheme: dark) {
4800
- :where(& > :not(:last-child)) {
4801
- border-color: color-mix(in srgb, oklch(37.4% 0.01 67.558) 30%, transparent);
4834
+ .card:not(.card-list):has(.card-link:hover) {
4835
+ background-color: var(--color-stone-50);
4836
+ @media (prefers-color-scheme: dark) {
4837
+ background-color: color-mix(in srgb, oklch(37.4% 0.01 67.558) 30%, transparent);
4802
4838
  @supports (color: color-mix(in lab, red, red)) {
4803
- border-color: color-mix(in oklab, var(--color-stone-700) 30%, transparent);
4839
+ background-color: color-mix(in oklab, var(--color-stone-700) 30%, transparent);
4804
4840
  }
4805
4841
  }
4806
4842
  }
4807
- }
4808
- .card:not(.card-list):has(.card-link) {
4809
- position: relative;
4810
- transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
4811
- transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
4812
- transition-duration: var(--tw-duration, var(--default-transition-duration));
4813
- }
4814
- .card:not(.card-list):has(.card-link:hover) {
4815
- background-color: var(--color-stone-50);
4816
- @media (prefers-color-scheme: dark) {
4817
- background-color: color-mix(in srgb, oklch(37.4% 0.01 67.558) 30%, transparent);
4818
- @supports (color: color-mix(in lab, red, red)) {
4819
- background-color: color-mix(in oklab, var(--color-stone-700) 30%, transparent);
4820
- }
4843
+ .card-link::after {
4844
+ content: "";
4845
+ position: absolute;
4846
+ inset: 0;
4821
4847
  }
4822
- }
4823
- .card-link::after {
4824
- content: "";
4825
- position: absolute;
4826
- inset: 0;
4827
- }
4828
- .card-item {
4829
- padding-inline: calc(var(--spacing) * 5);
4830
- padding-block: calc(var(--spacing) * 2.5);
4831
- transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
4832
- transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
4833
- transition-duration: var(--tw-duration, var(--default-transition-duration));
4834
- }
4835
- .card-item:has(.card-link) {
4836
- position: relative;
4837
- &:hover {
4838
- @media (hover: hover) {
4839
- background-color: var(--color-stone-50);
4840
- }
4848
+ .card-item {
4849
+ padding-inline: calc(var(--spacing) * 5);
4850
+ padding-block: calc(var(--spacing) * 2.5);
4851
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
4852
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
4853
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
4841
4854
  }
4842
- @media (prefers-color-scheme: dark) {
4855
+ .card-item:has(.card-link) {
4856
+ position: relative;
4843
4857
  &:hover {
4844
4858
  @media (hover: hover) {
4845
- background-color: color-mix(in srgb, oklch(37.4% 0.01 67.558) 30%, transparent);
4846
- @supports (color: color-mix(in lab, red, red)) {
4847
- background-color: color-mix(in oklab, var(--color-stone-700) 30%, transparent);
4859
+ background-color: var(--color-stone-50);
4860
+ }
4861
+ }
4862
+ @media (prefers-color-scheme: dark) {
4863
+ &:hover {
4864
+ @media (hover: hover) {
4865
+ background-color: color-mix(in srgb, oklch(37.4% 0.01 67.558) 30%, transparent);
4866
+ @supports (color: color-mix(in lab, red, red)) {
4867
+ background-color: color-mix(in oklab, var(--color-stone-700) 30%, transparent);
4868
+ }
4848
4869
  }
4849
4870
  }
4850
4871
  }
@@ -6920,6 +6941,14 @@ dialog.modal {
6920
6941
  text-overflow: ellipsis;
6921
6942
  white-space: nowrap;
6922
6943
  }
6944
+ .selection-list-description {
6945
+ font-size: var(--text-xs);
6946
+ line-height: var(--tw-leading, var(--text-xs--line-height));
6947
+ color: var(--color-stone-400);
6948
+ @media (prefers-color-scheme: dark) {
6949
+ color: var(--color-stone-500);
6950
+ }
6951
+ }
6923
6952
  .selection-list-handle {
6924
6953
  font-family: var(--font-mono);
6925
6954
  font-size: var(--text-xs);
@@ -1,33 +1,35 @@
1
- .card {
2
- @apply bg-white dark:bg-stone-800/50;
3
- @apply rounded-xl shadow-sm overflow-hidden;
4
- @apply ring-1 ring-stone-900/5 dark:ring-white/10;
5
- }
1
+ @layer components {
2
+ .card {
3
+ @apply bg-white dark:bg-stone-800/50;
4
+ @apply rounded-xl shadow-sm overflow-hidden;
5
+ @apply ring-1 ring-stone-900/5 dark:ring-white/10;
6
+ }
6
7
 
7
- .card-list {
8
- @apply divide-y divide-stone-100 dark:divide-stone-700/30;
9
- }
8
+ .card-list {
9
+ @apply divide-y divide-stone-100 dark:divide-stone-700/30;
10
+ }
10
11
 
11
- .card:not(.card-list):has(.card-link) {
12
- @apply relative transition-colors;
13
- }
12
+ .card:not(.card-list):has(.card-link) {
13
+ @apply relative transition-colors;
14
+ }
14
15
 
15
- .card:not(.card-list):has(.card-link:hover) {
16
- @apply bg-stone-50 dark:bg-stone-700/30;
17
- }
16
+ .card:not(.card-list):has(.card-link:hover) {
17
+ @apply bg-stone-50 dark:bg-stone-700/30;
18
+ }
18
19
 
19
- .card-link::after {
20
- content: "";
21
- position: absolute;
22
- inset: 0;
23
- }
20
+ .card-link::after {
21
+ content: "";
22
+ position: absolute;
23
+ inset: 0;
24
+ }
24
25
 
25
- .card-item {
26
- @apply px-5 py-2.5;
27
- @apply transition-colors;
28
- }
26
+ .card-item {
27
+ @apply px-5 py-2.5;
28
+ @apply transition-colors;
29
+ }
29
30
 
30
- .card-item:has(.card-link) {
31
- @apply relative;
32
- @apply hover:bg-stone-50 dark:hover:bg-stone-700/30;
31
+ .card-item:has(.card-link) {
32
+ @apply relative;
33
+ @apply hover:bg-stone-50 dark:hover:bg-stone-700/30;
34
+ }
33
35
  }
@@ -26,6 +26,10 @@
26
26
  @apply flex-1 min-w-0 truncate;
27
27
  }
28
28
 
29
+ .selection-list-description {
30
+ @apply text-xs text-stone-400 dark:text-stone-500;
31
+ }
32
+
29
33
  .selection-list-handle {
30
34
  @apply text-xs text-stone-400 dark:text-stone-500 font-mono;
31
35
  @apply shrink-0;
@@ -0,0 +1,51 @@
1
+ module Iron
2
+ module Api
3
+ module CursorPagination
4
+ extend ActiveSupport::Concern
5
+
6
+ DEFAULT_PER_PAGE = 25
7
+ MAX_PER_PAGE = 100
8
+
9
+ private
10
+
11
+ def paginate(scope)
12
+ per_page = (params[:per_page].presence || DEFAULT_PER_PAGE).to_i.clamp(1, MAX_PER_PAGE)
13
+ ordered = scope.order(updated_at: :desc, id: :desc)
14
+
15
+ records = if params[:after].present?
16
+ cursor = decode_cursor(params[:after])
17
+ ordered.where(
18
+ "(iron_entries.updated_at, iron_entries.id) < (?, ?)", cursor[:updated_at], cursor[:id]
19
+ ).limit(per_page + 1)
20
+ else
21
+ ordered.limit(per_page + 1)
22
+ end
23
+
24
+ has_more = records.size > per_page
25
+ records = records.first(per_page)
26
+ next_cursor = has_more ? encode_cursor(records.last) : nil
27
+
28
+ OpenStruct.new(
29
+ records: records,
30
+ pagination: OpenStruct.new(
31
+ per_page: per_page,
32
+ next_cursor: next_cursor,
33
+ has_more: has_more
34
+ )
35
+ )
36
+ end
37
+
38
+ def encode_cursor(record)
39
+ Base64.urlsafe_encode64("#{record.updated_at.iso8601(6)},#{record.id}", padding: false)
40
+ end
41
+
42
+ def decode_cursor(cursor)
43
+ decoded = Base64.urlsafe_decode64(cursor)
44
+ updated_at_str, id_str = decoded.split(",", 2)
45
+ { updated_at: Time.zone.parse(updated_at_str), id: id_str.to_i }
46
+ rescue ArgumentError
47
+ { updated_at: Time.current, id: 0 }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ module Iron
2
+ module Api
3
+ module LocaleResolution
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ before_action :set_locale
8
+ end
9
+
10
+ private
11
+
12
+ def set_locale
13
+ Current.locale = resolve_locale
14
+ end
15
+
16
+ def resolve_locale
17
+ if params[:locale].present?
18
+ Locale.find_by(code: params[:locale])
19
+ end || Locale.default
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module Iron
2
+ module Api
3
+ module TokenAuthentication
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include ActionController::HttpAuthentication::Token::ControllerMethods
8
+
9
+ before_action :authenticate_integration
10
+ end
11
+
12
+ private
13
+
14
+ def authenticate_integration
15
+ authenticate_with_http_token do |token|
16
+ integration = Integration.find_by(token: token)
17
+ return head(:unauthorized) unless integration
18
+ return head(:unauthorized) if integration.expired?
19
+
20
+ user = integration.user
21
+ return head(:unauthorized) unless user&.active?
22
+
23
+ Current.user = user
24
+ integration.touch(:last_used_at)
25
+ end || head(:unauthorized)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -4,6 +4,10 @@ module Iron
4
4
 
5
5
  private
6
6
 
7
+ def ensure_can_write
8
+ head :forbidden unless Current.user.can_write?
9
+ end
10
+
7
11
  def ensure_can_administer
8
12
  head :forbidden unless Current.user.can_administer?
9
13
  end
@@ -0,0 +1,18 @@
1
+ module Iron
2
+ module Api
3
+ class BaseController < ActionController::API
4
+ include ActiveStorage::SetCurrent
5
+ include TokenAuthentication, LocaleResolution, Authorization
6
+
7
+ rate_limit to: 60, within: 1.minute
8
+
9
+ rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
10
+
11
+ private
12
+
13
+ def render_not_found
14
+ render json: { error: "Not found" }, status: :not_found
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ module Iron
2
+ module Api
3
+ class ContentController < BaseController
4
+ include CursorPagination
5
+
6
+ before_action :set_content_type
7
+ before_action :set_entry, only: %i[show update destroy]
8
+ before_action :ensure_can_write, only: %i[create update destroy]
9
+ before_action :ensure_collection_content_type, only: :create
10
+
11
+ def index
12
+ if params.key?(:route)
13
+ @entry = @content_type.entries.find_by!(route: params[:route])
14
+ render :show
15
+ else
16
+ @collection = paginate(@content_type.entries)
17
+ end
18
+ end
19
+
20
+ def show
21
+ end
22
+
23
+ def create
24
+ @entry = @content_type.entries.build
25
+ @entry.route = params[:route] if params.key?(:route)
26
+ @entry.assign_content(content_params)
27
+
28
+ if @entry.save
29
+ render :show, status: :created, location: api_content_url(handle: @content_type.handle, id: @entry.id)
30
+ else
31
+ render json: @entry.content_errors, status: :unprocessable_entity
32
+ end
33
+ end
34
+
35
+ def update
36
+ @entry.route = params[:route] if params.key?(:route)
37
+ @entry.assign_content(content_params)
38
+
39
+ if @entry.save
40
+ render :show
41
+ else
42
+ render json: @entry.content_errors, status: :unprocessable_entity
43
+ end
44
+ end
45
+
46
+ def destroy
47
+ @entry.destroy!
48
+ head :no_content
49
+ end
50
+
51
+ private
52
+
53
+ def set_content_type
54
+ @content_type = ContentType.find_by!(handle: params[:handle])
55
+ end
56
+
57
+ def set_entry
58
+ @entry = if params[:id].blank?
59
+ raise ActiveRecord::RecordNotFound unless @content_type.single?
60
+ @content_type.entry
61
+ else
62
+ @content_type.entries.find(params[:id])
63
+ end
64
+ end
65
+
66
+ def content_params
67
+ params.fetch(:content, {}).permit!.to_h
68
+ end
69
+
70
+ def ensure_collection_content_type
71
+ head :method_not_allowed unless @content_type.collection?
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,22 @@
1
+ module Iron
2
+ module Api
3
+ class OpenapiController < BaseController
4
+ skip_before_action :authenticate_integration
5
+
6
+ def show
7
+ spec = Rails.cache.fetch(cache_key, expires_in: 1.hour) do
8
+ OpenapiSpec.new.to_h
9
+ end
10
+
11
+ render json: spec
12
+ end
13
+
14
+ private
15
+
16
+ def cache_key
17
+ latest = [ ContentType, FieldDefinition, BlockDefinition ].filter_map { |m| m.maximum(:updated_at) }.max
18
+ "iron/openapi/#{latest&.to_i}"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Iron
2
+ module Api
3
+ class SearchController < BaseController
4
+ include CursorPagination
5
+
6
+ def show
7
+ return render json: { error: "Missing required parameter: q" }, status: :bad_request if params[:q].blank?
8
+
9
+ scope = Entry.search(params[:q], locale: Current.locale)
10
+ .includes(:fields, content_type: :field_definitions)
11
+ @collection = paginate(scope)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Iron
2
+ module Api
3
+ class UploadsController < BaseController
4
+ before_action :ensure_can_write
5
+
6
+ def create
7
+ blob = ActiveStorage::Blob.create_and_upload!(
8
+ io: params.require(:file),
9
+ filename: params[:file].original_filename,
10
+ content_type: params[:file].content_type
11
+ )
12
+
13
+ render json: { signed_id: blob.signed_id }, status: :created
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,14 +3,14 @@ module Iron
3
3
  layout "iron/authentication"
4
4
  allow_unauthenticated_access
5
5
  before_action :require_email_configuration, only: %i[ new create ]
6
- before_action :set_user_by_token, only: %i[ edit update ]
6
+ before_action :set_person_by_token, only: %i[ edit update ]
7
7
 
8
8
  def new
9
9
  end
10
10
 
11
11
  def create
12
- if user = User.active.find_by(email_address: params[:email_address])
13
- Iron::PasswordsMailer.reset(user).deliver_later
12
+ if (person = Person.find_by(email_address: params[:email_address])) && person.active?
13
+ Iron::PasswordsMailer.reset(person.user).deliver_later
14
14
  end
15
15
 
16
16
  redirect_to sign_in_url, notice: t("iron.passwords.notifications.reset_instructions_sent")
@@ -20,7 +20,7 @@ module Iron
20
20
  end
21
21
 
22
22
  def update
23
- if @user.update(params.permit(:password, :password_confirmation))
23
+ if @person.update(params.permit(:password, :password_confirmation))
24
24
  redirect_to sign_in_url, notice: t("iron.passwords.notifications.reset_success")
25
25
  else
26
26
  redirect_to edit_password_url(params[:token]), alert: t("iron.passwords.alerts.passwords_mismatch")
@@ -34,9 +34,10 @@ module Iron
34
34
  end
35
35
  end
36
36
 
37
- def set_user_by_token
38
- @user = User.find_by_password_reset_token!(params[:token])
39
- redirect_to new_password_url, alert: t("iron.passwords.alerts.reset_link_invalid") unless @user.active?
37
+ def set_person_by_token
38
+ @person = Person.find_by_password_reset_token!(params[:token])
39
+ @user = @person.user
40
+ redirect_to new_password_url, alert: t("iron.passwords.alerts.reset_link_invalid") unless @person.active?
40
41
  rescue ActiveSupport::MessageVerifier::InvalidSignature
41
42
  redirect_to new_password_url, alert: t("iron.passwords.alerts.reset_link_invalid")
42
43
  end
@@ -9,8 +9,10 @@ module Iron
9
9
  end
10
10
 
11
11
  def update
12
- if (user = User.find_by_transfer_id(params[:id])) && user.active?
13
- start_new_session_for user
12
+ person = Person.find_by_transfer_id(params[:id])
13
+
14
+ if person&.active?
15
+ start_new_session_for person.user
14
16
  redirect_to after_authentication_url
15
17
  else
16
18
  redirect_to sign_in_url, alert: "Transfer link is invalid or has expired."
@@ -10,8 +10,10 @@ module Iron
10
10
  end
11
11
 
12
12
  def create
13
- if user = User.active.authenticate_by(params.permit(:email_address, :password))
14
- start_new_session_for user
13
+ person = Person.authenticate_by(params.permit(:email_address, :password))
14
+
15
+ if person&.active?
16
+ start_new_session_for person.user
15
17
  redirect_to after_authentication_url
16
18
  else
17
19
  redirect_to sign_in_url, alert: "Try another email address or password."