studio-engine 0.84.0 → 0.86.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -0
- data/README.md +162 -1
- data/app/controllers/magic_links_controller.rb +1 -1
- data/app/controllers/registrations_controller.rb +4 -2
- data/app/helpers/studio/site_footer_helper.rb +9 -0
- data/app/views/registrations/new.html.erb +59 -36
- data/app/views/sessions/_sso_continue.html.erb +1 -1
- data/app/views/sessions/new.html.erb +6 -3
- data/app/views/studio/_confirm_interstitial.html.erb +1 -1
- data/app/views/studio/site_footer/_assets.html.erb +27 -159
- data/app/views/studio/site_footer/_map.html.erb +4 -4
- data/app/views/studio/site_footer/_map_assets.html.erb +157 -0
- data/docs/BOOKING.md +377 -0
- data/docs/SITE_FOOTER.md +375 -0
- data/lib/active_storage/service/studio_trash_s3_service.rb +64 -0
- data/lib/studio/auth_labels.rb +72 -0
- data/lib/studio/engine.rb +81 -2
- data/lib/studio/s3/trash.rb +248 -0
- data/lib/studio/s3.rb +56 -3
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +34 -0
- data/lib/tasks/studio_trash.rake +83 -0
- data/studio-engine.gemspec +8 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b6007443c9eeb21b16c521e76c956e5c05755ff0c264bbbf0f387f04ee79bd8
|
|
4
|
+
data.tar.gz: 1c6dd57eaf48aa3900de531b440766c8cb69281172f6c8a1aeb0daae45b7ca1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9fe2eb18d609180eeaaf03be1cf497bfc1b57407266565b8328757de5dd7f7c0633d5c5a81ab244cb39e4be9a211080a903c2d0490e181cfa07e89aca4838f97
|
|
7
|
+
data.tar.gz: 997135fe0ac9f3aea6d0da57c0d8af31b95e49320464fda777f441479250edf81af4bdbd4e93043a971d406e1779f819a7490707e733621e156ad95432c1c204
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,102 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.86.0 — 2026-10-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Footer-only consumers: an app with no ActiveRecord can take the site
|
|
12
|
+
footer alone, with no workarounds.** When `ActiveRecord::Railtie` is not
|
|
13
|
+
loaded (`Studio.active_record?` reads false) the engine requires the
|
|
14
|
+
ActiveSupport core extensions it calls at load time, skips the user-contract
|
|
15
|
+
check, keeps every `app/` root but `app/helpers` (the two `concerns` roots
|
|
16
|
+
included) out of eager loading, and answers
|
|
17
|
+
`studio_engine:install:migrations` with a no-op that exits 0 and writes
|
|
18
|
+
nothing. A host with ActiveRecord is unchanged. docs/SITE_FOOTER.md, *An app
|
|
19
|
+
with no database*, has the recipe; rantly can drop its four workarounds
|
|
20
|
+
(`require "active_support/core_ext/integer/time"`,
|
|
21
|
+
`Studio.validate_user_contract = false`, its `do_not_eager_load` loop, and its
|
|
22
|
+
replacement rake task).
|
|
23
|
+
- **docs/SITE_FOOTER.md and docs/BOOKING.md ship in the gem**, so a consumer's
|
|
24
|
+
citation of them resolves in the installed copy.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Footer links are painted at full opacity in `--ftr-link-ink`**, which
|
|
29
|
+
defaults to `--color-text-secondary`, instead of the inherited body colour at
|
|
30
|
+
78% opacity. On the default theme the links clear WCAG AA on the band in both
|
|
31
|
+
themes; an app that maps the tokens onto its own palette sets
|
|
32
|
+
`--color-text-secondary` (or `--ftr-link-ink`) to an ink that clears 4.5:1 on
|
|
33
|
+
its `--color-surface-alt` (docs/SITE_FOOTER.md, *Colours*). A refused href's
|
|
34
|
+
label (`.ftr-link-plain`) takes the same ink.
|
|
35
|
+
- **A footer with no address names no Leaflet.** The map's styles and mount
|
|
36
|
+
script moved from `studio/site_footer/_assets` to a new
|
|
37
|
+
`studio/site_footer/_map_assets`, rendered only with a map
|
|
38
|
+
(`studio_footer_map_assets`).
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **`/signup` honours `Studio.auth_methods`, as `/login` already did.** The
|
|
43
|
+
engine's `registrations/new` rendered Password, Confirm Password and "Sign up
|
|
44
|
+
with Google" whatever the app configured, while the passwordless
|
|
45
|
+
`RegistrationsController#create` reads the email alone. It now renders the
|
|
46
|
+
password form only when `Studio.password_login_available?`, otherwise the
|
|
47
|
+
email-only form (button "Send sign-in link", wording from
|
|
48
|
+
`Studio.sign_in_label`) when `:magic_link` is enabled, and the Google button and
|
|
49
|
+
its divider only with `:google`. The Name field renders only on the password
|
|
50
|
+
path, the one path that stores it; a passwordless signup discarded the name, so
|
|
51
|
+
it no longer asks. `#create` now branches on the same predicate the view does
|
|
52
|
+
(`password_login_available?` rather than `auth_method?(:password)`); for every
|
|
53
|
+
app that boots, the two agree. A password app renders exactly as before.
|
|
54
|
+
Consumers that wrote a host `app/views/registrations/new.html.erb` to hide the
|
|
55
|
+
password fields (McRitchie Industries) may delete it after the bump; the hub
|
|
56
|
+
and Turf Monster override it for their unified auth card and keep theirs.
|
|
57
|
+
|
|
58
|
+
## 0.85.0 — 2026-10-02
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- **`Studio::S3.delete` now moves the object to `trash/` instead of deleting
|
|
63
|
+
it.** The object is copied to `trash/<UTC date>/<epoch ms>/<key>` in the same
|
|
64
|
+
bucket, then deleted; a failed copy raises and never deletes. It returns the
|
|
65
|
+
trash key (nil when nothing was there) rather than the S3 response. The
|
|
66
|
+
bucket's lifecycle rule must expire `trash/` after three days, or trash never
|
|
67
|
+
expires: set it up before upgrading (README, *Trash and restore*). The
|
|
68
|
+
`/admin/emails` banner and logo replace and revert paths go through this
|
|
69
|
+
delete.
|
|
70
|
+
- **`Studio::S3.list` leaves `trash/` keys out.** Pass `include_trash: true` to
|
|
71
|
+
keep them. The filter runs after `max_keys`, so a page can come back short.
|
|
72
|
+
- **The engine's auth pages speak `Studio.sign_in_label`.** 0.80.0 put the label
|
|
73
|
+
on the navbar's signed-out buttons only, so an app that set `"Sign in"` still
|
|
74
|
+
showed "Log in to continue", a "Log In" button and a "Log in" link on its
|
|
75
|
+
`/login` and `/signup` pages. Those words, the magic-link button, the SSO
|
|
76
|
+
divider, the magic-link confirm page's button and the link-sent notice now
|
|
77
|
+
derive from the label (`lib/studio/auth_labels.rb`; README, *Navbar
|
|
78
|
+
identity*). An app that leaves the label at its default renders those pages
|
|
79
|
+
byte-identical to before. No routes change.
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **`Studio::S3.purge!(key:)`**, the hard delete, for objects with no value
|
|
84
|
+
after deletion.
|
|
85
|
+
- **A production-bucket guard.** `Studio::S3.delete`, `purge!`, and the trash
|
|
86
|
+
service's `delete` and `delete_prefixed` raise
|
|
87
|
+
`Studio::S3::Trash::ProductionBucketRefused` before any request when the
|
|
88
|
+
bucket ends in `-production` and the process is not production (`QA_ENV`
|
|
89
|
+
first, then `Rails.env`).
|
|
90
|
+
- **`ActiveStorage::Service::StudioTrashS3Service`** (`service: StudioTrashS3`
|
|
91
|
+
in `config/storage.yml`): Active Storage's S3 service whose `delete` trashes
|
|
92
|
+
the blob's object, recording the content type, byte size, checksum and
|
|
93
|
+
filename it can read off the object so a blob row can be rebuilt.
|
|
94
|
+
`delete_prefixed` (variants) stays a hard delete. Opt-in per app.
|
|
95
|
+
- **`rake studio:trash:list[KEY]` and `studio:trash:restore[TRASH_KEY]`**: find
|
|
96
|
+
a key's trash copies, and copy one back to its original key (`FORCE=1` to
|
|
97
|
+
overwrite, `SERVICE=<name>` for an Active Storage bucket). A restore prints the
|
|
98
|
+
`ActiveStorage::Blob.create!` that re-creates the blob row.
|
|
99
|
+
- **`Studio::S3::Trash`**, the shared copy-then-delete, key mapping, listing and
|
|
100
|
+
restore. A single CopyObject moves at most 5 GiB, so a larger object raises
|
|
101
|
+
`TooLarge` and is left in place.
|
|
102
|
+
|
|
7
103
|
## 0.84.0 — 2026-10-01
|
|
8
104
|
|
|
9
105
|
### Changed
|
data/README.md
CHANGED
|
@@ -73,6 +73,14 @@ Studio.configure do |config|
|
|
|
73
73
|
end
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
`auth_methods` draws both auth pages. `/login` and `/signup` render the password
|
|
77
|
+
form only when `:password` is enabled and the User has `authenticate`
|
|
78
|
+
(`Studio.password_login_available?`); otherwise they render the email-only
|
|
79
|
+
sign-in-link form when `:magic_link` is enabled, and the Google button only with
|
|
80
|
+
`:google`. A passwordless signup stores nothing from the form: it mails a link,
|
|
81
|
+
and using the link creates the account. So `registration_params`' `:name` shows a
|
|
82
|
+
Name field only on a password app.
|
|
83
|
+
|
|
76
84
|
### Local log rotation (automatic — nothing to configure)
|
|
77
85
|
|
|
78
86
|
The engine caps the host app's **development** log at 16 MB and its **test** log
|
|
@@ -629,7 +637,8 @@ Studio.configure do |config|
|
|
|
629
637
|
config.navbar_user_name = :player_name # a method on the user
|
|
630
638
|
# config.navbar_user_name = ->(user, view) { user.player_name } # or a callable
|
|
631
639
|
|
|
632
|
-
# The signed-out button in layouts/_navbar and components/_user_nav
|
|
640
|
+
# The signed-out button in layouts/_navbar and components/_user_nav, and the
|
|
641
|
+
# sign-in wording on the engine's own auth pages (below).
|
|
633
642
|
config.sign_in_label = "Sign in" # default "Log in"
|
|
634
643
|
end
|
|
635
644
|
```
|
|
@@ -646,6 +655,24 @@ end
|
|
|
646
655
|
- The engine has no I18n catalogue, so the label is plain config, not a locale
|
|
647
656
|
key. A blank or non-String label, or a `navbar_user_name` of any other type,
|
|
648
657
|
raises `Studio::NavbarIdentity::InvalidConfig` at assignment.
|
|
658
|
+
- The label also sets the sign-in words on the engine's own auth pages, so the
|
|
659
|
+
navbar and `/login` never disagree. Rules: `lib/studio/auth_labels.rb`.
|
|
660
|
+
|
|
661
|
+
| Where | `"Log in"` (default) | `"Sign in"` |
|
|
662
|
+
|-------|----------------------|-------------|
|
|
663
|
+
| `/login` prompt | Log in to continue | Sign in to continue |
|
|
664
|
+
| `/login` password button | Log In | Sign In |
|
|
665
|
+
| `/login` magic-link button | Send sign-in link | Send sign-in link |
|
|
666
|
+
| `/login` SSO divider | or sign in below | or sign in below |
|
|
667
|
+
| `/signup` link back to `/login` | Log in | Sign in |
|
|
668
|
+
| Magic-link confirm page button | Sign in to *App* | Sign in to *App* |
|
|
669
|
+
| Link-sent notice | …emailed you a sign-in link. | …emailed you a sign-in link. |
|
|
670
|
+
|
|
671
|
+
The default reproduces the pages' earlier wording byte for byte, including the
|
|
672
|
+
four places that already said "sign in". Any other label derives every form:
|
|
673
|
+
`"Log on"` gives *Log On*, *Log on to continue*, *or log on below* and
|
|
674
|
+
*log-on link*. The "Sign in with Google" and "Sign up" buttons are not
|
|
675
|
+
sign-in labels and do not change.
|
|
649
676
|
|
|
650
677
|
### User nav slots
|
|
651
678
|
|
|
@@ -920,6 +947,9 @@ set as a pair. R2 serves nothing anonymously from its S3 endpoint, so with an
|
|
|
920
947
|
endpoint and no `s3_public_url`, `Studio::S3.url` raises `NotConfigured` and
|
|
921
948
|
`upload` writes but returns `nil`; serve private objects with `signed_url`.
|
|
922
949
|
|
|
950
|
+
`Studio::S3.delete` moves the object to `trash/` for three days rather than
|
|
951
|
+
deleting it; see [Trash and restore](#trash-and-restore).
|
|
952
|
+
|
|
923
953
|
An app with **no** bucket configured does not error — `/admin/emails` renders
|
|
924
954
|
read-only, showing the inherited defaults it is genuinely sending and naming the
|
|
925
955
|
one setting that turns uploads on.
|
|
@@ -932,6 +962,137 @@ Add it to the app's admin sidebar section:
|
|
|
932
962
|
{ label: "Emails", href: admin_emails_path, emoji: "✉️", desc: "Transactional email banners" }
|
|
933
963
|
```
|
|
934
964
|
|
|
965
|
+
## Trash and restore
|
|
966
|
+
|
|
967
|
+
A delete through the engine is recoverable for three days. R2 has no object
|
|
968
|
+
versioning, so instead of deleting, the engine **moves** the object under
|
|
969
|
+
`trash/` in the same bucket, and a lifecycle rule on the bucket expires
|
|
970
|
+
`trash/` after three days:
|
|
971
|
+
|
|
972
|
+
```text
|
|
973
|
+
avatars/abc.png -> trash/2026-10-01/1759302000123/avatars/abc.png
|
|
974
|
+
trash/<UTC date>/<epoch ms>/<original key>
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
The copy goes first and the delete second. A copy that fails raises and the
|
|
978
|
+
delete is never sent, so a failure leaves the original in place. The trash copy
|
|
979
|
+
keeps the object's content type, cache headers and metadata, and adds
|
|
980
|
+
`original-key`, `deleted-at`, `deleted-env`, and what an Active Storage blob row
|
|
981
|
+
needs to be rebuilt (`blob-content-type`, `blob-byte-size`, `blob-checksum`
|
|
982
|
+
from a single-part ETag, and `blob-filename` when the upload carried a
|
|
983
|
+
Content-Disposition). A single CopyObject moves at most 5 GiB, so a larger
|
|
984
|
+
object raises `Studio::S3::Trash::TooLarge` and stays where it is.
|
|
985
|
+
|
|
986
|
+
`trash/` always sits at the bucket **root**, even for an app under a
|
|
987
|
+
`s3_key_prefix` in a shared bucket, so one lifecycle rule and one Cloudflare
|
|
988
|
+
rule cover every app in the bucket.
|
|
989
|
+
|
|
990
|
+
### What deletes, and what purges
|
|
991
|
+
|
|
992
|
+
| Call | Effect |
|
|
993
|
+
|------|--------|
|
|
994
|
+
| `Studio::S3.delete(key:)` | Moves to trash; returns the trash key (nil if nothing was there) |
|
|
995
|
+
| `Studio::S3.purge!(key:)` | Hard delete, gone at once |
|
|
996
|
+
| `Studio::S3.list` | Leaves `trash/` keys out; `include_trash: true` keeps them |
|
|
997
|
+
| Active Storage `service: StudioTrashS3`, `delete` | Moves the blob's object to trash (`Blob#purge`, a replaced attachment) |
|
|
998
|
+
| Active Storage `service: StudioTrashS3`, `delete_prefixed` | Hard delete: Active Storage calls it for `variants/<key>/`, which regenerate from the original |
|
|
999
|
+
|
|
1000
|
+
**The production guard.** All four destructive paths refuse a bucket whose name
|
|
1001
|
+
ends in `-production` when the process is not production, by `Studio::S3`'s own
|
|
1002
|
+
resolution (`QA_ENV` first, so a QA app running Rails as production is NOT
|
|
1003
|
+
production, then `Rails.env`). They raise
|
|
1004
|
+
`Studio::S3::Trash::ProductionBucketRefused` before sending any request. The
|
|
1005
|
+
guard reads the bucket NAME, so it protects only buckets that follow the
|
|
1006
|
+
`<app>-production` convention.
|
|
1007
|
+
|
|
1008
|
+
### Set up the bucket (once per bucket, before an app adopts)
|
|
1009
|
+
|
|
1010
|
+
Without the lifecycle rule, trash never expires. In the Cloudflare dashboard:
|
|
1011
|
+
R2 → the bucket → Settings → Object lifecycle rules → add a rule for prefix
|
|
1012
|
+
`trash/` that deletes objects 3 days after upload. A trash copy is a new object,
|
|
1013
|
+
so "uploaded" is the moment it was deleted. The same rule as S3 lifecycle JSON
|
|
1014
|
+
(R2's S3 API and AWS both take it). `aws s3api
|
|
1015
|
+
put-bucket-lifecycle-configuration` REPLACES the bucket's whole lifecycle
|
|
1016
|
+
configuration, R2's default abort-incomplete-multipart rule included, so read
|
|
1017
|
+
the current rules first (`get-bucket-lifecycle-configuration`) and send them
|
|
1018
|
+
together with this one, or use the dashboard:
|
|
1019
|
+
|
|
1020
|
+
```json
|
|
1021
|
+
{
|
|
1022
|
+
"Rules": [
|
|
1023
|
+
{
|
|
1024
|
+
"ID": "expire-trash-3d",
|
|
1025
|
+
"Status": "Enabled",
|
|
1026
|
+
"Filter": { "Prefix": "trash/" },
|
|
1027
|
+
"Expiration": { "Days": 3 }
|
|
1028
|
+
}
|
|
1029
|
+
]
|
|
1030
|
+
}
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
A public bucket also serves `trash/` from its custom domain unless it is
|
|
1034
|
+
blocked, which would keep a "deleted" profile picture reachable by URL for three
|
|
1035
|
+
days. Add a Cloudflare WAF custom rule on the zone, action **Block**:
|
|
1036
|
+
|
|
1037
|
+
```text
|
|
1038
|
+
(http.host in {"assets.example.com"} and starts_with(http.request.uri.path, "/trash/"))
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Name every `assets.<domain>` host the bucket answers on. If the bucket has the
|
|
1042
|
+
`r2.dev` public URL enabled, turn it off; a WAF rule cannot cover it.
|
|
1043
|
+
|
|
1044
|
+
### Adopt it in an app
|
|
1045
|
+
|
|
1046
|
+
Active Storage: change the R2 service in `config/storage.yml` from `service: S3`
|
|
1047
|
+
to `service: StudioTrashS3`. Every other key stays as it is:
|
|
1048
|
+
|
|
1049
|
+
```yaml
|
|
1050
|
+
r2:
|
|
1051
|
+
service: StudioTrashS3
|
|
1052
|
+
bucket: my-app-production
|
|
1053
|
+
endpoint: <%= ENV["R2_ENDPOINT"] %>
|
|
1054
|
+
region: auto
|
|
1055
|
+
access_key_id: <%= ENV["R2_ACCESS_KEY_ID"] %>
|
|
1056
|
+
secret_access_key: <%= ENV["R2_SECRET_ACCESS_KEY"] %>
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
An app with its own S3 service subclass (turf-monster's
|
|
1060
|
+
`ActiveStorage::Service::R2PublicService`) inherits from the trash service
|
|
1061
|
+
instead:
|
|
1062
|
+
|
|
1063
|
+
```ruby
|
|
1064
|
+
require "active_storage/service/studio_trash_s3_service"
|
|
1065
|
+
|
|
1066
|
+
module ActiveStorage
|
|
1067
|
+
class Service::R2PublicService < Service::StudioTrashS3Service
|
|
1068
|
+
# ...
|
|
1069
|
+
end
|
|
1070
|
+
end
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
`Studio::S3` callers need no change: `delete` trashes from this release on. Call
|
|
1074
|
+
`purge!` where an object truly has no value after deletion.
|
|
1075
|
+
|
|
1076
|
+
### Find and restore a deleted object
|
|
1077
|
+
|
|
1078
|
+
```bash
|
|
1079
|
+
bin/rails "studio:trash:list" # everything in trash
|
|
1080
|
+
bin/rails "studio:trash:list[avatars/abc.png]" # one key's trash copies
|
|
1081
|
+
bin/rails "studio:trash:restore[trash/2026-10-01/1759302000123/avatars/abc.png]"
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
Both read `Studio::S3`'s bucket. `SERVICE=<storage.yml service name>` reads an
|
|
1085
|
+
Active Storage service's bucket instead, e.g. `SERVICE=r2`. A restore copies the
|
|
1086
|
+
object back to its original key and leaves the trash copy for the lifecycle rule
|
|
1087
|
+
to expire. It refuses to overwrite an object already at the original key unless
|
|
1088
|
+
`FORCE=1`.
|
|
1089
|
+
|
|
1090
|
+
Restoring an Active Storage object restores its **bytes only**. The blob row was
|
|
1091
|
+
destroyed before the object was trashed, so the task prints the
|
|
1092
|
+
`ActiveStorage::Blob.create!(...)` to run, filled from the trash copy's metadata,
|
|
1093
|
+
and the record must be re-attached by hand. `filename` is known only when the
|
|
1094
|
+
upload carried a Content-Disposition; supply it otherwise.
|
|
1095
|
+
|
|
935
1096
|
## Overriding Views
|
|
936
1097
|
|
|
937
1098
|
This is a non-isolated engine -- app views at the same path automatically override engine views. For example, placing `app/views/sessions/new.html.erb` in the consuming app replaces the engine's login page.
|
|
@@ -28,7 +28,7 @@ class MagicLinksController < ApplicationController
|
|
|
28
28
|
end
|
|
29
29
|
respond_to do |format|
|
|
30
30
|
format.json { render json: { success: true } }
|
|
31
|
-
format.html { redirect_to login_path, notice: "Check your inbox — we just emailed you a
|
|
31
|
+
format.html { redirect_to login_path, notice: "Check your inbox — we just emailed you a #{Studio::AuthLabels.link_noun(Studio.sign_in_label)}." }
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -12,13 +12,15 @@ class RegistrationsController < ApplicationController
|
|
|
12
12
|
# straight from a signup POST would skip proof of email ownership, so we
|
|
13
13
|
# treat "sign up" as a magic-link request — the create-or-login link (which
|
|
14
14
|
# only fires after the recipient clicks it) does the account creation.
|
|
15
|
-
|
|
15
|
+
# Gated on the same predicate registrations/new renders the password form on,
|
|
16
|
+
# so the form a visitor sees is always the form this action reads.
|
|
17
|
+
unless Studio.password_login_available?
|
|
16
18
|
email = (params.dig(:user, :email) || params[:email]).to_s.strip.downcase
|
|
17
19
|
if email.match?(URI::MailTo::EMAIL_REGEXP)
|
|
18
20
|
token = issue_magic_link(email, nil)
|
|
19
21
|
Studio::Email.deliver(UserMailer, :magic_link, email, token, to: email)
|
|
20
22
|
end
|
|
21
|
-
return redirect_to login_path, notice: "Check your inbox — we just emailed you a
|
|
23
|
+
return redirect_to login_path, notice: "Check your inbox — we just emailed you a #{Studio::AuthLabels.link_noun(Studio.sign_in_label)}."
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
@user = User.new(user_params)
|
|
@@ -155,6 +155,15 @@ module Studio
|
|
|
155
155
|
render "studio/site_footer/assets"
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
+
# The map's styles and mount script, once per page. Only the map partial
|
|
159
|
+
# asks, so a page with no map names no Leaflet.
|
|
160
|
+
def studio_footer_map_assets
|
|
161
|
+
return if @_studio_footer_map_assets_rendered
|
|
162
|
+
|
|
163
|
+
@_studio_footer_map_assets_rendered = true
|
|
164
|
+
render "studio/site_footer/map_assets"
|
|
165
|
+
end
|
|
166
|
+
|
|
158
167
|
def studio_booking_assets
|
|
159
168
|
return if @_studio_booking_assets_rendered
|
|
160
169
|
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
words = Studio.app_name.split
|
|
4
4
|
last_word = words.pop
|
|
5
5
|
prefix = words.join(" ")
|
|
6
|
+
# /signup renders from the app's configured auth_methods, exactly as /login does
|
|
7
|
+
# (sessions/new.html.erb): the PASSWORD form only when passwords are available
|
|
8
|
+
# (Studio.password_login_available?), otherwise the passwordless email form when
|
|
9
|
+
# :magic_link is enabled, and the Google button only when :google is. The passwordless
|
|
10
|
+
# POST (RegistrationsController#create) reads the email alone and mails a sign-in link,
|
|
11
|
+
# so that form asks for no password and no name: neither would be stored.
|
|
12
|
+
password_signup = Studio.password_login_available?
|
|
13
|
+
email_signup = password_signup || Studio.auth_method?(:magic_link)
|
|
14
|
+
ask_name = password_signup && Studio.registration_params.include?(:name)
|
|
6
15
|
%>
|
|
7
16
|
|
|
8
17
|
<div class="min-h-[70vh] flex items-center justify-center">
|
|
@@ -24,56 +33,70 @@
|
|
|
24
33
|
</div>
|
|
25
34
|
<% end %>
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
<% if email_signup %>
|
|
37
|
+
<%= form_with model: @user, url: signup_path, method: :post do |f| %>
|
|
38
|
+
<% if ask_name %>
|
|
39
|
+
<div class="mb-5">
|
|
40
|
+
<label for="user_name" class="block text-sm text-secondary mb-2 font-medium">Name</label>
|
|
41
|
+
<%= f.text_field :name, id: "user_name", autofocus: true,
|
|
42
|
+
class: "input-field",
|
|
43
|
+
placeholder: "Your name" %>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
|
|
29
47
|
<div class="mb-5">
|
|
30
|
-
<label for="
|
|
31
|
-
<%= f.
|
|
48
|
+
<label for="user_email" class="block text-sm text-secondary mb-2 font-medium">Email</label>
|
|
49
|
+
<%= f.email_field :email, id: "user_email", autofocus: !ask_name,
|
|
32
50
|
class: "input-field",
|
|
33
|
-
placeholder: "
|
|
51
|
+
placeholder: "you@example.com" %>
|
|
34
52
|
</div>
|
|
35
|
-
<% end %>
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
<% if password_signup %>
|
|
55
|
+
<div class="mb-5">
|
|
56
|
+
<label for="user_password" class="block text-sm text-secondary mb-2 font-medium">Password</label>
|
|
57
|
+
<%= f.password_field :password, id: "user_password",
|
|
58
|
+
class: "input-field",
|
|
59
|
+
placeholder: "Choose a password" %>
|
|
60
|
+
</div>
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
<div class="mb-6">
|
|
63
|
+
<label for="user_password_confirmation" class="block text-sm text-secondary mb-2 font-medium">Confirm Password</label>
|
|
64
|
+
<%= f.password_field :password_confirmation, id: "user_password_confirmation",
|
|
65
|
+
class: "input-field",
|
|
66
|
+
placeholder: "Confirm your password" %>
|
|
67
|
+
</div>
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
<button type="submit" class="btn btn-primary btn-lg w-full">
|
|
70
|
+
Sign Up
|
|
71
|
+
</button>
|
|
72
|
+
<% else %>
|
|
73
|
+
<button type="submit" class="btn btn-primary btn-lg w-full">
|
|
74
|
+
Send <%= Studio::AuthLabels.link_noun(Studio.sign_in_label) %>
|
|
75
|
+
</button>
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
<p class="text-center text-secondary text-sm mt-4">We will email you a link that signs you in and creates your account.</p>
|
|
78
|
+
<% end %>
|
|
79
|
+
<% end %>
|
|
61
80
|
<% end %>
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
82
|
+
<% if Studio.auth_method?(:google) %>
|
|
83
|
+
<% if email_signup %>
|
|
84
|
+
<div class="relative my-6">
|
|
85
|
+
<div class="absolute inset-0 flex items-center"><div class="w-full border-t border-subtle"></div></div>
|
|
86
|
+
<div class="relative flex justify-center text-sm"><span class="bg-surface px-3 text-secondary">or</span></div>
|
|
87
|
+
</div>
|
|
88
|
+
<% end %>
|
|
67
89
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
<%= button_to "/auth/google_oauth2", method: :post, data: { turbo: false },
|
|
91
|
+
class: "btn btn-google btn-lg w-full gap-3" do %>
|
|
92
|
+
<%= render "components/google_logo" %>
|
|
93
|
+
Sign up with Google
|
|
94
|
+
<% end %>
|
|
72
95
|
<% end %>
|
|
73
96
|
|
|
74
97
|
<p class="text-center text-secondary text-sm mt-6">
|
|
75
98
|
Already have an account?
|
|
76
|
-
<%= link_to
|
|
99
|
+
<%= link_to Studio::AuthLabels.link(Studio.sign_in_label), login_path, class: "text-primary hover:text-primary-300 font-medium underline underline-offset-2" %>
|
|
77
100
|
</p>
|
|
78
101
|
</div>
|
|
79
102
|
</div>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<div class="flex items-center gap-3 mt-5">
|
|
13
13
|
<div class="flex-1 h-px border-t border-subtle"></div>
|
|
14
|
-
<span class="text-muted text-xs uppercase tracking-wider"
|
|
14
|
+
<span class="text-muted text-xs uppercase tracking-wider"><%= Studio::AuthLabels.or_below(Studio.sign_in_label) %></span>
|
|
15
15
|
<div class="flex-1 h-px border-t border-subtle"></div>
|
|
16
16
|
</div>
|
|
17
17
|
</div>
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
words = Studio.app_name.split
|
|
5
5
|
last_word = words.pop
|
|
6
6
|
prefix = words.join(" ")
|
|
7
|
+
# The sign-in wording follows Studio.sign_in_label, as the navbar does
|
|
8
|
+
# (lib/studio/auth_labels.rb); the default label prints the wording these pages always had.
|
|
9
|
+
sign_in_label = Studio.sign_in_label
|
|
7
10
|
%>
|
|
8
11
|
|
|
9
12
|
<div class="min-h-[70vh] flex items-center justify-center">
|
|
@@ -13,7 +16,7 @@
|
|
|
13
16
|
<img src="<%= logo_path %>" alt="<%= Studio.app_name %>" class="w-20 h-20 rounded-full shadow-lg mx-auto mb-4">
|
|
14
17
|
<% end %>
|
|
15
18
|
<h1 class="text-3xl font-extrabold text-heading tracking-tight"><%= prefix %> <span class="text-primary"><%= last_word %></span></h1>
|
|
16
|
-
<p class="text-secondary mt-2"
|
|
19
|
+
<p class="text-secondary mt-2"><%= Studio::AuthLabels.prompt(sign_in_label) %></p>
|
|
17
20
|
</div>
|
|
18
21
|
|
|
19
22
|
<% if has_sso %>
|
|
@@ -57,7 +60,7 @@
|
|
|
57
60
|
</div>
|
|
58
61
|
|
|
59
62
|
<button type="submit" class="btn btn-primary btn-lg w-full">
|
|
60
|
-
|
|
63
|
+
<%= Studio::AuthLabels.button(sign_in_label) %>
|
|
61
64
|
</button>
|
|
62
65
|
<% end %>
|
|
63
66
|
<% elsif Studio.auth_method?(:magic_link) %>
|
|
@@ -71,7 +74,7 @@
|
|
|
71
74
|
</div>
|
|
72
75
|
|
|
73
76
|
<button type="submit" class="btn btn-primary btn-lg w-full">
|
|
74
|
-
Send
|
|
77
|
+
Send <%= Studio::AuthLabels.link_noun(sign_in_label) %>
|
|
75
78
|
</button>
|
|
76
79
|
<% end %>
|
|
77
80
|
<% end %>
|