studio-engine 0.5.2 → 0.5.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +2 -2
- data/app/controllers/magic_links_controller.rb +19 -8
- data/app/controllers/registrations_controller.rb +1 -1
- data/app/jobs/studio/email_delivery_job.rb +9 -0
- data/app/models/studio/email_delivery.rb +46 -0
- data/app/views/magic_links/confirm.html.erb +96 -0
- data/db/migrate/20260614000000_create_studio_email_deliveries.rb +22 -0
- data/lib/studio/email.rb +45 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +11 -6
- data/studio-engine.gemspec +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf5f392cf1242e7e541db8ceb7d4ad1865ef2f71f3c787e582de4ac51fc6f836
|
|
4
|
+
data.tar.gz: 5498dc5d35fbe69c589ba197f3d6fadacbe2d30e93da8b226f0a7d5309ad5ec5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c38ce794988d826bc9f91ad47786f314ea94dfebdfb42164741ffe70379dfb912d3073ac74f61f96a2814157fdcb651293c6d5d3ea9ce9927567d982c153120
|
|
7
|
+
data.tar.gz: 6046671a635cef22c25af3231d3e562d258ab0fa45ab568d2255d2c734c44d3e27950acd49ead0bda282b17075def27a6ba6c8e35df14807b6df4e2575890d3c
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
6
6
|
|
|
7
7
|
No entries yet.
|
|
8
8
|
|
|
9
|
+
## v0.5.4 (2026-06-14)
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Engine magic links are now scanner-safe: emailed links land on an inert GET confirmation page, and the token is consumed only by the CSRF-protected POST from that page.
|
|
13
|
+
- Added the shared `magic_link_consume_path` route helper for consumer apps using engine-drawn auth routes.
|
|
14
|
+
|
|
15
|
+
## v0.5.3 (2026-06-14)
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **`Studio::Email.deliver`** — shared ActionMailer delivery entry point that uses an app-level `EmailDelivery` when present, the engine's namespaced durable outbox when installed, and raw `deliver_later` as a fallback.
|
|
19
|
+
- **`Studio::EmailDelivery` / `Studio::EmailDeliveryJob`** — namespaced durable delivery rows for apps that want shared audit, retry, and resend recovery without colliding with an existing top-level `EmailDelivery` model.
|
|
20
|
+
- **`studio_email_deliveries` migration template** — installable shared outbox table for new or migrating consumer apps.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Engine magic-link and passwordless signup controllers now send through `Studio::Email.deliver` instead of calling `deliver_later` directly.
|
|
24
|
+
|
|
9
25
|
## v0.5.2 (2026-06-13)
|
|
10
26
|
|
|
11
27
|
### Added
|
data/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Shared Rails engine for McRitchie apps. Provides authentication, error handling,
|
|
|
11
11
|
gem "studio-engine", "~> 0.5"
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Then `bundle install`. The current release is **v0.5.
|
|
14
|
+
Then `bundle install`. The current release is **v0.5.4**; see [`CHANGELOG.md`](./CHANGELOG.md) for the history.
|
|
15
15
|
|
|
16
16
|
> Published to RubyGems as of v0.4.0 (2026-05-17). New installs should use the RubyGems form, which the consumer Rails apps (`mcritchie-studio`, `turf-monster`) already use.
|
|
17
17
|
|
|
@@ -62,7 +62,7 @@ Rails.application.routes.draw do
|
|
|
62
62
|
end
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
This draws the enabled auth routes (`/login`, `/signup`, `/logout`, magic-link routes, Solana routes), OAuth callbacks, optional SSO routes, `/error_logs`, and `/admin/theme`.
|
|
65
|
+
This draws the enabled auth routes (`/login`, `/signup`, `/logout`, magic-link request/confirm/consume routes, Solana routes), OAuth callbacks, optional SSO routes, `/error_logs`, and `/admin/theme`. Magic-link emails point at the inert GET confirmation route; the single-use token is consumed only by the CSRF-protected POST to `magic_link_consume_path`.
|
|
66
66
|
|
|
67
67
|
## Overriding Views
|
|
68
68
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Unified create-or-login email magic link (the passwordless email path).
|
|
2
2
|
#
|
|
3
3
|
# POST /magic_link — request a link (email [, return_to])
|
|
4
|
-
# GET /magic_link/:token —
|
|
4
|
+
# GET /magic_link/:token — "Confirm sign-in" interstitial (does NOT consume)
|
|
5
|
+
# POST /magic_link/:token — consume it: log in OR create the account
|
|
5
6
|
#
|
|
6
7
|
# create-or-login: clicking the link IS proof of email ownership, so an email
|
|
7
8
|
# that collides with a Google/wallet-only account that was never email-verified
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
# sign_in_existing / sign_up_new building blocks.
|
|
15
16
|
class MagicLinksController < ApplicationController
|
|
16
17
|
skip_before_action :require_authentication
|
|
18
|
+
layout false, only: :confirm
|
|
17
19
|
|
|
18
20
|
# Respond uniformly for any well-formed email. Under create-or-login every
|
|
19
21
|
# address is "valid" (it logs in or signs up), so there is nothing to
|
|
@@ -22,7 +24,7 @@ class MagicLinksController < ApplicationController
|
|
|
22
24
|
email = params[:email].to_s.strip.downcase
|
|
23
25
|
if email.match?(URI::MailTo::EMAIL_REGEXP)
|
|
24
26
|
token = MagicLink.generate(email: email, return_to: safe_path(params[:return_to]))
|
|
25
|
-
UserMailer
|
|
27
|
+
Studio::Email.deliver(UserMailer, :magic_link, email, token, to: email)
|
|
26
28
|
end
|
|
27
29
|
respond_to do |format|
|
|
28
30
|
format.json { render json: { success: true } }
|
|
@@ -30,13 +32,22 @@ class MagicLinksController < ApplicationController
|
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
# GET /magic_link/:token is deliberately inert. Email link scanners and link
|
|
36
|
+
# preview clients frequently prefetch emailed URLs with GET/HEAD; if GET burned
|
|
37
|
+
# the token, the human's first real click could already be invalid. The page
|
|
38
|
+
# renders a CSRF-protected form that a browser auto-POSTs to #consume.
|
|
39
|
+
def confirm
|
|
40
|
+
# strict-origin strips the token-bearing path from subresource Referer
|
|
41
|
+
# headers while preserving a usable Origin header for Rails' CSRF origin
|
|
42
|
+
# check on the consume POST.
|
|
43
|
+
response.set_header("Referrer-Policy", "strict-origin")
|
|
44
|
+
@token = params[:token]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# POST /magic_link/:token is the authoritative consume. This is the only place
|
|
48
|
+
# the single-use token is burned.
|
|
33
49
|
def consume
|
|
34
|
-
|
|
35
|
-
# loads. Single-use + short TTL is the primary defence; this closes the
|
|
36
|
-
# passive-leak gap. NOTE: aggressive email link-scanners (Outlook SafeLinks,
|
|
37
|
-
# Mimecast) may pre-fetch the link and burn the single-use token before the
|
|
38
|
-
# human clicks — a known magic-link tradeoff; documented for support.
|
|
39
|
-
response.set_header("Referrer-Policy", "no-referrer")
|
|
50
|
+
response.set_header("Referrer-Policy", "strict-origin")
|
|
40
51
|
result = MagicLink.consume(params[:token])
|
|
41
52
|
user = User.find_by(email: result.email)
|
|
42
53
|
user ? sign_in_existing(user, result) : sign_up_new(result)
|
|
@@ -14,7 +14,7 @@ class RegistrationsController < ApplicationController
|
|
|
14
14
|
email = (params.dig(:user, :email) || params[:email]).to_s.strip.downcase
|
|
15
15
|
if email.match?(URI::MailTo::EMAIL_REGEXP)
|
|
16
16
|
token = MagicLink.generate(email: email)
|
|
17
|
-
UserMailer
|
|
17
|
+
Studio::Email.deliver(UserMailer, :magic_link, email, token, to: email)
|
|
18
18
|
end
|
|
19
19
|
return redirect_to login_path, notice: "Check your inbox — we just emailed you a sign-in link."
|
|
20
20
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Studio
|
|
2
|
+
class EmailDelivery < ApplicationRecord
|
|
3
|
+
self.table_name = "studio_email_deliveries"
|
|
4
|
+
|
|
5
|
+
belongs_to :user, optional: true
|
|
6
|
+
|
|
7
|
+
scope :unsent, -> { where(sent: false) }
|
|
8
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
9
|
+
|
|
10
|
+
def self.available?
|
|
11
|
+
connection.data_source_exists?(table_name)
|
|
12
|
+
rescue ActiveRecord::ActiveRecordError, NoMethodError
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.deliver(mailer, action, *args, to:, user: nil, **kwargs)
|
|
17
|
+
record = create!(
|
|
18
|
+
mailer: mailer.to_s,
|
|
19
|
+
action: action.to_s,
|
|
20
|
+
email_key: "#{mailer}##{action}",
|
|
21
|
+
to: to.to_s,
|
|
22
|
+
user: user,
|
|
23
|
+
args: ActiveJob::Arguments.serialize(args),
|
|
24
|
+
kwargs: ActiveJob::Arguments.serialize([kwargs]).first
|
|
25
|
+
)
|
|
26
|
+
Studio::EmailDeliveryJob.perform_later(record.id)
|
|
27
|
+
record
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def deliver_now!
|
|
31
|
+
return if sent?
|
|
32
|
+
|
|
33
|
+
pos = ActiveJob::Arguments.deserialize(args)
|
|
34
|
+
kw = ActiveJob::Arguments.deserialize([kwargs]).first.symbolize_keys
|
|
35
|
+
mailer.constantize.public_send(action, *pos, **kw).deliver_now
|
|
36
|
+
update!(sent: true, sent_at: Time.current, error: nil)
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
update(error: e.message.to_s.first(500))
|
|
39
|
+
raise
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.resend_unsent!
|
|
43
|
+
unsent.find_each { |delivery| Studio::EmailDeliveryJob.perform_later(delivery.id) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<title>Signing you in - <%= Studio.app_name %></title>
|
|
8
|
+
<style>
|
|
9
|
+
@keyframes magic-spin { to { transform: rotate(360deg); } }
|
|
10
|
+
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
display: grid;
|
|
17
|
+
place-items: center;
|
|
18
|
+
color: #f8fafc;
|
|
19
|
+
background: <%= Studio.theme_dark %>;
|
|
20
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main { width: min(100% - 32px, 420px); text-align: center; }
|
|
24
|
+
|
|
25
|
+
.magic-spinner {
|
|
26
|
+
width: 88px;
|
|
27
|
+
height: 88px;
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
border-radius: 9999px;
|
|
30
|
+
border: 8px solid rgba(255, 255, 255, 0.14);
|
|
31
|
+
border-top-color: <%= Studio.theme_success %>;
|
|
32
|
+
animation: magic-spin 0.8s linear infinite;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.magic-fallback {
|
|
36
|
+
display: none;
|
|
37
|
+
margin-top: 2rem;
|
|
38
|
+
color: rgba(248, 250, 252, 0.8);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.magic-fallback p { margin: 0 0 1rem; font-size: 15px; }
|
|
42
|
+
|
|
43
|
+
.magic-submit {
|
|
44
|
+
display: inline-block;
|
|
45
|
+
max-width: 100%;
|
|
46
|
+
padding: 14px 34px;
|
|
47
|
+
border: 0;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
color: #ffffff;
|
|
50
|
+
background: <%= Studio.theme_success %>;
|
|
51
|
+
font: inherit;
|
|
52
|
+
font-size: 16px;
|
|
53
|
+
font-weight: 700;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
<main>
|
|
60
|
+
<div class="magic-spinner" role="status" aria-label="Signing you in"></div>
|
|
61
|
+
|
|
62
|
+
<div id="magic-fallback" class="magic-fallback">
|
|
63
|
+
<p>Taking longer than expected?</p>
|
|
64
|
+
<%= button_to magic_link_consume_path(token: @token),
|
|
65
|
+
method: :post,
|
|
66
|
+
form: { id: "magic-consume-form", data: { turbo: "false" } },
|
|
67
|
+
class: "magic-submit" do %>
|
|
68
|
+
Sign in to <%= Studio.app_name %>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
71
|
+
</main>
|
|
72
|
+
|
|
73
|
+
<noscript>
|
|
74
|
+
<style>
|
|
75
|
+
#magic-fallback { display: block !important; }
|
|
76
|
+
.magic-spinner { display: none; }
|
|
77
|
+
</style>
|
|
78
|
+
</noscript>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
(function () {
|
|
82
|
+
var form = document.getElementById("magic-consume-form");
|
|
83
|
+
if (form && !form.dataset.autoSubmitted) {
|
|
84
|
+
form.dataset.autoSubmitted = "1";
|
|
85
|
+
if (typeof form.requestSubmit === "function") form.requestSubmit();
|
|
86
|
+
else form.submit();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setTimeout(function () {
|
|
90
|
+
var fallback = document.getElementById("magic-fallback");
|
|
91
|
+
if (fallback) fallback.style.display = "block";
|
|
92
|
+
}, 4000);
|
|
93
|
+
})();
|
|
94
|
+
</script>
|
|
95
|
+
</body>
|
|
96
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateStudioEmailDeliveries < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :studio_email_deliveries do |t|
|
|
4
|
+
t.string :email_key, null: false
|
|
5
|
+
t.string :to
|
|
6
|
+
t.string :mailer, null: false
|
|
7
|
+
t.string :action, null: false
|
|
8
|
+
t.jsonb :args, null: false, default: []
|
|
9
|
+
t.jsonb :kwargs, null: false, default: {}
|
|
10
|
+
t.boolean :sent, null: false, default: false
|
|
11
|
+
t.datetime :sent_at
|
|
12
|
+
t.text :error
|
|
13
|
+
t.references :user, foreign_key: true
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :studio_email_deliveries, :sent
|
|
19
|
+
add_index :studio_email_deliveries, :email_key
|
|
20
|
+
add_index :studio_email_deliveries, :created_at
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/studio/email.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
module Email
|
|
5
|
+
class << self
|
|
6
|
+
# Shared email send entry point for Studio apps.
|
|
7
|
+
#
|
|
8
|
+
# Apps with an existing top-level EmailDelivery model keep using it through
|
|
9
|
+
# this facade. Apps that have installed the engine outbox migration use the
|
|
10
|
+
# namespaced Studio::EmailDelivery model. Apps without either still send via
|
|
11
|
+
# ActionMailer's normal deliver_later path.
|
|
12
|
+
def deliver(mailer, action, *args, to:, user: nil, **kwargs)
|
|
13
|
+
if (adapter = app_delivery_adapter)
|
|
14
|
+
return adapter.deliver(mailer, action, *args, to: to, user: user, **kwargs)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if (adapter = studio_delivery_adapter)
|
|
18
|
+
return adapter.deliver(mailer, action, *args, to: to, user: user, **kwargs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
mailer.public_send(action, *args, **kwargs).deliver_later
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def app_delivery_adapter
|
|
27
|
+
return unless Object.const_defined?(:EmailDelivery, false)
|
|
28
|
+
|
|
29
|
+
adapter = Object.const_get(:EmailDelivery, false)
|
|
30
|
+
adapter if adapter.respond_to?(:deliver)
|
|
31
|
+
rescue NameError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def studio_delivery_adapter
|
|
36
|
+
return unless Studio.const_defined?(:EmailDelivery, false)
|
|
37
|
+
|
|
38
|
+
adapter = Studio.const_get(:EmailDelivery, false)
|
|
39
|
+
adapter if adapter.respond_to?(:available?) && adapter.available?
|
|
40
|
+
rescue NameError
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "studio/theme_resolver"
|
|
|
5
5
|
require "studio/username_generator"
|
|
6
6
|
require "studio/s3"
|
|
7
7
|
require "studio/image_cache"
|
|
8
|
+
require "studio/email"
|
|
8
9
|
require "studio/mail_transport"
|
|
9
10
|
|
|
10
11
|
module Studio
|
|
@@ -37,7 +38,7 @@ module Studio
|
|
|
37
38
|
mattr_accessor :draw_auth_routes, default: true
|
|
38
39
|
|
|
39
40
|
# Default From: for engine-sent mail (magic links). Apps set this to their
|
|
40
|
-
# verified
|
|
41
|
+
# verified sending address in config/initializers/studio.rb.
|
|
41
42
|
mattr_accessor :mailer_from, default: nil
|
|
42
43
|
|
|
43
44
|
# Theme role colors (7 roles)
|
|
@@ -158,12 +159,16 @@ module Studio
|
|
|
158
159
|
get "auth/failure", to: "omniauth_callbacks#failure"
|
|
159
160
|
|
|
160
161
|
# Passwordless email (magic link). Helpers: magic_link_request_path (POST
|
|
161
|
-
# to request a link)
|
|
162
|
-
#
|
|
163
|
-
#
|
|
162
|
+
# to request a link), magic_link_path(token) / magic_link_url(token:)
|
|
163
|
+
# for the emailed GET confirmation page, and magic_link_consume_path(token)
|
|
164
|
+
# for the scanner-safe POST consume. The token is a URL-safe
|
|
165
|
+
# MessageVerifier blob but the constraint guards against a stray "."
|
|
166
|
+
# segment.
|
|
164
167
|
if Studio.draw_auth_routes && Studio.auth_method?(:magic_link)
|
|
165
|
-
post "magic_link", to: "magic_links#create",
|
|
166
|
-
get "magic_link/:token", to: "magic_links#
|
|
168
|
+
post "magic_link", to: "magic_links#create", as: :magic_link_request
|
|
169
|
+
get "magic_link/:token", to: "magic_links#confirm", as: :magic_link,
|
|
170
|
+
constraints: { token: %r{[^/]+} }
|
|
171
|
+
post "magic_link/:token", to: "magic_links#consume", as: :magic_link_consume,
|
|
167
172
|
constraints: { token: %r{[^/]+} }
|
|
168
173
|
end
|
|
169
174
|
|
data/studio-engine.gemspec
CHANGED
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
"changelog_uri" => "https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md"
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", "tailwind/**/*", "Gemfile", "studio-engine.gemspec", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
21
|
+
spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", "db/**/*", "tailwind/**/*", "Gemfile", "studio-engine.gemspec", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
22
22
|
spec.require_paths = ["lib"]
|
|
23
23
|
|
|
24
24
|
spec.add_dependency "rails", ">= 7.0", "< 8.0"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -153,6 +153,7 @@ files:
|
|
|
153
153
|
- app/controllers/theme_settings_controller.rb
|
|
154
154
|
- app/helpers/studio_theme_helper.rb
|
|
155
155
|
- app/jobs/error_log_cleanup_job.rb
|
|
156
|
+
- app/jobs/studio/email_delivery_job.rb
|
|
156
157
|
- app/mailers/application_mailer.rb
|
|
157
158
|
- app/mailers/user_mailer.rb
|
|
158
159
|
- app/models/concerns/sluggable.rb
|
|
@@ -160,6 +161,7 @@ files:
|
|
|
160
161
|
- app/models/error_log.rb
|
|
161
162
|
- app/models/image_cache.rb
|
|
162
163
|
- app/models/session_context.rb
|
|
164
|
+
- app/models/studio/email_delivery.rb
|
|
163
165
|
- app/models/theme_setting.rb
|
|
164
166
|
- app/services/google_oauth_validator.rb
|
|
165
167
|
- app/services/magic_link.rb
|
|
@@ -182,6 +184,7 @@ files:
|
|
|
182
184
|
- app/views/layouts/_navbar.html.erb
|
|
183
185
|
- app/views/layouts/studio/_flash.html.erb
|
|
184
186
|
- app/views/layouts/studio/_head.html.erb
|
|
187
|
+
- app/views/magic_links/confirm.html.erb
|
|
185
188
|
- app/views/navbar/show.html.erb
|
|
186
189
|
- app/views/registrations/new.html.erb
|
|
187
190
|
- app/views/schema/index.html.erb
|
|
@@ -199,9 +202,11 @@ files:
|
|
|
199
202
|
- app/views/theme_settings/edit.html.erb
|
|
200
203
|
- app/views/user_mailer/magic_link.html.erb
|
|
201
204
|
- app/views/user_mailer/magic_link.text.erb
|
|
205
|
+
- db/migrate/20260614000000_create_studio_email_deliveries.rb
|
|
202
206
|
- lib/studio-engine.rb
|
|
203
207
|
- lib/studio.rb
|
|
204
208
|
- lib/studio/color_scale.rb
|
|
209
|
+
- lib/studio/email.rb
|
|
205
210
|
- lib/studio/engine.rb
|
|
206
211
|
- lib/studio/image_cache.rb
|
|
207
212
|
- lib/studio/mail_transport.rb
|