masks 0.2.0

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 (126) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +50 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/builds/application.css +4764 -0
  6. data/app/assets/builds/application.js +8236 -0
  7. data/app/assets/builds/application.js.map +7 -0
  8. data/app/assets/builds/masks/application.css +1 -0
  9. data/app/assets/builds/masks/application.js +7122 -0
  10. data/app/assets/builds/masks/application.js.map +7 -0
  11. data/app/assets/images/masks.png +0 -0
  12. data/app/assets/javascripts/application.js +2 -0
  13. data/app/assets/javascripts/controllers/application.js +9 -0
  14. data/app/assets/javascripts/controllers/emails_controller.js +28 -0
  15. data/app/assets/javascripts/controllers/index.js +12 -0
  16. data/app/assets/javascripts/controllers/keys_controller.js +20 -0
  17. data/app/assets/javascripts/controllers/recover_controller.js +21 -0
  18. data/app/assets/javascripts/controllers/recover_password_controller.js +21 -0
  19. data/app/assets/javascripts/controllers/session_controller.js +94 -0
  20. data/app/assets/manifest.js +2 -0
  21. data/app/assets/masks_manifest.js +2 -0
  22. data/app/assets/stylesheets/application.css +26 -0
  23. data/app/controllers/concerns/masks/controller.rb +114 -0
  24. data/app/controllers/masks/actors_controller.rb +15 -0
  25. data/app/controllers/masks/application_controller.rb +35 -0
  26. data/app/controllers/masks/backup_codes_controller.rb +34 -0
  27. data/app/controllers/masks/debug_controller.rb +9 -0
  28. data/app/controllers/masks/devices_controller.rb +20 -0
  29. data/app/controllers/masks/emails_controller.rb +60 -0
  30. data/app/controllers/masks/error_controller.rb +14 -0
  31. data/app/controllers/masks/keys_controller.rb +45 -0
  32. data/app/controllers/masks/manage/actor_controller.rb +35 -0
  33. data/app/controllers/masks/manage/actors_controller.rb +12 -0
  34. data/app/controllers/masks/manage/base_controller.rb +12 -0
  35. data/app/controllers/masks/one_time_code_controller.rb +49 -0
  36. data/app/controllers/masks/passwords_controller.rb +33 -0
  37. data/app/controllers/masks/recoveries_controller.rb +43 -0
  38. data/app/controllers/masks/sessions_controller.rb +53 -0
  39. data/app/helpers/masks/application_helper.rb +49 -0
  40. data/app/jobs/masks/application_job.rb +7 -0
  41. data/app/jobs/masks/expire_actors_job.rb +15 -0
  42. data/app/jobs/masks/expire_recoveries_job.rb +15 -0
  43. data/app/mailers/masks/actor_mailer.rb +22 -0
  44. data/app/mailers/masks/application_mailer.rb +15 -0
  45. data/app/models/concerns/masks/access.rb +162 -0
  46. data/app/models/concerns/masks/actor.rb +132 -0
  47. data/app/models/concerns/masks/adapter.rb +68 -0
  48. data/app/models/concerns/masks/role.rb +9 -0
  49. data/app/models/concerns/masks/scoped.rb +54 -0
  50. data/app/models/masks/access/actor_password.rb +20 -0
  51. data/app/models/masks/access/actor_scopes.rb +18 -0
  52. data/app/models/masks/access/actor_signup.rb +22 -0
  53. data/app/models/masks/actors/anonymous.rb +40 -0
  54. data/app/models/masks/actors/system.rb +24 -0
  55. data/app/models/masks/adapters/active_record.rb +85 -0
  56. data/app/models/masks/application_model.rb +15 -0
  57. data/app/models/masks/application_record.rb +8 -0
  58. data/app/models/masks/check.rb +192 -0
  59. data/app/models/masks/credential.rb +166 -0
  60. data/app/models/masks/credentials/backup_code.rb +30 -0
  61. data/app/models/masks/credentials/device.rb +59 -0
  62. data/app/models/masks/credentials/email.rb +48 -0
  63. data/app/models/masks/credentials/factor2.rb +71 -0
  64. data/app/models/masks/credentials/key.rb +38 -0
  65. data/app/models/masks/credentials/last_login.rb +12 -0
  66. data/app/models/masks/credentials/masquerade.rb +32 -0
  67. data/app/models/masks/credentials/nickname.rb +63 -0
  68. data/app/models/masks/credentials/one_time_code.rb +34 -0
  69. data/app/models/masks/credentials/password.rb +28 -0
  70. data/app/models/masks/credentials/recovery.rb +71 -0
  71. data/app/models/masks/credentials/session.rb +67 -0
  72. data/app/models/masks/device.rb +30 -0
  73. data/app/models/masks/error.rb +51 -0
  74. data/app/models/masks/event.rb +14 -0
  75. data/app/models/masks/mask.rb +255 -0
  76. data/app/models/masks/rails/actor.rb +190 -0
  77. data/app/models/masks/rails/actor_role.rb +12 -0
  78. data/app/models/masks/rails/device.rb +47 -0
  79. data/app/models/masks/rails/email.rb +96 -0
  80. data/app/models/masks/rails/key.rb +61 -0
  81. data/app/models/masks/rails/recovery.rb +116 -0
  82. data/app/models/masks/rails/role.rb +20 -0
  83. data/app/models/masks/rails/scope.rb +15 -0
  84. data/app/models/masks/session.rb +447 -0
  85. data/app/models/masks/sessions/access.rb +26 -0
  86. data/app/models/masks/sessions/inline.rb +16 -0
  87. data/app/models/masks/sessions/request.rb +42 -0
  88. data/app/resources/masks/actor_resource.rb +9 -0
  89. data/app/resources/masks/session_resource.rb +15 -0
  90. data/app/views/layouts/masks/application.html.erb +17 -0
  91. data/app/views/layouts/masks/mailer.html.erb +17 -0
  92. data/app/views/layouts/masks/mailer.text.erb +1 -0
  93. data/app/views/layouts/masks/manage.html.erb +25 -0
  94. data/app/views/masks/actor_mailer/recover_credentials.html.erb +33 -0
  95. data/app/views/masks/actor_mailer/recover_credentials.text.erb +1 -0
  96. data/app/views/masks/actor_mailer/verify_email.html.erb +34 -0
  97. data/app/views/masks/actor_mailer/verify_email.text.erb +8 -0
  98. data/app/views/masks/actors/current.html.erb +152 -0
  99. data/app/views/masks/application/_header.html.erb +31 -0
  100. data/app/views/masks/backup_codes/new.html.erb +103 -0
  101. data/app/views/masks/emails/new.html.erb +103 -0
  102. data/app/views/masks/emails/verify.html.erb +51 -0
  103. data/app/views/masks/keys/new.html.erb +127 -0
  104. data/app/views/masks/manage/actor/show.html.erb +126 -0
  105. data/app/views/masks/manage/actors/index.html.erb +40 -0
  106. data/app/views/masks/one_time_code/new.html.erb +150 -0
  107. data/app/views/masks/passwords/edit.html.erb +58 -0
  108. data/app/views/masks/recoveries/new.html.erb +71 -0
  109. data/app/views/masks/recoveries/password.html.erb +64 -0
  110. data/app/views/masks/sessions/new.html.erb +153 -0
  111. data/config/brakeman.ignore +28 -0
  112. data/config/locales/en.yml +286 -0
  113. data/config/routes.rb +46 -0
  114. data/db/migrate/20231205173845_create_actors.rb +94 -0
  115. data/lib/generators/masks/install/USAGE +8 -0
  116. data/lib/generators/masks/install/install_generator.rb +33 -0
  117. data/lib/generators/masks/install/templates/initializer.rb +5 -0
  118. data/lib/generators/masks/install/templates/masks.json +6 -0
  119. data/lib/masks/configuration.rb +236 -0
  120. data/lib/masks/engine.rb +25 -0
  121. data/lib/masks/middleware.rb +70 -0
  122. data/lib/masks/version.rb +5 -0
  123. data/lib/masks.rb +183 -0
  124. data/lib/tasks/masks_tasks.rake +71 -0
  125. data/masks.json +274 -0
  126. metadata +416 -0
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html class="h-full bg-base-100" data-theme="<%= Masks.configuration.theme %>">
3
+ <head>
4
+ <title><%= t(".meta.title") %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ <%= stylesheet_link_tag "masks/application", "data-turbo-track": "reload" %>
9
+ <%= javascript_include_tag "masks/application", "data-turbo-track": "reload", defer: true %>
10
+ </head>
11
+ <body class="h-screen flex px-2">
12
+ <div class="m-auto w-full max-w-screen-sm p-6 md:p-10 md:pt-8 pt-4 bg-base-300 rounded-2xl shadow">
13
+ <%= render "header" %>
14
+ <%= yield %>
15
+ </div>
16
+ </body>
17
+ </html>
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html class="bg-base-300" data-theme="dark">
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
+ <style>
6
+ /* TODO: mail styles need to be inlined (or integrate maizzle) */
7
+ </style>
8
+ </head>
9
+ <body>
10
+ <div class="px-6 pt-4">
11
+ <div class="-ml-2 mb-2">
12
+ <%= render "masks/application/header" %>
13
+ </div>
14
+ <%= yield %>
15
+ </div>
16
+ </body>
17
+ </html>
@@ -0,0 +1 @@
1
+ <%= yield %>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html class="h-full bg-base-200" data-theme="luxury">
3
+ <head>
4
+ <title><%= t(".meta.title") %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ <%= stylesheet_link_tag "masks/application", "data-turbo-track": "reload" %>
9
+ <%= javascript_include_tag "masks/application", "data-turbo-track": "reload", defer: true %>
10
+ </head>
11
+ <body class="h-full">
12
+ <div class="navbar my-2 join box-border px-6">
13
+ <div class="flex items-center gap-1 bg-accent rounded px-2 py-1.5 join-item">
14
+ <%= image_tag "masks.png", class: 'w-5 h-5 rounded-full border-black border' %>
15
+ </div>
16
+ <div class="flex items-center bg-base-100 rounded px-2 py-1.5 join-item">
17
+ <a href="<%= actors_path %>" class="text-sm text-accent-content opacity-75 hover:opacity-100 hover:underline focus:underline font-mono">/manage</a>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="w-full px-6 box-border">
22
+ <%= yield %>
23
+ </div>
24
+ </body>
25
+ </html>
@@ -0,0 +1,33 @@
1
+ <div class="flex flex-col gap-4">
2
+ <div role="alert" class="flex gap-4 items-center">
3
+ <%= t(".subheading") %>
4
+ </div>
5
+ <div class="card bg-neutral">
6
+ <div class="p-5">
7
+ <div class="flex items-center gap-4">
8
+ <div class="flex-grow flex flex-col">
9
+ <span class="font-bold">
10
+ <%= @recovery.actor.nickname %>
11
+ </span>
12
+ <span class="text-xs">
13
+ <%= t(".nickname") %>
14
+ </span>
15
+ </div>
16
+ <a
17
+ role="button"
18
+ href="<%= recover_password_url(token: @recovery)%>"
19
+ class="btn"
20
+ >
21
+ <%= lucide_icon("rotate-cw", class: "stroke-warning") %>
22
+ <%= t(".recover") %>
23
+ </a>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ <div>
28
+ <%= t(".call_to_action") %>
29
+ </div>
30
+ <div>
31
+ <%= t(".sign_off") %>
32
+ </div>
33
+ </div>
@@ -0,0 +1,34 @@
1
+ <div class="flex flex-col gap-4">
2
+ <div role="alert" class="flex gap-4 items-center">
3
+ <%= t(".subheading") %>
4
+ </div>
5
+ <div class="card bg-neutral">
6
+ <div class="p-5">
7
+ <div class="flex items-center gap-4">
8
+ <div class="flex-grow flex flex-col">
9
+ <span class="font-mono font-bold">
10
+ <%= @email.email %>
11
+ </span>
12
+ <span class="text-xs">
13
+ <%= t(".added") %>
14
+ <%= @email.created_at.strftime("on %B%e, %Y") %>
15
+ </span>
16
+ </div>
17
+ <a
18
+ role="button"
19
+ href="<%= email_verify_url(email: @email, approve: true)%>"
20
+ class="btn"
21
+ >
22
+ <%= lucide_icon("mail-check", class: "stroke-success") %>
23
+ <%= t(".verify") %>
24
+ </a>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ <div>
29
+ <%= t(".call_to_action") %>
30
+ </div>
31
+ <div>
32
+ <%= t(".sign_off") %>
33
+ </div>
34
+ </div>
@@ -0,0 +1,8 @@
1
+ <%= t('.subheading_plain') %>
2
+ > <%= @email.email %>
3
+ > <%= t('.added') %> <%= @email.created_at.strftime("on %B%e, %Y") %>
4
+
5
+ <%= t('.verify_plain') %><%= email_verify_url(email: @email)%>
6
+
7
+ <%= t('.call_to_action') %>
8
+ <%= t('.sign_off') %>
@@ -0,0 +1,152 @@
1
+ <div class="space-y-3">
2
+ <div class="join join-vertical w-full">
3
+ <div class="join-item alert bg-neutral flex items-center rounded-box">
4
+ <%= lucide_icon("user") %>
5
+ <div class="flex items-center gap-4 text-left w-full">
6
+ <div class="flex flex-col flex-grow">
7
+ <span class="font-bold">
8
+ <%= @actor.nickname %>
9
+ </span>
10
+
11
+ <span class="text-xs">
12
+ <% if @actor.last_login_at %>
13
+ <%= t(".last_login") %>
14
+ <%= time_ago_in_words(@actor.last_login_at) %> <%= t('.ago') %>&hellip;
15
+ <% else %>
16
+ <%= t(".created") %>
17
+ <%= time_ago_in_words(@actor.created_at) %> <%= t('.ago') %>&hellip;
18
+ <% end %>
19
+ </span>
20
+ </div>
21
+
22
+ <ul class="menu menu-horizontal p-0">
23
+ <li>
24
+ <details>
25
+ <summary class="font-bold">manage</summary>
26
+ <ul>
27
+ <li>
28
+ <%= form_with url: session_path, method: :delete do |form| %>
29
+ <%= form.submit t(".logout") %>
30
+ <% end %>
31
+ </li>
32
+ </ul>
33
+ </details>
34
+ </li>
35
+ </ul>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="join-item alert text-left flex items-center gap-4 <%= @actor.emails.any? ? 'bg-neutral' : 'bg-base-100' %>">
40
+ <%= lucide_icon("mail") %>
41
+ <span class="flex-grow">
42
+ <%= t(
43
+ ".emails",
44
+ count: @actor.email_addresses.length,
45
+ email: @actor.email_addresses[0],
46
+ ) %>
47
+ </span>
48
+
49
+ <a href="<%= emails_path %>" class="btn btn-sm btn-ghost hover:btn-info">
50
+ <%= t(@actor.emails.empty? ? ".add_email" : ".add_more_email") %>
51
+ </a>
52
+ </div>
53
+ </div>
54
+
55
+
56
+ <div class="divider text-xs">
57
+ <%= t(".credentials") %>
58
+ </div>
59
+
60
+ <div class="alert text-left bg-neutral flex items-center gap-4">
61
+ <%= lucide_icon("check-circle-2", class: "stroke-success") %>
62
+
63
+ <span class="flex-grow">
64
+ <%= t(".password") %>
65
+ </span>
66
+ <a
67
+ href="<%= password_path %>"
68
+ class="btn btn-sm btn-ghost hover:btn-secondary"
69
+ >
70
+ <%= t(".change") %>
71
+ </a>
72
+ </div>
73
+
74
+ <div class="alert text-left flex items-center gap-4 <%= @actor.totp_secret ? 'bg-neutral' : 'bg-base-100' %>">
75
+ <% if @actor.totp_secret %>
76
+ <%= lucide_icon("check-circle-2", class: "stroke-success") %>
77
+ <% else %>
78
+ <%= lucide_icon("minus-circle") %>
79
+ <% end %>
80
+
81
+ <span class="flex-grow">
82
+ <%= t(".one_time_codes") %>
83
+ </span>
84
+ <a
85
+ href="<%= one_time_codes_path %>"
86
+ class="btn btn-sm btn-ghost hover:btn-secondary"
87
+ >
88
+ <%= @actor.totp_secret ? t(".manage") : t(".enable") %>
89
+ </a>
90
+ </div>
91
+
92
+ <% if @actor.factor2? %>
93
+ <div
94
+ class="alert text-left flex items-center gap-4 <%= @actor.saved_backup_codes? ? 'bg-neutral' : 'bg-base-100' %>"
95
+ >
96
+ <% if @actor.saved_backup_codes? %>
97
+ <%= lucide_icon("check-circle-2", class: "stroke-success") %>
98
+ <% else %>
99
+ <%= lucide_icon("minus-circle") %>
100
+ <% end %>
101
+ <span class="flex-grow">
102
+ <%= t(".backup_codes") %>
103
+ </span>
104
+ <a
105
+ href="<%= backup_codes_path %>"
106
+ class="btn btn-sm btn-ghost hover:btn-secondary"
107
+ >
108
+ <%= @actor.saved_backup_codes? ? t(".manage") : t(".enable") %>
109
+ </a>
110
+ </div>
111
+ <% end %>
112
+
113
+ <div
114
+ class="alert text-left flex items-center gap-4 <%= @actor.keys.any? ? 'bg-neutral' : 'bg-base-100' %>"
115
+ >
116
+ <% if @actor.keys.any? %>
117
+ <%= lucide_icon("check-circle-2", class: "stroke-success") %>
118
+ <% else %>
119
+ <%= lucide_icon("minus-circle") %>
120
+ <% end %>
121
+ <span class="flex-grow">
122
+ <%= t(".keys") %>
123
+ </span>
124
+ <a
125
+ href="<%= keys_path %>"
126
+ class="btn btn-sm btn-ghost hover:btn-secondary"
127
+ >
128
+ <%= @actor.keys.any? ? t(".add_more_key") : t(".add_key") %>
129
+ </a>
130
+ </div>
131
+
132
+ <div class="divider text-xs"><%= t('.devices') %></div>
133
+
134
+ <% @actor.devices.each do |device| %>
135
+ <div class="flex items-center gap-4">
136
+ <%= lucide_icon(device_icon(device)) %>
137
+
138
+ <span class="flex-grow flex flex-col">
139
+ <span class="">
140
+ <%= device.name %> @ <span class="font-mono"><%= device.ip_address %></span>
141
+ </span>
142
+ <span class="text-xs">
143
+ <%= t('.last_accessed') %> <%= time_ago_in_words(device.accessed_at) %> <%= t('.ago') %>
144
+ </span>
145
+ </span>
146
+
147
+ <%= form_with url: device_path(key: device.key), method: :update do |form| %>
148
+ <%= form.submit t(".logout"), name: 'reset', class: 'btn btn-xs hover:btn-error focus:btn-error' %>
149
+ <% end %>
150
+ </div>
151
+ <% end %>
152
+ </div>
@@ -0,0 +1,31 @@
1
+ <div class="navbar gap-2 px-1">
2
+ <div class="flex-1">
3
+ <% if @config.site_url %>
4
+ <a
5
+ href="<%=@config.site_url%>"
6
+ class="text-xl flex items-center gap-3 hover:underline"
7
+ >
8
+ <% if @config.site_logo %>
9
+ <div class="w-7 h-7 overflow-hidden">
10
+ <img src="<%=@config.site_logo%>"/>
11
+ </div>
12
+ <% else %>
13
+ <%= lucide_icon("lock") %>
14
+ <% end %>
15
+ <span class="font-bold">
16
+ <%= @config.site_name %>
17
+ </span>
18
+ </a>
19
+ <% else %>
20
+ <span class="text-2xl"><%= @config.site_name %></span>
21
+ <% end %>
22
+ </div>
23
+ <% if logged_in? %>
24
+ <span class="whitespace-nowrap text-sm italic">
25
+ <%= t(".logged_in") %>
26
+ <a href="<%= current_path %>" class="not-italic underline pl-1">
27
+ <%= @session.actor.nickname %>
28
+ </a>
29
+ </span>
30
+ <% end %>
31
+ </div>
@@ -0,0 +1,103 @@
1
+ <div>
2
+ <div class="flex items-center gap-4 mb-2">
3
+ <div class="flex flex-col">
4
+ <h3 class="font-bold">
5
+ <%= t(".heading") %>
6
+ </h3>
7
+ <span class="text-xs">
8
+ <%= t(".tfa") %>
9
+
10
+ <span class="italic">
11
+ <%= @actor.saved_backup_codes? ? t(".enabled") : t(".disabled") %>
12
+ </span>
13
+ </span>
14
+ </div>
15
+ </div>
16
+
17
+ <% if !@actor.backup_codes %>
18
+ <div class="card bg-neutral p-4 mt-4 mb-3 flex flex-col gap-2">
19
+ <span class="font-bold flex items-center gap-4">
20
+ <%= lucide_icon("alert-triangle", class: "stroke-warning") %>
21
+ <span class="text-sm font-normal">
22
+ <%= t(".empty_detail") %>
23
+ </span>
24
+ </span>
25
+ </div>
26
+ <% elsif @actor.should_save_backup_codes? %>
27
+ <div class="divider mt-2 mb-0.5"></div>
28
+ <div class="flex gap-3 mb-4">
29
+ <span class="">
30
+ <%= t(".save_codes") %>
31
+ </span>
32
+ </div>
33
+
34
+ <div class="grid grid-cols-3 bg-base-100 p-4 my-3 mb-0 rounded-md shadow-inner">
35
+ <% @actor.backup_codes.each do |code, enabled| %>
36
+ <% if enabled %>
37
+ <span class="font-mono text-sm">
38
+ <%= code %>
39
+ </span>
40
+ <% end %>
41
+ <% end %>
42
+ </div>
43
+
44
+ <%= form_with url: backup_codes_path, method: :post, class: 'w-full' do |form| %>
45
+ <div class="divider my-2"></div>
46
+ <div class="flex items-center mb-2 gap-4">
47
+ <%= lucide_icon("shield-check") %>
48
+ <div class="flex items-center gap-4">
49
+ <input
50
+ type="password"
51
+ name="session[password]"
52
+ placeholder="<%= t('.placeholder.password') %>"
53
+ class="input w-full"
54
+ />
55
+ <%= form.submit t(".enable"),
56
+ name: "backup_codes[enable]",
57
+ class: "btn btn-secondary",
58
+ data: {
59
+ one_time_code_target: "submit",
60
+ } %>
61
+ </div>
62
+ </div>
63
+ <% end %>
64
+ <% else %>
65
+ <div class="alert bg-neutral my-4">
66
+ <%= lucide_icon("download", class: "stroke-success") %>
67
+
68
+ <span>
69
+ saved
70
+ <%= time_ago_in_words @actor.saved_backup_codes_at %>
71
+ ago...
72
+ </span>
73
+ </div>
74
+
75
+ <div class="join join-vertical">
76
+ <div class="join-item bg-base-100 flex gap-3 alert">
77
+ <span class="text-sm">
78
+ <%= t(".reset_codes") %>
79
+ </span>
80
+ </div>
81
+
82
+ <%= form_with url: backup_codes_path, method: :post, class: 'w-full' do |form| %>
83
+ <div class="alert join-item flex items-center gap-4">
84
+ <%= lucide_icon("shield-check") %>
85
+ <div class="flex items-center gap-4">
86
+ <input
87
+ type="password"
88
+ name="session[password]"
89
+ placeholder="<%= t('.placeholder.password') %>"
90
+ class="input input-sm w-full"
91
+ />
92
+ <%= form.submit t(".delete"),
93
+ name: "backup_codes[reset]",
94
+ class: "btn btn-sm btn-outline hover:btn-error",
95
+ data: {
96
+ one_time_code_target: "submit",
97
+ } %>
98
+ </div>
99
+ </div>
100
+ <% end %>
101
+ </div>
102
+ <% end %>
103
+ </div>
@@ -0,0 +1,103 @@
1
+ <div data-controller="emails" class="-mt-2">
2
+ <% if !@emails.any? %>
3
+ <div class="card bg-neutral p-4 mt-4 mb-3 flex flex-col gap-2">
4
+ <span class="font-bold flex items-center gap-4">
5
+ <%= lucide_icon("mail-question", class: "stroke-warning") %>
6
+ <%= t(".no_emails", count: @emails.length) %>
7
+ </span>
8
+ <div class="text-sm">
9
+ <%= t(".no_emails_detail") %>
10
+ </div>
11
+ </div>
12
+ <% else %>
13
+ <div class="mt-4 flex flex-col gap-2">
14
+ <span class="font-bold flex items-center gap-4">
15
+ <%= t(".heading_new", count: @emails.length) %>
16
+ </span>
17
+ </div>
18
+ <% end %>
19
+ <% if @emails.any? %>
20
+ <div class="flex flex-col gap-1">
21
+ <% @emails.each do |email| %>
22
+ <div class="divider my-1"></div>
23
+ <div class="flex items-center gap-4">
24
+ <%= lucide_icon(
25
+ email.verified? ? "mail-check" : "mail-question",
26
+ class: email.verified? ? "stroke-success" : "",
27
+ ) %>
28
+ <span class="flex-grow flex flex-col">
29
+ <span class="flex-grow font-mono">
30
+ <%= email.email %>
31
+ </span>
32
+ <span class="text-xs mb-1 flex items-center gap-1">
33
+ <% if email.verified? %>
34
+ <%= t(".email_verified", ago: time_ago_in_words(email.verified_at)) %>
35
+ <% elsif email.taken? %>
36
+ <%= t(".email_taken") %>
37
+ <% elsif email.expired? %>
38
+ <%= t(".expired_verification") %>
39
+ <%= form_with url: emails_path, method: :patch do |form| %>
40
+ <input type="hidden" name="email[value]" value="<%= email.email %>"/>
41
+ <button type="submit" class="underline">
42
+ <%= t(".resend") %>
43
+ </button>
44
+ <% end %>
45
+ <% else %>
46
+ <%= t(".pending_verification") %>
47
+ <% end %>
48
+ </span>
49
+ </span>
50
+ <%= form_with url: emails_path, method: :delete, class: 'flex items-center gap-4' do |form| %>
51
+ <input type="hidden" name="email[value]" value="<%= email.email %>"/>
52
+ <button type="submit">
53
+ <%= lucide_icon("x", class: "stroke-error") %>
54
+ </button>
55
+ <% end %>
56
+ </div>
57
+ <% end %>
58
+ </div>
59
+ <% end %>
60
+ <div class='divider my-2 text-xs'><%= t(".add") %></div>
61
+ <% if flash[:errors]&.any? %>
62
+ <div role="alert" class="alert mb-2">
63
+ <div class="flex items-center gap-4">
64
+ <%= lucide_icon("mail-x", class: "stroke-error") %>
65
+ <span><%= flash[:errors][0] %></span>
66
+ </div>
67
+ </div>
68
+ <% end %>
69
+ <div class=" <%= @emails.empty? ? 'mt-2' : '' %>">
70
+ <%= form_with url: emails_path, method: :post, class: 'pt-2 flex flex-col gap-2', data: { emails_target: 'add' } do |form| %>
71
+ <label class="flex gap-4 items-center">
72
+ <%= lucide_icon("mail-plus") %>
73
+ <input
74
+ type="text"
75
+ data-emails-target="email"
76
+ data-action="emails#updateEmail"
77
+ placeholder="<%= t('.placeholder.email', count: @emails.length) %>"
78
+ name="email[value]"
79
+ class="input w-full"
80
+ />
81
+ </label>
82
+ <div class="flex items-center gap-2">
83
+ <label class="flex gap-4 items-center">
84
+ <%= lucide_icon("shield-check") %>
85
+ <input
86
+ type="password"
87
+ data-emails-target="password"
88
+ data-action="emails#updatePassword"
89
+ placeholder="<%= t('.placeholder.password') %>"
90
+ name="session[password]"
91
+ class="input w-full"
92
+ />
93
+ </label>
94
+ <%= form.submit t(".submit"),
95
+ class: "btn btn-info",
96
+ data: {
97
+ emails_target: "submit",
98
+ },
99
+ disabled: true %>
100
+ </div>
101
+ <% end %>
102
+ </div>
103
+ </div>
@@ -0,0 +1,51 @@
1
+ <div>
2
+ <% if !@email || @email.expired? %>
3
+ <div class="alert alert-error text-error-content mb-2" role="alert">
4
+ <%= lucide_icon("mail-x") %>
5
+ <div>
6
+ <h3>
7
+ <%= t(".verification_expired") %>
8
+ </h3>
9
+ <h3 class="flex items-center gap-4 text-xs">
10
+ <span>
11
+ <a class="underline" href="<%= emails_path %>"><%= t(".emails") %></a>
12
+ <%= t(".manage_expired") %>
13
+ </span>
14
+ </h3>
15
+ </div>
16
+ </div>
17
+ <% elsif @email %>
18
+ <div class="alert alert-success mb-2" role="alert">
19
+ <%= lucide_icon("mail-check") %>
20
+ <div>
21
+ <h3 class="mb-1"><span class="font-mono"><%= @email.email %></span>
22
+ <%= t(".verified") %>
23
+ </h3>
24
+ <h3 class="flex items-center gap-4 text-xs">
25
+ <span>
26
+ <a class="underline" href="<%= emails_path %>"><%= t(".emails") %></a>
27
+ <%= t(".manage") %>
28
+ </span>
29
+ </h3>
30
+ </div>
31
+ </div>
32
+ <div class="card bg-base-100">
33
+ <div class="card-body">
34
+ <div class="card-title">
35
+ <%= t(".card_title") %>
36
+ </div>
37
+ <div>
38
+ <p class="mb-4 text-sm flex items-center gap-3">
39
+ <%= t(".details") %>
40
+ </p>
41
+ <ul class="text-sm list-disc pl-4 space-y-2 mb-4">
42
+ <%= t(".list_html") %>
43
+ </ul>
44
+ <p class="text-sm">
45
+ <%= t(".privacy") %>
46
+ </p>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <% end %>
51
+ </div>