trek 2.5.0 → 2.6.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/.node-version +1 -1
- data/AGENTS.md +7 -4
- data/CHANGELOG.md +37 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +16 -3
- data/Makefile +2 -0
- data/app/components/trek/dashboard/tile_component/tile_component.css +9 -0
- data/app/components/trek/dashboard/tile_component/tile_component.html.slim +4 -2
- data/app/components/trek/dashboard/tile_component.rb +12 -2
- data/app/components/trek/form/content_editor_component/content_editor_component.css +5 -0
- data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +19 -93
- data/app/components/trek/form/content_editor_component/content_editor_component.js +94 -251
- data/app/components/trek/form/content_editor_component/extensions/block_view.js +40 -0
- data/app/components/trek/form/content_editor_component/extensions/image_block.js +28 -26
- data/app/components/trek/form/content_editor_component/extensions/prompts_block.js +15 -26
- data/app/components/trek/form/content_editor_component/extensions/table_of_contents.js +1 -3
- data/app/components/trek/form/content_editor_component.rb +15 -0
- data/app/components/trek/form/image_field_component/image_field_component.css +40 -1
- data/app/components/trek/form/image_field_component/image_field_component.html.slim +0 -9
- data/app/components/trek/form/image_field_component.rb +36 -9
- data/app/content/trek/content/cleaner.rb +53 -0
- data/app/content/trek/content/document.rb +2 -2
- data/app/content/trek/content/prompts_block.rb +1 -2
- data/app/content/trek/content.rb +6 -0
- data/app/controllers/concerns/trek/reorderable.rb +24 -6
- data/app/controllers/trek/csp_reports_controller.rb +14 -1
- data/app/controllers/trek/panels/prompts_controller.rb +1 -1
- data/app/controllers/trek/resource_controller.rb +5 -5
- data/app/jobs/trek/purge_expired_sessions_job.rb +4 -3
- data/app/models/concerns/trek/contentable.rb +15 -1
- data/app/models/concerns/trek/users/sign_in_code.rb +70 -0
- data/app/models/concerns/trek/users/sign_in_lockout.rb +4 -2
- data/app/views/trek/panels/links/_form.html.slim +2 -4
- data/app/views/trek/panels/prompts/create.turbo_stream.slim +0 -1
- data/config/locales/trek.en.yml +26 -11
- data/config/locales/trek.fr.yml +26 -11
- data/docs/.vitepress/config.mjs +4 -1
- data/docs/background-jobs.md +69 -0
- data/docs/content-editor.md +219 -0
- data/docs/customize.md +2 -0
- data/docs/deploy.md +2 -2
- data/docs/getting-started.md +1 -1
- data/docs/index.md +1 -1
- data/docs/reference/components.md +2 -2
- data/docs/reference/concerns/contentable.md +10 -4
- data/docs/reference/concerns/formattable.md +1 -1
- data/docs/reference/environment-variables.md +1 -0
- data/docs/reference/formatters.md +2 -30
- data/docs/reference/forms.md +1 -1
- data/docs/reference/generators/audit-log.md +2 -2
- data/docs/reference/generators/install.md +3 -2
- data/docs/reference/generators/public-accounts.md +141 -0
- data/docs/reference/generators/security-headers.md +1 -1
- data/docs/reference/index.md +2 -1
- data/docs/reference/models/user.md +3 -1
- data/docs/what-is-trek.md +1 -1
- data/lib/generators/trek/audit_log_generator.rb +54 -0
- data/lib/generators/trek/install/admin/users_generator.rb +6 -1
- data/lib/generators/trek/install/audit_log_generator.rb +9 -45
- data/lib/generators/trek/install/good_job_generator.rb +161 -0
- data/lib/generators/trek/install/model_specs_generator.rb +3 -3
- data/lib/generators/trek/install/ruby_generator.rb +1 -1
- data/lib/generators/trek/install/scalingo_generator.rb +7 -45
- data/lib/generators/trek/install_generator.rb +15 -0
- data/lib/generators/trek/public_accounts_generator.rb +267 -0
- data/lib/generators/trek/scalingo_generator.rb +55 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
- data/lib/generators/trek/templates/controllers/admin/password_renewals_controller.rb +11 -0
- data/lib/generators/trek/templates/controllers/admin/user_sessions_controller.rb +1 -1
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +0 -8
- data/lib/generators/trek/templates/initializers/content_security_policy.rb +3 -2
- data/lib/generators/trek/templates/initializers/good_job.rb.tt +38 -0
- data/lib/generators/trek/templates/jobs/mail_delivery_job.rb +6 -0
- data/lib/generators/trek/templates/layouts/admin.html.slim +5 -0
- data/lib/generators/trek/templates/mailers/admin_mailer.rb +4 -2
- data/lib/generators/trek/templates/package.json.tt +1 -1
- data/lib/generators/trek/templates/public_accounts/api/authentication.rb.tt +76 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/accounts_controller.rb.tt +22 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/active_sessions_controller.rb.tt +21 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/base_controller.rb.tt +71 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/email_changes_controller.rb.tt +55 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/email_confirmations_controller.rb.tt +19 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/password_resets_controller.rb.tt +28 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/passwords_controller.rb.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/registrations_controller.rb.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/sessions_controller.rb.tt +45 -0
- data/lib/generators/trek/templates/public_accounts/api/controllers/sign_in_codes_controller.rb.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/jobs/sign_in_code_job.rb.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/locales/accounts.en.yml.tt +205 -0
- data/lib/generators/trek/templates/public_accounts/locales/accounts.fr.yml.tt +205 -0
- data/lib/generators/trek/templates/public_accounts/mailers/account_mailer.rb.tt +85 -0
- data/lib/generators/trek/templates/public_accounts/migrations/create_accounts.rb.tt +46 -0
- data/lib/generators/trek/templates/public_accounts/models/account/session.rb.tt +47 -0
- data/lib/generators/trek/templates/public_accounts/models/account/sign_in_code.rb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/models/account.rb.tt +73 -0
- data/lib/generators/trek/templates/public_accounts/spec/api/accounts_spec.rb.tt +90 -0
- data/lib/generators/trek/templates/public_accounts/spec/api/sign_ins_spec.rb.tt +158 -0
- data/lib/generators/trek/templates/public_accounts/spec/models/account/sign_in_code_spec.rb.tt +56 -0
- data/lib/generators/trek/templates/public_accounts/spec/models/account_spec.rb.tt +109 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/accounts_spec.rb.tt +126 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/authentication_spec.rb.tt +43 -0
- data/lib/generators/trek/templates/public_accounts/spec/web/sign_ins_spec.rb.tt +215 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.html.slim.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.en.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.html.slim.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_change_code.fr.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/email_confirmation.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.html.slim.tt +10 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.en.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.html.slim.tt +13 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/password_reset.fr.text.erb.tt +8 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.en.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.html.slim.tt +9 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_code.fr.text.erb.tt +11 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.html.slim.tt +23 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.en.text.erb.tt +14 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.html.slim.tt +24 -0
- data/lib/generators/trek/templates/public_accounts/views/mailer/sign_in_locked.fr.text.erb.tt +14 -0
- data/lib/generators/trek/templates/public_accounts/web/authentication.rb.tt +97 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/accounts_controller.rb.tt +33 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/active_sessions_controller.rb.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/base_controller.rb.tt +52 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/email_changes_controller.rb.tt +85 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/email_confirmations_controller.rb.tt +19 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/password_resets_controller.rb.tt +41 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/passwords_controller.rb.tt +23 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/registrations_controller.rb.tt +24 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/sessions_controller.rb.tt +78 -0
- data/lib/generators/trek/templates/public_accounts/web/controllers/sign_in_codes_controller.rb.tt +27 -0
- data/lib/generators/trek/templates/public_accounts/web/views/_flash.html.slim.tt +5 -0
- data/lib/generators/trek/templates/public_accounts/web/views/accounts/show.html.slim.tt +52 -0
- data/lib/generators/trek/templates/public_accounts/web/views/email_changes/edit.html.slim.tt +17 -0
- data/lib/generators/trek/templates/public_accounts/web/views/email_changes/new.html.slim.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/views/password_resets/edit.html.slim.tt +26 -0
- data/lib/generators/trek/templates/public_accounts/web/views/password_resets/new.html.slim.tt +15 -0
- data/lib/generators/trek/templates/public_accounts/web/views/passwords/edit.html.slim.tt +25 -0
- data/lib/generators/trek/templates/public_accounts/web/views/registrations/new.html.slim.tt +28 -0
- data/lib/generators/trek/templates/public_accounts/web/views/sessions/new.html.slim.tt +38 -0
- data/lib/generators/trek/templates/public_accounts/web/views/sign_in_codes/new.html.slim.tt +17 -0
- data/lib/generators/trek/templates/spec/jobs/mail_delivery_job_spec.rb +11 -0
- data/lib/generators/trek/templates/spec/mailers/admin_mailer_spec.rb +10 -0
- data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/good_job_spec.rb +34 -0
- data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/panels_spec.rb +16 -0
- data/lib/generators/trek/templates/spec/requests/admin/password_renewal_spec.rb +2 -2
- data/lib/generators/trek/templates/spec/requests/admin/sign_in_lockout_spec.rb +1 -1
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +2 -2
- data/lib/generators/trek/templates/views/admin/users/index.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.en.text.erb +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin_mailer/new_sign_in.fr.text.erb +1 -1
- data/lib/trek/csp_reports.rb +22 -4
- data/lib/trek/typography_formatter.rb +35 -4
- data/lib/trek/version.rb +1 -1
- data/package.json +12 -8
- data/trek.gemspec +1 -1
- data/vitest.config.mjs +24 -0
- data/yarn.lock +1004 -288
- metadata +90 -3
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Public accounts
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
rails g trek:public_accounts Customer [--sign-in=code|password] [--api] [--path=customers] [--model-only]
|
|
5
|
+
bin/rails db:migrate
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Accounts for the people using the site or the mobile app (customers, members, readers…), apart from the admin's [`User`](/reference/models/user). They have their own model, table, sessions and sign-in, so nothing public can reach a staff account: signing up, deleting inactive accounts or merging duplicates only ever sees public accounts, and a public session never opens the admin.
|
|
9
|
+
|
|
10
|
+
The name is required, and is the app's own: `User` is the admin's account, so the generator refuses it, a namespaced name, and the names of Trek's models. Pick the word of the app's domain (`Customer`, `Member`, `Reader`…).
|
|
11
|
+
|
|
12
|
+
| Option | Effect |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `--sign-in=code` (default) | Accounts sign in with a code emailed to them, which creates the account the first time: there is no password, nor sign-up form |
|
|
15
|
+
| `--sign-in=password` | Accounts sign up with an email and a password, confirm the email by a link, and reset a forgotten password by one: there are no codes |
|
|
16
|
+
| `--api` | A mobile app: JSON endpoints under `/api/v1`, and a session held through a JWT, with no pages. Without it, a website: pages, and a session kept in a cookie |
|
|
17
|
+
| `--path=customers` | Puts the routes under a path, e.g. `/customers/sign-in`: for an app with two kinds of public accounts |
|
|
18
|
+
| `--model-only` | Neither pages nor endpoints: see [Model only](#model-only). Can't be combined with `--api` or `--path` |
|
|
19
|
+
|
|
20
|
+
## Model only
|
|
21
|
+
|
|
22
|
+
With `--model-only`, the generator adds the models, their migration, `Current.customer_session` and `Current.customer`, the mailer, its job for codes, the locales and the model specs, but no controllers, pages, routes nor JWT. It's for an app whose sign-in is its own, e.g. an API ported from another app that its mobile app already calls: the app keeps its endpoints and tokens, on top of `Customer` and its rules (passwords, lockout, sessions, inactive accounts).
|
|
23
|
+
|
|
24
|
+
The app then creates and finds the `Customer::Session` rows itself, or ignores them, and sets `Current.customer_session`. With passwords, the emailed links point at the app's domain (`https://<host>/account/confirm?token=…`, `/password-reset?token=…`, `/account/email?token=…`), or at `CUSTOMERS_APP_LINKS_URL`, as for the API: the app's own pages or mobile app handle them.
|
|
25
|
+
|
|
26
|
+
## Signing in with an emailed code
|
|
27
|
+
|
|
28
|
+
1. The person types their email; the app emails them a 6-digit code. It answers the same whether or not the email has an account (ANSSI guide on passwords, R14).
|
|
29
|
+
2. They type the code: it signs them in to the email's account, created the first time. Signing up is signing in.
|
|
30
|
+
|
|
31
|
+
A code is valid for 10 minutes, for 5 tries (the right one included), and once. A new code replaces the earlier one. Only an HMAC of the code is stored, keyed on the app's `secret_key_base`, and the code is created in the job sending the email, so it's never in the job queue either. Change `validity` and `max_attempts` on `Customer::SignInCode`.
|
|
32
|
+
|
|
33
|
+
A new email address is confirmed the same way: a code sent to it.
|
|
34
|
+
|
|
35
|
+
## Signing in with a password
|
|
36
|
+
|
|
37
|
+
- **Signing up** takes an email and a password, following the same rules as the admin's (`Trek::Users::PasswordPolicy`: 12 characters, a digit and a symbol below 20…, configurable), and signs in at once. A link confirming the email is sent, valid a day; following it sets `confirmed_at`. The app decides what an unconfirmed account may do (`customer.confirmed?`), and the account page sends the link again.
|
|
38
|
+
- **Failed sign-ins** lock signing in as for the admin (`Trek::Users::SignInLockout`), and `CustomerMailer#sign_in_locked` tells the account.
|
|
39
|
+
- **A forgotten password** is reset by a link, valid 15 minutes and used up by the new password (`has_secure_password`'s `password_reset_token`), which signs out every session. Asking for one answers the same whether or not the email has an account.
|
|
40
|
+
- **Changing the password** needs a recent sign-in (15 minutes, `Customer::Session::RECENT`), and signs out the other sessions.
|
|
41
|
+
- **A new email address** is kept in `unconfirmed_email` until the link sent to it, valid an hour, is followed.
|
|
42
|
+
|
|
43
|
+
## Rate limits
|
|
44
|
+
|
|
45
|
+
Against guessing and flooding, each IP address may try to sign in 10 times in 3 minutes (5 with a password) and fail 20 times an hour, and ask for 10 emails (codes, links) in 15 minutes; each address gets at most 5 emails an hour. The limits are counted in `Rails.cache`, which must be shared between servers (see [Failed sign-ins](/reference/models/user#failed-sign-ins)).
|
|
46
|
+
|
|
47
|
+
## Sessions
|
|
48
|
+
|
|
49
|
+
| | Website | API (`--api`) |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| Held by | a signed `customer_session_id` cookie (`HttpOnly`, `SameSite=Lax`) | a JWT the app sends as `Authorization: Bearer …` |
|
|
52
|
+
| Expires | after 30 days without a request, and a year after signing in | the same, checked on the session row: the JWT only names it |
|
|
53
|
+
| Signing out | deletes the session and the cookie | deletes the session, and the JWT stops working |
|
|
54
|
+
|
|
55
|
+
Both are `Customer::Session` rows (`customer_sessions`), listed on the account, where the person can sign out the others. Change `idle_timeout` and `lifetime` on `Customer::Session` ([Trek::Users::SessionLifetime](/reference/models/user#sessions)). The JWT is signed with HS256 and a key derived from `secret_key_base`, and carries the session's id (`sub`) and the end of its lifetime (`exp`): unlike a self-contained token, it can be revoked.
|
|
56
|
+
|
|
57
|
+
Deleting the account, and changing the password, need a sign-in less than 15 minutes old; otherwise the website sends the person to sign in again, and the API answers 403 with `recent_sign_in_needed`.
|
|
58
|
+
|
|
59
|
+
Schedule the purge of expired sessions daily, as for the admin's: `Trek::PurgeExpiredSessionsJob.perform_later("Customer::Session")`.
|
|
60
|
+
|
|
61
|
+
The admin's own session (`session_id`, `User::Session`) is untouched: someone may be signed in to both, as two accounts that can share an email address.
|
|
62
|
+
|
|
63
|
+
## What the generator adds
|
|
64
|
+
|
|
65
|
+
- `Customer` and `Customer::Session`, `Customer::SignInCode` for codes, and a migration creating their tables
|
|
66
|
+
- `Current.customer_session`, and `Current.customer`
|
|
67
|
+
- `Customers::Authentication`, and the controllers: under `Customers::` with pages for a website, under `Api::V1::Customers::` for the API
|
|
68
|
+
- `CustomerMailer` with its views in English and French, and `Customers::SignInCodeJob` for codes
|
|
69
|
+
- `config/locales/customers.{en,fr}.yml`
|
|
70
|
+
- the routes, on paths of their own (below), so renaming the model keeps them
|
|
71
|
+
- specs, when the app has a `spec` directory
|
|
72
|
+
|
|
73
|
+
### Website
|
|
74
|
+
|
|
75
|
+
`Customers::Authentication` is included in `ApplicationController`, so every page may use:
|
|
76
|
+
|
|
77
|
+
| Helper | Returns |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `current_customer`, `customer_signed_in?` | the signed-in customer, if any |
|
|
80
|
+
| `staff_user` | the admin's user signed in in this browser, when privileged, for previews and links to the admin on the site. Read only: it neither extends nor ends their admin session, and never signs a customer in |
|
|
81
|
+
|
|
82
|
+
A page for customers only calls `before_action :require_customer`, which sends the others to sign in and brings them back after.
|
|
83
|
+
|
|
84
|
+
| Path | Page |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `GET /sign-in` | the sign-in form (`customer_sign_in_path`): the email for a code, or the email and password |
|
|
87
|
+
| `GET /sign-in/code` | the code form (codes) |
|
|
88
|
+
| `GET /sign-up` | the sign-up form (passwords) |
|
|
89
|
+
| `GET /password-reset/new` | asking for a link to choose a new password (passwords) |
|
|
90
|
+
| `GET /account/confirm?token=…` | the link confirming the email (passwords) |
|
|
91
|
+
| `DELETE /sign-out` | signs out |
|
|
92
|
+
| `GET /account` | the account: name, email, password, signed-in browsers, deletion (`customer_account_path`) |
|
|
93
|
+
| `GET /account/email/new` | changing the email, confirmed by a code or a link sent to the new address |
|
|
94
|
+
| `GET /account/password/edit` | changing the password (passwords) |
|
|
95
|
+
|
|
96
|
+
The pages render in the site's layout, unstyled, with their notices in `app/views/customers/_flash.html.slim`: move those to the layout if it shows flash messages.
|
|
97
|
+
|
|
98
|
+
### API
|
|
99
|
+
|
|
100
|
+
The endpoints are the API's first version, under `/api/v1`, so that a later version can be served next to them while older releases of the mobile app still call these. Every endpoint answers JSON; an error comes as `{ "error": "invalid_code", "message": "…" }`, the message in the request's locale.
|
|
101
|
+
|
|
102
|
+
| Endpoint | Body | Answer |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `POST /api/v1/sign-in` | codes: `email`; passwords: `email`, `password` | codes: 202, a code is emailed; passwords: 201, `{ token, customer }` |
|
|
105
|
+
| `POST /api/v1/sign-in/code` | `email`, `otp` | 201, `{ token, customer }` (codes) |
|
|
106
|
+
| `POST /api/v1/sign-up` | `email`, `password`, `name` | 201, `{ token, customer }` (passwords) |
|
|
107
|
+
| `POST /api/v1/password-reset` | `email` | 202, a link is emailed if the email has an account (passwords) |
|
|
108
|
+
| `PATCH /api/v1/password-reset` | `token`, `password` | 204 (passwords) |
|
|
109
|
+
| `POST`, `PATCH /api/v1/account/confirmation` | `token` for `PATCH` | sends the confirmation link again; confirms the email (passwords) |
|
|
110
|
+
| `DELETE /api/v1/session` | | 204, signs out |
|
|
111
|
+
| `GET`, `PATCH`, `DELETE /api/v1/account` | `name` | the account; deleting needs a recent sign-in |
|
|
112
|
+
| `POST /api/v1/account/email` | `email` | 202, a code or a link is emailed to the new address |
|
|
113
|
+
| `PATCH /api/v1/account/email` | codes: `email`, `otp`; passwords: `token` | the account, with its new email; 409 when another account has it |
|
|
114
|
+
| `PATCH /api/v1/account/password` | `password` | 204 (passwords); needs a recent sign-in |
|
|
115
|
+
| `GET /api/v1/account/sessions` | | the signed-in devices, `current` for this one |
|
|
116
|
+
| `DELETE /api/v1/account/sessions/:id`, `/others` | | 204 |
|
|
117
|
+
|
|
118
|
+
With passwords, the emailed links point at the app's domain (`https://<host>/account/confirm?token=…`, `/password-reset?token=…`, `/account/email?token=…`), or at `CUSTOMERS_APP_LINKS_URL`: the mobile app opens them (iOS universal links, Android app links) and sends their `token` to the endpoints above.
|
|
119
|
+
|
|
120
|
+
## Audit log
|
|
121
|
+
|
|
122
|
+
When the app has the [audit log](/reference/generators/audit-log), the account controllers write to it too: sign-ins, failed or not, sign-ups, code and link requests (with no actor), and account changes, the customer being the actor.
|
|
123
|
+
|
|
124
|
+
## Inactive accounts
|
|
125
|
+
|
|
126
|
+
`Customer.inactive_since(time)` lists the accounts with no request since `time` (`last_activity_at`, written at most every 5 minutes), e.g. for a job deleting them. It can only ever list customers.
|
|
127
|
+
|
|
128
|
+
## Upgrading Trek
|
|
129
|
+
|
|
130
|
+
The generated files are the app's own: upgrading Trek never changes them. On each upgrade, check:
|
|
131
|
+
|
|
132
|
+
- **The CHANGELOG,** for entries on what the generated code relies on in the gem: `Trek::Users::SessionLifetime`, `Trek::Users::SignInCode` (codes), `Trek::Users::SignInLockout` and `Trek::Users::PasswordPolicy` (passwords), `Trek::PurgeExpiredSessionsJob`, `Trek::ResolveSessionHostnameJob` and `Trek::Auditable`.
|
|
133
|
+
- **The generator's own changes,** a security fix among them, which reach the app only by running it again. On a clean working tree, run it with the same name and options, then review `git diff` and restore the app's own changes:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
rails g trek:public_accounts Customer --force # plus the options it was generated with
|
|
137
|
+
git diff
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Running it again adds nothing already there: no second migration, route or `Current` attribute.
|
|
141
|
+
- **The admin's session,** which `staff_user` reads: its `session_id` cookie and `User::Session`, in `Customers::Authentication#staff_user`. An upgrade note says when they change.
|
|
@@ -41,7 +41,7 @@ The policy only **reports** violations at first (`config.content_security_policy
|
|
|
41
41
|
|
|
42
42
|
### Violation reports
|
|
43
43
|
|
|
44
|
-
Browsers send each violation to the policy's `report-uri`: `/csp-reports`, which `Trek::CspReportsController` answers (the generator adds `post "csp-reports"` to the routes). It reports each violation to **AppSignal**, when the app uses it, in a `csp` namespace: one incident per directive and blocked origin (e.g. `script-src-elem https://cdn.example.net`), tagged with the page and whether the policy was enforced, so a new violation is notified once and closed once fixed. It logs each one too (`[CSP] script-src-elem blocked https://
|
|
44
|
+
Browsers send each violation to the policy's `report-uri`: `/csp-reports`, which `Trek::CspReportsController` answers (the generator adds `post "csp-reports"` to the routes). It reports each violation to **AppSignal**, when the app uses it, in a `csp` namespace: one incident per directive and blocked origin (e.g. `script-src-elem https://cdn.example.net`), tagged with the page and whether the policy was enforced, so a new violation is notified once and closed once fixed. To tell what was blocked, each report is also tagged with the browser's user agent and, when the browser gives them, where it happened (`source`: the file's URL, without its query, and the line) and the `sample`: the first 40 characters of an inline script, which the policy asks for with `'report-sample'` (`policy.script_src :self, :report_sample`). Tags are cut to 255 characters, as AppSignal drops longer ones. It logs each one too (`[CSP] script-src-elem blocked inline on /pages/team (report) at https://www.example.com/pages/team:12: "alert(1)"`), which is all an app without AppSignal gets.
|
|
45
45
|
|
|
46
46
|
It keeps the reports about the app's own pages only, and drops what browser extensions load or inject, which makes most reports and says nothing about the app. It keeps the page's path, not its query, which may carry a token, and the blocked resource's origin, not its URL. Browsers send no CSRF token nor cookie: the endpoint needs neither, and answers `204`.
|
|
47
47
|
|
data/docs/reference/index.md
CHANGED
|
@@ -8,7 +8,8 @@ A comprehensive list of features and settings included in Trek.
|
|
|
8
8
|
- [Admin user](/reference/generators/admin-user) — `rails g trek:admin:user`
|
|
9
9
|
- [Scaffold](/reference/generators/scaffold) — `rails g trek:scaffold`
|
|
10
10
|
- [Taxonomies](/reference/generators/taxonomies) — `rails g trek:taxonomies`
|
|
11
|
-
- [Audit log](/reference/generators/audit-log) — `rails g trek:
|
|
11
|
+
- [Audit log](/reference/generators/audit-log) — `rails g trek:audit_log`
|
|
12
|
+
- [Public accounts](/reference/generators/public-accounts) — `rails g trek:public_accounts`
|
|
12
13
|
- [Security headers](/reference/generators/security-headers) — `rails g trek:install:security_headers`
|
|
13
14
|
|
|
14
15
|
## Models
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The host application's `User` model, into which Trek injects authentication (`has_secure_password` and database-backed sessions), roles, theme & locale preferences and the invitation workflow.
|
|
4
4
|
|
|
5
|
+
It is the admin's account. The site's or the mobile app's users (customers, members…) get a model of their own, apart from it: see [Public accounts](/reference/generators/public-accounts).
|
|
6
|
+
|
|
5
7
|
## Columns added by Trek
|
|
6
8
|
|
|
7
9
|
| Column | Type | Notes |
|
|
@@ -68,7 +70,7 @@ Users see their **active sessions** in a tab of their own profile: browser and s
|
|
|
68
70
|
`Trek::Users::SignIns` helps users spot a sign-in that wasn't theirs:
|
|
69
71
|
|
|
70
72
|
- the **Active sessions** tab of their profile shows their **previous sign-in**: its date, browser and IP address. It's kept on the user (`last_sign_in_at`, `last_sign_in_ip`, `last_sign_in_user_agent`, with their `current_sign_in_*` counterparts), as sessions are deleted when they end;
|
|
71
|
-
- a sign-in from a **browser that never signed in as the user** emails them (`AdminMailer#new_sign_in`): date, browser, IP address, with links to change their password and to their active sessions. A browser is recognized by a signed cookie, kept for years, listing the users who signed in from it. Their very first sign-in sends nothing.
|
|
73
|
+
- a sign-in from a **browser that never signed in as the user** emails them (`AdminMailer#new_sign_in`): date, browser, IP address and its host name (as on the active sessions), with links to change their password and to their active sessions. A browser is recognized by a signed cookie, kept for years, listing the users who signed in from it. Their very first sign-in sends nothing.
|
|
72
74
|
|
|
73
75
|
Set `self.new_sign_in_email = false` in the `User` model to turn the email off.
|
|
74
76
|
|
data/docs/what-is-trek.md
CHANGED
|
@@ -9,7 +9,7 @@ Trek ships as a Rails engine (a gem plus an NPM package) that integrates into yo
|
|
|
9
9
|
- **A complete admin panel** — layout, navigation, search, pagination, light & dark themes, all built with [ViewComponent](https://viewcomponent.org), [Stimulus](https://stimulus.hotwired.dev) and [Turbo](https://turbo.hotwired.dev).
|
|
10
10
|
- **Pages** — hierarchical, translatable pages with version history, SEO metadata and stable, redirect-aware URL paths.
|
|
11
11
|
- **Fragments** — reusable chunks of content (a footer text, a hero tagline…) editable from the admin and usable anywhere in your views.
|
|
12
|
-
- **A
|
|
12
|
+
- **A content editor** — based on [Tiptap](https://tiptap.dev), with image uploads (Uppy), link insertion and content stored as structured ProseMirror JSON and rendered safely to HTML (see [Content editor](/content-editor)).
|
|
13
13
|
- **Authentication & authorization** — sign in built on Rails' [`has_secure_password`](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html) with database-backed sessions, password resets, invitations, and [ActionPolicy](https://actionpolicy.evilmartians.io) policies with `admin`, `editor`, `user`, `reader` and `guest` roles.
|
|
14
14
|
- **Internationalization** — content translations with [Mobility](https://github.com/shioyama/mobility), locale management with [i18n-tasks](https://github.com/glebm/i18n-tasks), and optional automatic translation through DeepL.
|
|
15
15
|
- **File uploads** — [Shrine](https://shrinerb.com)-backed attachments with S3 or local storage, image processing via libvips, and blurhash placeholders.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "rails/generators/actions"
|
|
2
|
+
require "rails/generators/migration"
|
|
3
|
+
require "generators/trek/helpers"
|
|
4
|
+
|
|
5
|
+
module Trek
|
|
6
|
+
module Generators
|
|
7
|
+
class AuditLogGenerator < Rails::Generators::Base
|
|
8
|
+
include Rails::Generators::Migration
|
|
9
|
+
include Trek::Generators::Helpers
|
|
10
|
+
|
|
11
|
+
desc "Create the AuditLog model, record the admin's requests, and list them in the admin."
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
def copy_model
|
|
16
|
+
template "models/audit_log.rb", "app/models/audit_log.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_migration_file
|
|
20
|
+
return unless defined?(ActiveRecord)
|
|
21
|
+
|
|
22
|
+
migration_template("migrations/audit_logs.rb.erb",
|
|
23
|
+
"db/migrate/trek_audit_logs.rb",
|
|
24
|
+
migration_class_name:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def copy_initializer
|
|
28
|
+
template "initializers/audit_log.rb", "config/initializers/audit_log.rb"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def copy_controller
|
|
32
|
+
template "controllers/admin/audit_logs_controller.rb",
|
|
33
|
+
"app/controllers/admin/audit_logs_controller.rb"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def copy_views
|
|
37
|
+
directory "views/admin/audit_logs", "app/views/admin/audit_logs"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def copy_policy
|
|
41
|
+
template "policies/audit_log.rb", "app/policies/admin/audit_log_policy.rb"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def add_routes
|
|
45
|
+
route "resources :audit_logs, only: %i[index show]", namespace: :admin
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def copy_specs
|
|
49
|
+
template "spec/models/audit_log_spec.rb", "spec/models/audit_log_spec.rb"
|
|
50
|
+
template "spec/requests/admin/audit_logs_spec.rb", "spec/requests/admin/audit_logs_spec.rb"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -25,10 +25,10 @@ module Trek
|
|
|
25
25
|
get :options, on: :collection
|
|
26
26
|
end
|
|
27
27
|
RUBY
|
|
28
|
+
route "resource :password_renewal, only: :create", namespace: :admin
|
|
28
29
|
route <<~RUBY, namespace: :admin
|
|
29
30
|
resources :users do
|
|
30
31
|
patch :theme, on: :collection
|
|
31
|
-
post :require_password_renewal, on: :collection
|
|
32
32
|
end
|
|
33
33
|
RUBY
|
|
34
34
|
end
|
|
@@ -52,6 +52,11 @@ module Trek
|
|
|
52
52
|
template "controllers/admin/passkeys_controller.rb",
|
|
53
53
|
"app/controllers/admin/passkeys_controller.rb"
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
def copy_password_renewals
|
|
57
|
+
template "controllers/admin/password_renewals_controller.rb",
|
|
58
|
+
"app/controllers/admin/password_renewals_controller.rb"
|
|
59
|
+
end
|
|
55
60
|
end
|
|
56
61
|
end
|
|
57
62
|
end
|
|
@@ -1,54 +1,18 @@
|
|
|
1
|
-
require "
|
|
2
|
-
require "rails/generators/migration"
|
|
3
|
-
require "generators/trek/helpers"
|
|
1
|
+
require "generators/trek/audit_log_generator"
|
|
4
2
|
|
|
5
3
|
module Trek
|
|
6
4
|
module Generators
|
|
7
5
|
module Install
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
# The audit log's generator under its former name: `trek:install` doesn't run it, so it's `trek:audit_log` now.
|
|
7
|
+
# Removed in Trek 3.
|
|
8
|
+
class AuditLogGenerator < Trek::Generators::AuditLogGenerator
|
|
9
|
+
hide!
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
# Not inherited: Rails derives it from the generator's own path.
|
|
12
|
+
source_root Trek::Generators::AuditLogGenerator.source_root
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def copy_model
|
|
17
|
-
template "models/audit_log.rb", "app/models/audit_log.rb"
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def copy_migration_file
|
|
21
|
-
return unless defined?(ActiveRecord)
|
|
22
|
-
|
|
23
|
-
migration_template("migrations/audit_logs.rb.erb",
|
|
24
|
-
"db/migrate/trek_audit_logs.rb",
|
|
25
|
-
migration_class_name:)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def copy_initializer
|
|
29
|
-
template "initializers/audit_log.rb", "config/initializers/audit_log.rb"
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def copy_controller
|
|
33
|
-
template "controllers/admin/audit_logs_controller.rb",
|
|
34
|
-
"app/controllers/admin/audit_logs_controller.rb"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def copy_views
|
|
38
|
-
directory "views/admin/audit_logs", "app/views/admin/audit_logs"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def copy_policy
|
|
42
|
-
template "policies/audit_log.rb", "app/policies/admin/audit_log_policy.rb"
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def add_routes
|
|
46
|
-
route "resources :audit_logs, only: %i[index show]", namespace: :admin
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def copy_specs
|
|
50
|
-
template "spec/models/audit_log_spec.rb", "spec/models/audit_log_spec.rb"
|
|
51
|
-
template "spec/requests/admin/audit_logs_spec.rb", "spec/requests/admin/audit_logs_spec.rb"
|
|
14
|
+
def deprecation_warning
|
|
15
|
+
say "`trek:install:audit_log` is deprecated and will be removed in Trek 3: run `trek:audit_log`.", :yellow
|
|
52
16
|
end
|
|
53
17
|
end
|
|
54
18
|
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
require "rails/generators/actions"
|
|
2
|
+
require "generators/trek/helpers"
|
|
3
|
+
|
|
4
|
+
module Trek
|
|
5
|
+
module Generators
|
|
6
|
+
module Install
|
|
7
|
+
# Replaces the Solid Queue backend `rails new` sets up with GoodJob: one PostgreSQL
|
|
8
|
+
# database instead of two, jobs running inside Puma, and a dashboard in the admin.
|
|
9
|
+
class GoodJobGenerator < Rails::Generators::Base
|
|
10
|
+
GOOD_JOB_VERSION = "~> 4.13".freeze
|
|
11
|
+
GEM_COMMENT = "Active Job backend on PostgreSQL, with a dashboard [https://github.com/bensheldon/good_job]".freeze
|
|
12
|
+
|
|
13
|
+
# The line `rails new` writes to size the connection pool: `max_connections` since
|
|
14
|
+
# Rails 8.1, `pool` before it. Whichever it is, it keeps its name.
|
|
15
|
+
POOL_LINE = /^(\s*)(max_connections|pool): <%= ENV\.fetch\("RAILS_MAX_THREADS"\)[^\n]*%>\n/
|
|
16
|
+
|
|
17
|
+
include Trek::Generators::Helpers
|
|
18
|
+
|
|
19
|
+
desc "Install GoodJob as the Active Job backend."
|
|
20
|
+
|
|
21
|
+
source_root File.expand_path("../templates", __dir__)
|
|
22
|
+
|
|
23
|
+
def swap_gems
|
|
24
|
+
return if in_gemfile?("good_job")
|
|
25
|
+
|
|
26
|
+
# Solid Queue wants a second database, which a Trek app does not provision (Solid
|
|
27
|
+
# Cache stays: its table lives in the primary database). GoodJob takes over its line,
|
|
28
|
+
# under the Active Job adapter comment it belongs to.
|
|
29
|
+
if in_gemfile?("solid_queue")
|
|
30
|
+
gsub_file "Gemfile", /^gem "solid_queue".*\n/,
|
|
31
|
+
%(# #{GEM_COMMENT}\ngem "good_job", "#{GOOD_JOB_VERSION}"\n)
|
|
32
|
+
else
|
|
33
|
+
gem "good_job", GOOD_JOB_VERSION, comment: GEM_COMMENT
|
|
34
|
+
end
|
|
35
|
+
bundle_install
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def remove_solid_queue_files
|
|
39
|
+
%w[config/queue.yml config/recurring.yml db/queue_schema.rb bin/jobs].each do |file|
|
|
40
|
+
remove_file file
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def remove_solid_queue_from_puma
|
|
45
|
+
gsub_file "config/puma.rb",
|
|
46
|
+
/^(?:#[^\n]*Solid Queue[^\n]*\n)?plugin :solid_queue if ENV\["SOLID_QUEUE_IN_PUMA"\]\n\n?/,
|
|
47
|
+
""
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def remove_queue_database
|
|
51
|
+
gsub_file "config/database.yml", /^ queue:\n(?: [^\n]*\n)+/, ""
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# GoodJob runs in the web container's process, so its threads need connections of their
|
|
55
|
+
# own — the pool `rails new` sizes for the web threads alone would starve them.
|
|
56
|
+
def size_connection_pool
|
|
57
|
+
gsub_file "config/database.yml", POOL_LINE, <<~'YAML'
|
|
58
|
+
\1# Web threads and job threads each check out their own connections, plus one for
|
|
59
|
+
\1# GoodJob's LISTEN/NOTIFY listener. GOOD_JOB_MAX_THREADS falls back to
|
|
60
|
+
\1# RAILS_MAX_THREADS inside GoodJob, so both shares match unless it is set.
|
|
61
|
+
\1\2: <%= web = ENV.fetch("RAILS_MAX_THREADS", 5).to_i; 1 + web + ENV.fetch("GOOD_JOB_MAX_THREADS", web).to_i %>
|
|
62
|
+
YAML
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Kamal's file is not what a Trek app deploys with, but leaving Solid Queue's variables
|
|
66
|
+
# in it would describe a backend the app no longer runs.
|
|
67
|
+
def clean_kamal_deploy_file
|
|
68
|
+
return unless File.exist?("config/deploy.yml")
|
|
69
|
+
|
|
70
|
+
gsub_file "config/deploy.yml",
|
|
71
|
+
/^(?:[ \t]*#[^\n]*\n)*([ \t]*)SOLID_QUEUE_IN_PUMA: true\n/,
|
|
72
|
+
<<~'YAML'
|
|
73
|
+
\1# Jobs run inside Puma (config.good_job.execution_mode = :async), so there is no
|
|
74
|
+
\1# separate job process to start. GOOD_JOB_MAX_THREADS sizes its thread pool.
|
|
75
|
+
\1# GOOD_JOB_MAX_THREADS: 5
|
|
76
|
+
YAML
|
|
77
|
+
gsub_file "config/deploy.yml",
|
|
78
|
+
/^[ \t]*# Set number of processes dedicated to Solid Queue[^\n]*\n[ \t]*# JOB_CONCURRENCY:[^\n]*\n\n?/,
|
|
79
|
+
""
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def configure_adapter
|
|
83
|
+
return if File.read("config/application.rb").include?("queue_adapter = :good_job")
|
|
84
|
+
|
|
85
|
+
inject_into_file "config/application.rb", adapter_configuration,
|
|
86
|
+
after: /^\s*config\.load_defaults [^\n]*\n/
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Active Job's test helper only swaps in its test adapter for a job class with no adapter
|
|
90
|
+
# configured, and config/application.rb now sets GoodJob: give the specs the test adapter
|
|
91
|
+
# back, for have_enqueued_job and the like.
|
|
92
|
+
def configure_test_adapter
|
|
93
|
+
return unless File.exist?("config/environments/test.rb")
|
|
94
|
+
return if File.read("config/environments/test.rb").include?("queue_adapter = :test")
|
|
95
|
+
|
|
96
|
+
inject_into_file "config/environments/test.rb", <<-RUBY, after: /^Rails\.application\.configure do\n/
|
|
97
|
+
# Jobs go to Active Job's test adapter, not GoodJob (config/application.rb), so specs can check
|
|
98
|
+
# what was enqueued.
|
|
99
|
+
config.active_job.queue_adapter = :test
|
|
100
|
+
|
|
101
|
+
RUBY
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def configure_production
|
|
105
|
+
gsub_file "config/environments/production.rb", /^\s*config\.solid_queue\.connects_to[^\n]*\n/, ""
|
|
106
|
+
gsub_file "config/environments/production.rb",
|
|
107
|
+
/^(\s*)(?:# )?config\.active_job\.queue_adapter\s*=[^\n]*\n/,
|
|
108
|
+
<<~'RUBY'
|
|
109
|
+
\1# Jobs run in the web container's own threads: no worker process to pay for, and a
|
|
110
|
+
\1# deploy that drains them (shutdown_timeout) rather than dropping them.
|
|
111
|
+
\1config.good_job.execution_mode = :async
|
|
112
|
+
\1config.good_job.shutdown_timeout = 25
|
|
113
|
+
RUBY
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def create_initializer
|
|
117
|
+
template "initializers/good_job.rb.tt", "config/initializers/good_job.rb"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def create_mail_delivery_job
|
|
121
|
+
copy_file "jobs/mail_delivery_job.rb", "app/jobs/mail_delivery_job.rb"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def mount_dashboard
|
|
125
|
+
route 'mount GoodJob::Engine => "good_job"', namespace: :admin
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def create_migration
|
|
129
|
+
generate "good_job:install"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def create_specs
|
|
133
|
+
return unless File.directory?("spec")
|
|
134
|
+
|
|
135
|
+
copy_file "spec/requests/admin/good_job_spec.rb", "spec/requests/admin/good_job_spec.rb"
|
|
136
|
+
copy_file "spec/jobs/mail_delivery_job_spec.rb", "spec/jobs/mail_delivery_job_spec.rb"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
# The model of a signed-in browser, which the dashboard's guard looks the session cookie up
|
|
142
|
+
# in: User::Session, or UserSession in an app generated before Trek nested it.
|
|
143
|
+
def session_model
|
|
144
|
+
File.exist?("app/models/user_session.rb") ? "UserSession" : "User::Session"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def adapter_configuration
|
|
148
|
+
<<-RUBY
|
|
149
|
+
|
|
150
|
+
# Use GoodJob as the Active Job backend (see config/initializers/good_job.rb).
|
|
151
|
+
config.active_job.queue_adapter = :good_job
|
|
152
|
+
|
|
153
|
+
# Every `deliver_later` email goes through MailDeliveryJob, which retries a failed
|
|
154
|
+
# delivery — GoodJob discards a failed job (see app/jobs/mail_delivery_job.rb).
|
|
155
|
+
config.action_mailer.delivery_job = "MailDeliveryJob"
|
|
156
|
+
RUBY
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -51,11 +51,11 @@ module Trek
|
|
|
51
51
|
directory "db/seeds", "db/seeds"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
# The audit log's specs come with it, from `trek:
|
|
55
|
-
# `trek:taxonomies`.
|
|
54
|
+
# The audit log's specs come with it, from `trek:audit_log`, the GoodJob dashboard's from
|
|
55
|
+
# `trek:install:good_job`, and the tags' (model and policies) from `trek:taxonomies`.
|
|
56
56
|
def copy_specs
|
|
57
57
|
directory "spec/models", "spec/models", exclude_pattern: /audit_log|tag_spec/
|
|
58
|
-
directory "spec/requests", "spec/requests", exclude_pattern: /audit_log/
|
|
58
|
+
directory "spec/requests", "spec/requests", exclude_pattern: /audit_log|good_job_spec/
|
|
59
59
|
directory "spec/mailers", "spec/mailers"
|
|
60
60
|
directory "spec/fixtures", "spec/fixtures"
|
|
61
61
|
directory "spec/policies", "spec/policies", exclude_pattern: /tag(_category)?_policy_spec/
|
|
@@ -1,53 +1,15 @@
|
|
|
1
|
-
require "
|
|
2
|
-
require "generators/trek/helpers"
|
|
1
|
+
require "generators/trek/scalingo_generator"
|
|
3
2
|
|
|
4
3
|
module Trek
|
|
5
4
|
module Generators
|
|
6
5
|
module Install
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
# The Scalingo generator under its former name: `trek:install` doesn't run it, so it's `trek:scalingo` now.
|
|
7
|
+
# Removed in Trek 3.
|
|
8
|
+
class ScalingoGenerator < Trek::Generators::ScalingoGenerator
|
|
9
|
+
hide!
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
desc "Create Scalingo files."
|
|
13
|
-
|
|
14
|
-
# The trek-buildpack replaces the apt + ruby buildpack combination. It
|
|
15
|
-
# pins Node from the .node-version that `rails new` already generates.
|
|
16
|
-
def remove_buildpacks
|
|
17
|
-
remove_file ".buildpacks"
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def create_aptfile
|
|
21
|
-
create_file "Aptfile" do
|
|
22
|
-
<<~TEXT
|
|
23
|
-
libjemalloc2
|
|
24
|
-
libvips-tools --no-install-recommends
|
|
25
|
-
libwebp-dev
|
|
26
|
-
TEXT
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def create_procfile
|
|
31
|
-
create_file "Procfile" do
|
|
32
|
-
<<~TEXT
|
|
33
|
-
web: bundle exec rails server -p $PORT -e $RAILS_ENV -b 0.0.0.0
|
|
34
|
-
# worker: bundle exec sidekiq -C config/sidekiq.yml
|
|
35
|
-
postdeploy: bundle exec rails db:migrate
|
|
36
|
-
TEXT
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def final_instructions
|
|
41
|
-
say ""
|
|
42
|
-
say "Configure the Scalingo app environment:"
|
|
43
|
-
say ""
|
|
44
|
-
say " scalingo --app <app> env-set BUILDPACK_URL=#{BUILDPACK_URL}"
|
|
45
|
-
say " scalingo --app <app> env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'"
|
|
46
|
-
say ""
|
|
47
|
-
say "and make sure config/database.yml reads DATABASE_URL in production."
|
|
48
|
-
say ""
|
|
49
|
-
say "See https://trek.etamin.studio/deploy for details."
|
|
50
|
-
say ""
|
|
11
|
+
def deprecation_warning
|
|
12
|
+
say "`trek:install:scalingo` is deprecated and will be removed in Trek 3: run `trek:scalingo`.", :yellow
|
|
51
13
|
end
|
|
52
14
|
end
|
|
53
15
|
end
|
|
@@ -72,6 +72,11 @@ module Trek
|
|
|
72
72
|
generate_inline "trek:install:strip_attributes"
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Before the emails: the mailer configuration points at MailDeliveryJob.
|
|
76
|
+
def install_jobs
|
|
77
|
+
generate_inline "trek:install:good_job"
|
|
78
|
+
end
|
|
79
|
+
|
|
75
80
|
def install_emails
|
|
76
81
|
generate_inline "trek:install:postmark"
|
|
77
82
|
generate_inline "trek:install:mailer"
|
|
@@ -180,6 +185,16 @@ module Trek
|
|
|
180
185
|
say ""
|
|
181
186
|
say " make dev"
|
|
182
187
|
say ""
|
|
188
|
+
say "More generators, to run when the app needs them:"
|
|
189
|
+
say ""
|
|
190
|
+
say " rails g trek:scaffold Book title intro:text # a model and its admin panel"
|
|
191
|
+
say " rails g trek:taxonomies # tags and their categories"
|
|
192
|
+
say " rails g trek:audit_log # a log of the admin's requests"
|
|
193
|
+
say " rails g trek:public_accounts Customer # accounts for the site's users, apart from the admin's"
|
|
194
|
+
say " rails g trek:scalingo # the files deploying to Scalingo"
|
|
195
|
+
say ""
|
|
196
|
+
say "All of Trek's generators and their options: https://trek.etamin.studio/reference/#generators"
|
|
197
|
+
say ""
|
|
183
198
|
end
|
|
184
199
|
|
|
185
200
|
private
|