masks 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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>