@hartl-services/medusa-base 2.6.0 → 2.7.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.
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +3 -2
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.js +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginActionsSection.js +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.d.ts +16 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.js +25 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginDetailPage.js +1 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +5 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.js +8 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +4 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.js +24 -27
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +2 -2
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +9 -9
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.d.ts +5 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.js +11 -15
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.d.ts +10 -4
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.js +10 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +0 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.js +73 -26
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -1
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -6
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.d.ts +11 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.js +15 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +36 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +144 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +36 -5
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.d.ts +15 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.js +66 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +144 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +49 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.js +72 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.d.ts +1 -12
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.js +4 -20
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.d.ts +43 -0
- package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.js +66 -0
- package/.medusa/server/src/admin/admin-components/i18n/texts.d.ts +22 -2
- package/.medusa/server/src/admin/admin-components/i18n/texts.js +29 -7
- package/.medusa/server/src/admin/admin-components/index.d.ts +12 -0
- package/.medusa/server/src/admin/admin-components/index.js +11 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.d.ts +22 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.d.ts +26 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.js +12 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.d.ts +23 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.js +20 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.d.ts +21 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.js +33 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.d.ts +27 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.js +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.d.ts +22 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.js +14 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/PdfPreviewModal.js +1 -1
- package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.d.ts +10 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.js +10 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.d.ts +27 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.js +15 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.d.ts +15 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.js +8 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.js +16 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.d.ts +13 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.js +11 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.d.ts +12 -0
- package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.js +29 -0
- package/.medusa/server/src/admin/index.js +3123 -2019
- package/.medusa/server/src/admin/index.mjs +3126 -2022
- package/.medusa/server/src/api/admin-first-login-password-policy.js +56 -0
- package/.medusa/server/src/api/answer-actor-type-casing-not-found.js +26 -0
- package/.medusa/server/src/api/answer-code-provider-route-not-found.js +34 -0
- package/.medusa/server/src/api/code-account-password-routes.js +92 -0
- package/.medusa/server/src/api/middlewares.js +82 -17
- package/.medusa/server/src/api/password-policy.js +55 -10
- package/.medusa/server/src/api/require-login-code-customer-email.js +40 -0
- package/.medusa/server/src/api/store/customer-auth/login-code/route.js +17 -0
- package/.medusa/server/src/api/store/customer-auth/password-policy/route.js +19 -0
- package/.medusa/server/src/customer-auth/application/prepare-code-account-password-reset.js +111 -0
- package/.medusa/server/src/customer-auth/application/request-login-code.js +145 -0
- package/.medusa/server/src/customer-auth/customer-auth-methods-snapshot.js +55 -0
- package/.medusa/server/src/customer-auth/customer-password-policy.js +40 -0
- package/.medusa/server/src/customer-auth/domain/account-linking.js +53 -0
- package/.medusa/server/src/customer-auth/domain/auth-methods.js +32 -0
- package/.medusa/server/src/customer-auth/domain/login-code.js +39 -0
- package/.medusa/server/src/customer-auth/login-code-accounts.js +146 -0
- package/.medusa/server/src/customer-auth/login-code-state.js +139 -0
- package/.medusa/server/src/customer-auth/plugin-features.js +80 -0
- package/.medusa/server/src/customer-auth/remove-provider-identity.js +18 -0
- package/.medusa/server/src/customer-auth/start-customer-auth-methods.js +20 -0
- package/.medusa/server/src/documents/plugin-features.js +8 -2
- package/.medusa/server/src/email/application/deliver-ephemeral-auth-mail.js +186 -0
- package/.medusa/server/src/email/application/deliver-login-code.js +89 -0
- package/.medusa/server/src/email/application/handle-customer-verification-event.js +29 -168
- package/.medusa/server/src/email/application/handle-password-reset-event.js +27 -166
- package/.medusa/server/src/email/customer-verification-snapshot.js +20 -108
- package/.medusa/server/src/email/domain/business-keys.js +15 -2
- package/.medusa/server/src/email/plugin-features.js +8 -3
- package/.medusa/server/src/email/rendering/default-templates.js +2 -1
- package/.medusa/server/src/email/templates/blocks/dynamic-blocks.js +19 -1
- package/.medusa/server/src/email/templates/defaults/documents.js +7 -1
- package/.medusa/server/src/email/templates/defaults/subjects.js +3 -1
- package/.medusa/server/src/email/templates/defaults/texts.js +5 -2
- package/.medusa/server/src/email/templates/render.js +2 -1
- package/.medusa/server/src/email/templates/sample-data.js +2 -1
- package/.medusa/server/src/email/templates/validate.js +8 -3
- package/.medusa/server/src/email/types/email-kinds.js +4 -2
- package/.medusa/server/src/email/types/template-data.js +9 -1
- package/.medusa/server/src/modules/customer-auth/index.js +14 -0
- package/.medusa/server/src/modules/customer-auth/service.js +35 -0
- package/.medusa/server/src/modules/email/migrations/Migration20261010090000.js +20 -0
- package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +4 -1
- package/.medusa/server/src/modules/plugin-features/service.js +26 -1
- package/.medusa/server/src/providers/email-otp/index.js +8 -0
- package/.medusa/server/src/providers/email-otp/service.js +96 -0
- package/.medusa/server/src/public/descriptor.js +117 -5
- package/.medusa/server/src/public/effective-features.js +22 -1
- package/.medusa/server/src/public/index.js +4 -2
- package/.medusa/server/src/public/plugin-icons.js +28 -0
- package/.medusa/server/src/public/shop-master-data.js +33 -14
- package/.medusa/server/src/shared/lib/index.js +2 -1
- package/.medusa/server/src/shared/lib/refreshing-snapshot.js +122 -0
- package/.medusa/server/src/shop-config/build-food-shop-config.js +38 -1
- package/README.md +436 -87
- package/dist/customer-auth/customer-auth-methods-snapshot.d.ts +25 -0
- package/dist/customer-auth/customer-auth-methods-snapshot.js +54 -0
- package/dist/customer-auth/domain/auth-methods.d.ts +16 -0
- package/dist/customer-auth/domain/auth-methods.js +31 -0
- package/dist/customer-auth/plugin-features.d.ts +2 -0
- package/dist/customer-auth/plugin-features.js +79 -0
- package/dist/email/customer-verification-snapshot.d.ts +3 -15
- package/dist/email/customer-verification-snapshot.js +19 -107
- package/dist/email/types/email-kinds.d.ts +4 -3
- package/dist/email/types/email-kinds.js +3 -1
- package/dist/email/types/template-data.d.ts +8 -0
- package/dist/email/types/template-data.js +8 -0
- package/dist/public/descriptor.js +116 -4
- package/dist/public/effective-features.d.ts +10 -0
- package/dist/public/effective-features.js +21 -0
- package/dist/public/index.d.ts +2 -1
- package/dist/public/index.js +3 -1
- package/dist/public/plugin-icons.d.ts +7 -0
- package/dist/public/plugin-icons.js +27 -0
- package/dist/public/shop-master-data.js +32 -13
- package/dist/public/types.d.ts +63 -1
- package/dist/shared/lib/index.d.ts +1 -0
- package/dist/shared/lib/index.js +1 -0
- package/dist/shared/lib/refreshing-snapshot.d.ts +52 -0
- package/dist/shared/lib/refreshing-snapshot.js +121 -0
- package/dist/shop-config/build-food-shop-config.js +37 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -40,8 +40,9 @@ packing lists) and S3 file storage, and the turnkey `medusa-config.ts` preset
|
|
|
40
40
|
- Redirects `GET /` to the admin dashboard and repairs dangling
|
|
41
41
|
`cart_promotion` links (see "Root redirect" and "Cart-promotion link
|
|
42
42
|
repair").
|
|
43
|
-
- Requires passwords of at least 15 characters for
|
|
44
|
-
(see "Password
|
|
43
|
+
- Requires passwords of at least 15 characters for Admin users and, by
|
|
44
|
+
default, customers; shops can set their own customer rules (see "Password
|
|
45
|
+
policy").
|
|
45
46
|
- Starts Admin draft orders without a customer order confirmation and marks
|
|
46
47
|
placed 0 € orders without payment as paid (see "Draft orders and 0 €
|
|
47
48
|
orders").
|
|
@@ -53,6 +54,9 @@ packing lists) and S3 file storage, and the turnkey `medusa-config.ts` preset
|
|
|
53
54
|
"E-Mail"), issues invoices, cancellation documents and packing lists as
|
|
54
55
|
private PDFs (module `documents`, see "Dokumente") and stores files in
|
|
55
56
|
S3-compatible storage behind the `/files` gateway (see "Dateispeicher (S3)").
|
|
57
|
+
- Declares which login methods customers may use (module `customer_auth`:
|
|
58
|
+
e-mail + password and a one-time code by e-mail), provides the `email-otp`
|
|
59
|
+
auth provider and the code request route (see "Kunden-Login").
|
|
56
60
|
- Builds a food shop's complete `medusa-config.ts` from a few shop facts
|
|
57
61
|
(`@hartl-services/medusa-base/shop-config`, see "Shop-Config-Preset").
|
|
58
62
|
|
|
@@ -131,13 +135,19 @@ dashboard uses.
|
|
|
131
135
|
| `POST` | `/admin/plugin-features/:plugin_key/settings` | Admin user | `AdminPluginFeaturesResponse` |
|
|
132
136
|
| `POST` | `/admin/plugin-features/:plugin_key/actions/:action_key` | Admin user | `AdminPluginActionRunResponse` (200 sync, 202 background), 409 `AdminPluginActionConflictResponse` while it runs |
|
|
133
137
|
| `GET` | `/admin/plugin-features/:plugin_key/actions/:action_key/runs` | Admin user | `AdminPluginActionRunsResponse` (`?limit=`, default 1, max 20) |
|
|
138
|
+
| `POST` | `/store/customer-auth/login-code` | publishable key | 202 `RequestLoginCodeResponse` (`{ accepted: true }`) for every address, 404 while `email_otp` is off; see "Kunden-Login" |
|
|
139
|
+
| `POST` | `/auth/customer/email-otp` | publishable key | Medusa's own login route with the `email-otp` provider: `{ token }` for a right code, 401 for any other, 400 while `email_otp` is off; see "Kunden-Login" |
|
|
140
|
+
| `GET` | `/store/customer-auth/password-policy` | publishable key | `PasswordPolicyResponse` (`{ password_policy }`, the effective customer rules), 404 while `email_password` is off; see "Password policy" |
|
|
134
141
|
| `POST` | `/store/customer-verification/resend` | publishable key | 202 `ResendCustomerVerificationResponse` (`{ accepted: true }`) for every address, 404 while `customer_verification` is off; see "Kontobestätigung per E-Mail" |
|
|
135
142
|
|
|
136
143
|
Every authenticated Admin user may change toggles and settings and run
|
|
137
144
|
actions (no RBAC by design). Admin plugin DTOs carry the toggles plus
|
|
138
145
|
`settings` (with `source`, never a secret's value), `configured`, `missing`,
|
|
139
146
|
`settings_version`, `actions`, `admin_page` (the plugin's own page or
|
|
140
|
-
`null`) and `switchable`,
|
|
147
|
+
`null`) and `switchable`, plus the presentation fields (`icon`, `summary`,
|
|
148
|
+
`disable_hint`, `disable_confirm`, `features_heading`, `features_note`,
|
|
149
|
+
`all_off_warning`, `require_enabled_feature`, `setting_groups`; see
|
|
150
|
+
"Presentation fields for the Admin page"), so the list is one round trip for the hub.
|
|
141
151
|
`POST` takes `{ plugin_key, expected_version, enabled, features }`; `features`
|
|
142
152
|
holds configured values for declared keys, missing keys keep their default and
|
|
143
153
|
an unknown key is 400. An unknown `plugin_key` is 404, a stale
|
|
@@ -196,6 +206,44 @@ base's own shop master data): the Admin shows no plugin switch, `enabled` is
|
|
|
196
206
|
always `true` whatever is stored, and `POST /admin/plugin-features` with
|
|
197
207
|
`enabled: false` is 400. It cannot be combined with `default_enabled: false`.
|
|
198
208
|
|
|
209
|
+
### Presentation fields for the Admin page
|
|
210
|
+
|
|
211
|
+
All optional; the Admin response always carries them (`null`, `[]` or
|
|
212
|
+
`"full"` when not declared). Texts are non-empty trimmed strings;
|
|
213
|
+
`definePluginFeatures` throws at import time otherwise.
|
|
214
|
+
|
|
215
|
+
- `icon`: name from `PLUGIN_ICON_NAMES` (exported by the base; every name is a
|
|
216
|
+
`@medusajs/icons` component) for the hub card and the page header. The Admin
|
|
217
|
+
falls back to a default icon for `null`.
|
|
218
|
+
- `summary`: one short sentence, hub card text and page subtitle (the Admin
|
|
219
|
+
falls back to `description`).
|
|
220
|
+
- `disable_hint`: line under the "Plugin eingeschaltet" switch, e.g. "Aus:
|
|
221
|
+
keine UID-Nummer mehr im Warenkorb."; `disable_confirm`: body of the
|
|
222
|
+
confirmation shown before the plugin is switched off. Both only for
|
|
223
|
+
switchable plugins.
|
|
224
|
+
- `features_heading` (default "Funktionen"), `features_note` (right-aligned note
|
|
225
|
+
in the features card header) and `all_off_warning` (shown while every
|
|
226
|
+
top-level feature is off; needs at least one feature).
|
|
227
|
+
- `require_enabled_feature: true`: at least one top-level feature (without
|
|
228
|
+
`parent_key`) must stay enabled. Needs at least one top-level feature with
|
|
229
|
+
`default_enabled: true`. The update route answers `400` (`INVALID_DATA`,
|
|
230
|
+
"Mindestens eine Funktion muss eingeschaltet bleiben.") and stores nothing
|
|
231
|
+
when the update switches off the last enabled top-level feature; features not
|
|
232
|
+
sent count by their default. Only such a reducing update is rejected: an
|
|
233
|
+
already stored all-off state stays readable and can be left (or re-saved
|
|
234
|
+
unchanged, e.g. by the plugin switch of a switchable plugin). The Admin locks
|
|
235
|
+
the switch of the last enabled top-level feature with a hint. The Admin DTO
|
|
236
|
+
field is `require_enabled_feature: boolean` (`false` when not declared).
|
|
237
|
+
- `setting_groups`: `{ name, description?, note?, requires_feature? }` per
|
|
238
|
+
settings group. `name` equals the `group` of at least one setting and is
|
|
239
|
+
unique; `requires_feature` is a declared feature key, the Admin dims the group
|
|
240
|
+
(still editable) while that feature is not effectively enabled.
|
|
241
|
+
- Per setting: `hint` (short text under the field, shown instead of
|
|
242
|
+
`description`), `placeholder` (not for `boolean`, `select`, `image`,
|
|
243
|
+
`color`), `width` (`"full"` default or `"half"`: consecutive half settings
|
|
244
|
+
share a two-column row; not for `boolean`) and `section` (sub-heading inside
|
|
245
|
+
the group, shared by consecutive settings with the same value).
|
|
246
|
+
|
|
199
247
|
### Gating routes
|
|
200
248
|
|
|
201
249
|
Put gate entries at the start of the plugin's `routes` array. An entry without
|
|
@@ -329,7 +377,7 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
|
|
|
329
377
|
```
|
|
330
378
|
|
|
331
379
|
- Types: `string`, `url` (http/https), `number` (finite, optional inclusive
|
|
332
|
-
`min`/`max`), `boolean`, `secret`, `select` (`options: [{ value, label }]`,
|
|
380
|
+
`min`/`max`, optional `integer: true` for whole numbers only), `boolean`, `secret`, `select` (`options: [{ value, label }]`,
|
|
333
381
|
non-empty, unique values; the stored value must be one of them), `image`
|
|
334
382
|
(the http(s) URL of a PNG/JPEG of at most 2 MiB that the Admin uploads
|
|
335
383
|
through Medusa's `/admin/uploads` into the public File Module storage and
|
|
@@ -340,7 +388,7 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
|
|
|
340
388
|
that heading whenever the group changes from one setting to the next. Keys
|
|
341
389
|
follow the feature key format and are unique. A `secret` has no default;
|
|
342
390
|
any other default must match its type, options and bounds. `options` exist
|
|
343
|
-
only on `select`, `min`/`max` only on `number`. `required` defaults to
|
|
391
|
+
only on `select`, `min`/`max`/`integer` only on `number`. `required` defaults to
|
|
344
392
|
`false`. `invalidates` (optional) lists secret settings of the same plugin
|
|
345
393
|
that a change of this setting deletes, e.g. `invalidates: ["api_key"]` on
|
|
346
394
|
`api_url` so a stored key is never sent to a new host. `definePluginFeatures`
|
|
@@ -372,7 +420,7 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
|
|
|
372
420
|
are dropped. When a setting with `invalidates` gets a different stored
|
|
373
421
|
value (also when it is cleared), the listed secrets are deleted in the same
|
|
374
422
|
transaction unless the update sets them anew; they appear in
|
|
375
|
-
`changed_keys`. Unknown keys and values of the wrong type, outside `min`/`max`
|
|
423
|
+
`changed_keys`. Unknown keys and values of the wrong type, outside `min`/`max`, a fraction for an `integer` number
|
|
376
424
|
or not among a select's options answer 400 naming the key, a stale `expected_version` 409. The response is the full
|
|
377
425
|
`AdminPluginFeaturesResponse`.
|
|
378
426
|
- After a saved update the base emits `PLUGIN_SETTINGS_UPDATED_EVENT`
|
|
@@ -548,9 +596,11 @@ only when neither plugin had one.
|
|
|
548
596
|
## Admin pages
|
|
549
597
|
|
|
550
598
|
- `/settings/hartl-services` (sidebar entry "Hartl Services Plugins") is the
|
|
551
|
-
hub:
|
|
552
|
-
|
|
553
|
-
|
|
599
|
+
hub: a banner for the Shop-Stammdaten (icon, summary, "Nicht konfiguriert"
|
|
600
|
+
while required data is missing, "Bearbeiten"), then one card per plugin with
|
|
601
|
+
its icon, `summary`, the plugin switch (not for `switchable: false`; switching
|
|
602
|
+
off asks for confirmation with `disable_confirm`), "Nicht konfiguriert"
|
|
603
|
+
while required settings are missing and "Öffnen".
|
|
554
604
|
- The Aktionen section shows per action the progress of a running run
|
|
555
605
|
("läuft seit …", bar, label; the button is disabled) and the outcome of the
|
|
556
606
|
last finished run; a finished run it watched and a second start ("läuft
|
|
@@ -559,7 +609,12 @@ only when neither plugin had one.
|
|
|
559
609
|
otherwise the base's generic page
|
|
560
610
|
`/settings/hartl-services/plugin/<plugin_key>` (Funktionen, Einstellungen,
|
|
561
611
|
Aktionen). The static `plugin` segment keeps it from colliding with
|
|
562
|
-
plugin-owned routes.
|
|
612
|
+
plugin-owned routes. The page header shows the plugin's `icon`, label,
|
|
613
|
+
`summary` (the `description` moves into an info tooltip) and status badges;
|
|
614
|
+
the breadcrumb is "Hartl Services Plugins › label". Settings are grouped by
|
|
615
|
+
`setting_groups` (one save button for the page, groups of a feature that is
|
|
616
|
+
off are dimmed), the plugin switch is the first row of the Funktionen
|
|
617
|
+
section with `disable_hint` below it.
|
|
563
618
|
- A plugin that needs more than the generic sections (its own forms, lists)
|
|
564
619
|
declares `admin_page: "/settings/hartl-services/<plugin-slug>"` and composes
|
|
565
620
|
that page from the base's components (below). Nested settings routes get no
|
|
@@ -574,8 +629,19 @@ build, with types) exports:
|
|
|
574
629
|
|
|
575
630
|
| Export | Purpose |
|
|
576
631
|
| --- | --- |
|
|
577
|
-
| `PluginPageShell({ pluginKey, children })` |
|
|
578
|
-
| `PluginFeatureSections({ pluginKey })` | Funktionen, Einstellungen, Aktionen exactly as on the generic page |
|
|
632
|
+
| `PluginPageShell({ pluginKey, variant?, children })` | Frame of a plugin page, then `children`; loading, "nicht installiert" and error states instead of `children`. `variant="back-link"` (default): back link to the hub above a title card with label, description and status chips. `variant="header"`: `AdminPageHeader` with the plugin's `icon`, `summary` and status badges, no back link; the route's `handle` carries the breadcrumb (as the generic page does) |
|
|
633
|
+
| `PluginFeatureSections({ pluginKey })` | Warnings (required settings missing, `all_off_warning`), Funktionen, Einstellungen, Aktionen exactly as on the generic page |
|
|
634
|
+
| `pluginBreadcrumb(pluginKey)`, `PluginLabelCrumb({ pluginKey })` | Route-`handle` breadcrumbs: `pluginBreadcrumb` returns "Hartl Services Plugins › plugin label" for a page of another plugin (`export const handle = { breadcrumb: () => pluginBreadcrumb("demo") }`); `PluginLabelCrumb` is the label alone, for pages whose parent crumbs the Admin already adds (same plugin) |
|
|
635
|
+
| `AdminPageHeader({ icon, title, description?, info?, badges?, actions? })` | Title container of a page: large `IconBadge`, `h1`, subtitle, an `InfoTooltip` slot and badges and actions at the end. The breadcrumb belongs in the route `handle` |
|
|
636
|
+
| `AdminSection({ title?, description?, note?, actions?, footer?, children })` | Section `Container` with dividers: optional header row (title, description, note, actions), one row per child (`SwitchRow` or own `px-6 py-5` blocks) and an optional right-aligned footer row (usually the save button) |
|
|
637
|
+
| `SwitchRow({ id, label, info?, description?, hint?, checked, onCheckedChange, disabled?, indent?, trailing? })` | One switch row for an `AdminSection`; `indent` for a child feature, `trailing` for an element after the switch (e.g. "Öffnen") |
|
|
638
|
+
| `BreadcrumbTrail({ items })`, type `BreadcrumbTrailItem` | Several crumbs (`{ label, to? }`) as ONE route-`handle` crumb, for a leaf route whose parents live in another plugin |
|
|
639
|
+
| `InfoTooltip({ label, children })` | Small info button next to a label or heading; the tooltip holds the longer explanation |
|
|
640
|
+
| `PluginIcon({ name })` | The `@medusajs/icons` icon a descriptor's `icon` names (default icon for `null` or an unknown name); for `IconBadge` / `AdminPageHeader` |
|
|
641
|
+
| `FocusModalShell({ open, onOpenChange, title, description, submitLabel, onSubmit, isPending?, submitDisabled?, cancelLabel?, children })` | Full-screen form dialog: cancel and primary button in the header, centered 720 px body for the form sections; closing is blocked while `isPending`. `title` and `description` are for screen readers |
|
|
642
|
+
| `MultiSelectDropdown({ options, value, onChange, id?, placeholder?, disabled? })`, type `MultiSelectOption` | Select for several values: chosen labels as badges in the trigger, one checkbox item per option, the menu stays open while ticking |
|
|
643
|
+
| `FileDropField({ accept, label, text?, disabled?, isLoading?, id?, onFileSelected })` | Dashed drop area for one file: file dialog button plus drag and drop; the caller validates the file (`validateFile`) |
|
|
644
|
+
| `WidgetHeader({ title, info?, actions? })` | Header of a compact side widget (order, inventory item): `h2`, optional `InfoTooltip` and actions on one line, at the top of a `Container className="divide-y p-0"` |
|
|
579
645
|
| `usePluginFeature(pluginKey)` | Query of `GET /admin/plugin-features/:plugin_key` |
|
|
580
646
|
| `pluginFeatureKeys` | Query key factory (`all`, `list()`, `detail(key)`) for invalidation after an own mutation |
|
|
581
647
|
| `usePluginFeatureEnabled(pluginKey, ...featureKeys)` | `{ enabled, isPending, isError }` from the shared list query (30 s stale time); plugin switch and every key must be effective; `enabled` is `false` while loading and on error |
|
|
@@ -609,12 +675,19 @@ build, with types) exports:
|
|
|
609
675
|
import {
|
|
610
676
|
PluginFeatureSections,
|
|
611
677
|
PluginPageShell,
|
|
678
|
+
pluginBreadcrumb,
|
|
612
679
|
} from "@hartl-services/medusa-base/admin-components"
|
|
613
680
|
|
|
614
681
|
import { DemoMappingSection } from "../../../../features/demo/DemoMappingSection"
|
|
615
682
|
|
|
683
|
+
// The Admin adds parent crumbs only for routes of the same plugin, so a page
|
|
684
|
+
// in another plugin names the hub itself.
|
|
685
|
+
export const handle = {
|
|
686
|
+
breadcrumb: () => pluginBreadcrumb("demo"),
|
|
687
|
+
}
|
|
688
|
+
|
|
616
689
|
const DemoPage = () => (
|
|
617
|
-
<PluginPageShell pluginKey="demo">
|
|
690
|
+
<PluginPageShell pluginKey="demo" variant="header">
|
|
618
691
|
<PluginFeatureSections pluginKey="demo" />
|
|
619
692
|
<DemoMappingSection />
|
|
620
693
|
</PluginPageShell>
|
|
@@ -925,29 +998,268 @@ reverse-proxy rule. The redirect is temporary on purpose: browsers cache a
|
|
|
925
998
|
should `/` ever get real content. A shop that needs its own `/` defines
|
|
926
999
|
`src/api/route.ts` itself.
|
|
927
1000
|
|
|
1001
|
+
## Kunden-Login
|
|
1002
|
+
|
|
1003
|
+
Module `customer_auth` (plugin key `customer_auth`, label "Kunden-Login")
|
|
1004
|
+
decides which login methods customers may use. The plugin cannot be switched
|
|
1005
|
+
off; it has two features:
|
|
1006
|
+
|
|
1007
|
+
| Key | Label | Default | Meaning |
|
|
1008
|
+
| ---------------- | ------------------------ | ------- | ------- |
|
|
1009
|
+
| `email_password` | E-Mail + Passwort | on | Medusa's `emailpass`: login, registration, password reset |
|
|
1010
|
+
| `email_otp` | Anmeldecode per E-Mail | off | login with a six digit code from a mail (provider `email-otp`) |
|
|
1011
|
+
|
|
1012
|
+
Existing shops are unchanged (password on, code off). At least one method
|
|
1013
|
+
must stay on (`require_enabled_feature`): the update route rejects switching
|
|
1014
|
+
off the last one with `400`, and the Admin locks that switch. Only a state
|
|
1015
|
+
stored before this rule (both off) can still exist; it stays readable, the
|
|
1016
|
+
`all_off_warning` shows in the Admin, and it can be left by switching one
|
|
1017
|
+
method on.
|
|
1018
|
+
Admin users always log in with e-mail and password. The plugin also holds the customer password rules
|
|
1019
|
+
(group "Passwort-Regeln", see "Password policy").
|
|
1020
|
+
|
|
1021
|
+
**Enforcement.** The switches are not only hidden in the storefront: the shop
|
|
1022
|
+
preset sets `projectConfig.http.authMethodsPerActor` to
|
|
1023
|
+
`{ customer: <getter>, user: ["emailpass"] }`, and Medusa answers a method that
|
|
1024
|
+
is not allowed with `400` (`NOT_ALLOWED`) on every `/auth/<actor>/<provider>`
|
|
1025
|
+
route (login, register, reset, update, callback). So with `email_password` off
|
|
1026
|
+
a customer can neither log in, register nor reset a password, and the `user`
|
|
1027
|
+
list also keeps Admin users from using `email-otp` (an actor without a list may
|
|
1028
|
+
use every registered provider). The getter answers from an in-memory copy of
|
|
1029
|
+
the effective switches with the same technique as `authVerificationsPerActor`
|
|
1030
|
+
(`createRefreshingSnapshot` in `src/shared/lib`): loaded when the application
|
|
1031
|
+
starts (the module's `onApplicationStart`), refreshed every 30 seconds by an
|
|
1032
|
+
unref'd timer and in the background when a read finds the copy older than 30
|
|
1033
|
+
seconds, kept on `globalThis`. A change in the Admin reaches a running instance
|
|
1034
|
+
within about 30 seconds (a storefront that caches `GET /store/plugin-features`
|
|
1035
|
+
for a minute follows after that). Before the first successful read and after
|
|
1036
|
+
a failed one the copy holds the descriptor defaults (`["emailpass"]`), never
|
|
1037
|
+
an empty list: an empty list blocks every customer login. It is empty only
|
|
1038
|
+
when both switches really are off. A shop with a hand-written config must set
|
|
1039
|
+
`authMethodsPerActor` and the auth module entry itself.
|
|
1040
|
+
|
|
1041
|
+
**Code login.** Two calls, no state in the storefront:
|
|
1042
|
+
|
|
1043
|
+
1. `POST /store/customer-auth/login-code` with `{ email }` (publishable key).
|
|
1044
|
+
It answers `202 { accepted: true }` at once, whatever happens next, and
|
|
1045
|
+
`404` while `email_otp` is off. The rest runs detached.
|
|
1046
|
+
2. `POST /auth/customer/email-otp` with `{ email, code }` (Medusa's login
|
|
1047
|
+
route) returns `{ token }`. A wrong, expired, used or unknown code answers
|
|
1048
|
+
`401` with one message ("Der Code ist falsch oder abgelaufen. Fordere bei
|
|
1049
|
+
Bedarf einen neuen Code an.") that never says which.
|
|
1050
|
+
|
|
1051
|
+
A new address gets an actorless token (an auth identity without a customer):
|
|
1052
|
+
the storefront creates the customer with `POST /store/customers`
|
|
1053
|
+
(`{ email }`), then calls `POST /auth/token/refresh` and uses the refreshed
|
|
1054
|
+
token, which carries the customer id. An address with an account lands in that
|
|
1055
|
+
account. The address is normalized (trimmed, lowercased) everywhere; the
|
|
1056
|
+
customer must be created with exactly that address, any other is rejected
|
|
1057
|
+
with `400 invalid_data` by a middleware on `POST /store/customers` for
|
|
1058
|
+
tokens issued by `email-otp` (Medusa takes the address from the body
|
|
1059
|
+
unchecked, so without this a token for `a@x` could create a customer for
|
|
1060
|
+
`victim@x` that the victim's later login would be linked to).
|
|
1061
|
+
|
|
1062
|
+
**The code.** Six digits from `crypto.randomInt`, valid for 10 minutes, used
|
|
1063
|
+
once, at most 3 guesses per code, at most one code per minute and address and
|
|
1064
|
+
at most 10 codes per address in a 24 hour window. It is stored only as a keyed
|
|
1065
|
+
hash (`HMAC-SHA256` over address and code) in
|
|
1066
|
+
`provider_identity.provider_metadata.login_code` of the address's `email-otp`
|
|
1067
|
+
identity (`{ hash, expires_at, attempts, sent_at }`); the issuance counter sits
|
|
1068
|
+
next to it as `login_code_budget` (`{ window_start, issued }`). A new code
|
|
1069
|
+
resets the guesses of the old one but not the budget, so an address offers at
|
|
1070
|
+
most 10 x 3 = 30 guesses per day (and 10 mails), not one per minute; the window
|
|
1071
|
+
is fixed from its first code and restarts when it has run out; a successful
|
|
1072
|
+
code login deletes the budget. When the cap is hit nothing is sent and a
|
|
1073
|
+
warning without the address is logged; the request still answers 202. Every
|
|
1074
|
+
transition is one conditional `UPDATE` in the database, so parallel requests
|
|
1075
|
+
cannot both pass: storing a new code (together with the spacing and budget
|
|
1076
|
+
rules) only succeeds if the last one is older than 55 seconds (the storefront
|
|
1077
|
+
starts its 60 second countdown at the 202, a little before `sent_at` is
|
|
1078
|
+
written; two parallel first requests send one mail), a guess is counted before
|
|
1079
|
+
it is compared, and the correct code is consumed by an `UPDATE ... WHERE hash =
|
|
1080
|
+
...` that exactly one parallel login wins. The limit lives in the provider and
|
|
1081
|
+
not in a route middleware on purpose: a middleware on
|
|
1082
|
+
`POST /auth/customer/email-otp` is bypassed by `GET` (the core route runs the
|
|
1083
|
+
same handler with the parsed body) and by other actor paths
|
|
1084
|
+
(`/auth/<anything>/email-otp`); the provider rejects any actor type but
|
|
1085
|
+
`customer` before it looks at the code. The provider uses the auth module's
|
|
1086
|
+
`PG_CONNECTION`. It supports neither register nor update, and the routes
|
|
1087
|
+
`register`, `update`, `reset-password` and `callback` below
|
|
1088
|
+
`/auth/:actor_type/email-otp` answer `404` (a middleware without `methods` in
|
|
1089
|
+
the base, so it runs before Medusa's handlers; `reset-password` would
|
|
1090
|
+
otherwise create a reset token for a code identity).
|
|
1091
|
+
|
|
1092
|
+
Addresses are printable ASCII only (`RequestLoginCodeSchema` answers `400`
|
|
1093
|
+
otherwise): the case-insensitive match in the database (`lower()`) must not
|
|
1094
|
+
depend on Unicode case folding, e.g. KELVIN SIGN (U+212A) lowercases to `k`.
|
|
1095
|
+
|
|
1096
|
+
**Key.** The HMAC key is derived with HKDF-SHA256 (`node:crypto` `hkdfSync`,
|
|
1097
|
+
`info` "email-otp-login-code") from the shop's JWT secret by one shared pure
|
|
1098
|
+
function (`hashLoginCode` in `src/customer-auth/domain/login-code.ts`). The
|
|
1099
|
+
code request reads the secret from `projectConfig.http.jwtSecret`; the
|
|
1100
|
+
`email-otp` provider has it as its `hmacSecret` option. **The two must be the
|
|
1101
|
+
same value**: the shop preset sets the option from the same `JWT_SECRET` it
|
|
1102
|
+
puts into `http.jwtSecret`. A shop with a hand-written config sets
|
|
1103
|
+
`hmacSecret: <value of http.jwtSecret>` on the provider entry in the auth
|
|
1104
|
+
module options; a mismatch means every code is rejected. Changing the JWT
|
|
1105
|
+
secret invalidates the codes in flight (10 minutes).
|
|
1106
|
+
|
|
1107
|
+
**Existing accounts (protection against account takeover).** The code login
|
|
1108
|
+
joins an existing customer account only through an `emailpass` identity with
|
|
1109
|
+
the same address (letter case ignored, `emailpass` stores the address as
|
|
1110
|
+
typed) that has a customer, never through `customer.email` alone, and only
|
|
1111
|
+
if password login is **off** in this shop, or if that address was confirmed
|
|
1112
|
+
through Medusa's auth verification (the `customer_verification` feature of the
|
|
1113
|
+
E-Mail module; `auth_verification.verified_at` of that identity and address),
|
|
1114
|
+
and only if the account's customer record (not deleted) has the same address
|
|
1115
|
+
(`lower(customer.email)`). Otherwise anyone could register an account with
|
|
1116
|
+
somebody else's address and a password of their own, and the owner of the
|
|
1117
|
+
address would land in that account the first time they ask for a code. In that
|
|
1118
|
+
case no code is sent (and nothing is created). Candidates that must not be
|
|
1119
|
+
linked (unconfirmed while password login is on, customer address different)
|
|
1120
|
+
are dropped before anything else, so a planted account cannot lock the owner
|
|
1121
|
+
out of the real one. If several candidates remain, confirmed ones are
|
|
1122
|
+
preferred, then the one whose stored address is exactly the requested
|
|
1123
|
+
spelling; if more than one is still left, no code is sent and a warning
|
|
1124
|
+
without the address is logged. A customer-less `email-otp` identity (code
|
|
1125
|
+
requested, never used, an account registered later) is removed and the code
|
|
1126
|
+
login is added to the account's auth identity. Guest customers (no account)
|
|
1127
|
+
with the same address are never touched: the new account is a separate
|
|
1128
|
+
customer, and earlier guest orders stay with the guest.
|
|
1129
|
+
|
|
1130
|
+
**Password identities of unconfirmed addresses.** After every successful code
|
|
1131
|
+
login the provider deletes (for good: the unique index on
|
|
1132
|
+
`provider_identity (entity_id, provider)` has no `deleted_at` filter, a soft
|
|
1133
|
+
delete would block a new registration) the `emailpass` identities of the
|
|
1134
|
+
logged-in auth identity whose address is not confirmed, by the same
|
|
1135
|
+
confirmation check as the linking rule, and logs that without the address. A
|
|
1136
|
+
password somebody may have planted for the address before the shop switched
|
|
1137
|
+
to the code login can then never be used, not even if password login is
|
|
1138
|
+
switched back on. The real owner of an unconfirmed password account keeps the
|
|
1139
|
+
account through the code login; the old password is gone.
|
|
1140
|
+
|
|
1141
|
+
**The mail.** Kind `auth.login_code` (see "E-Mail"), sent through the E-Mail
|
|
1142
|
+
module with the delivery path of the password reset (two attempts), but
|
|
1143
|
+
independent of the E-Mail plugin's `account_mails` feature and of its plugin
|
|
1144
|
+
switch: with the code login on, login would otherwise be impossible. The code
|
|
1145
|
+
exists only in the render context and the rendered content, never in
|
|
1146
|
+
`template_data`, the notification record or a log; failures are logged with
|
|
1147
|
+
the error class only, never the address. The template can be edited in the
|
|
1148
|
+
Admin; the `login_code` block is required.
|
|
1149
|
+
|
|
1150
|
+
**Back to password login ("Passwort vergessen").** A customer who so far only
|
|
1151
|
+
used codes has no `emailpass` identity, so Medusa's reset
|
|
1152
|
+
(`POST /auth/customer/emailpass/reset-password` with `{ identifier }`, which
|
|
1153
|
+
looks the identity up by the exact spelling and mails the address it was given)
|
|
1154
|
+
would do nothing. While `email_password` is on, a middleware in front of it
|
|
1155
|
+
(no `methods`, decoded and lowercased params like the others) prepares the
|
|
1156
|
+
request; whatever goes wrong there, the request goes on unchanged:
|
|
1157
|
+
|
|
1158
|
+
1. Exactly one `emailpass` identity with a customer matches the address (letter
|
|
1159
|
+
case ignored): the identifier becomes its stored spelling, so a differently
|
|
1160
|
+
cased input still gets the mail.
|
|
1161
|
+
2. Otherwise, if the address has a code login (`email-otp`) whose account
|
|
1162
|
+
carries a customer (not deleted) with that address, the address was proven
|
|
1163
|
+
by a code. `emailpass` identities of the address without a customer (left by
|
|
1164
|
+
a failed registration) are deleted for good first. Then an `emailpass`
|
|
1165
|
+
identity with the normalized address is added to the account, **without any
|
|
1166
|
+
password** (`emailpass` fails closed on a missing password, so nobody can
|
|
1167
|
+
log in with it before the reset sets one), together with a verified auth
|
|
1168
|
+
verification of the address (code provider `email-otp`). That verification
|
|
1169
|
+
is the same confirmation the linking rule uses: `customer_verification`
|
|
1170
|
+
never blocks the restored password login, and the clean-up after a code
|
|
1171
|
+
login does not delete the identity as unconfirmed. The identifier becomes
|
|
1172
|
+
the normalized address and Medusa's flow creates the token; the usual reset
|
|
1173
|
+
mail goes out. A parallel request that creates the identity first wins the
|
|
1174
|
+
unique index and the identity is read again.
|
|
1175
|
+
3. Anything else (unknown address, several accounts) changes nothing; Medusa
|
|
1176
|
+
answers 201 as always and sends nothing.
|
|
1177
|
+
|
|
1178
|
+
The reset then sets the first password (subject to the password rules), and
|
|
1179
|
+
password login works for the same customer. This is also the way back for a
|
|
1180
|
+
shop that returns to password-only: customers who only used codes set a
|
|
1181
|
+
password via "Passwort vergessen". The reverse protection: registering
|
|
1182
|
+
(`POST /auth/customer/emailpass/register`) an address for which any identity
|
|
1183
|
+
already belongs to a customer account (letter case ignored, password or code)
|
|
1184
|
+
answers `400` with "Zu dieser E-Mail-Adresse gibt es bereits ein Kundenkonto.
|
|
1185
|
+
Melde dich an oder setze über „Passwort vergessen?“ ein Passwort." instead of
|
|
1186
|
+
leaving a customer-less orphan identity next to the account. That reveals that
|
|
1187
|
+
an account exists, which Medusa's own refusal for an existing password account
|
|
1188
|
+
does already.
|
|
1189
|
+
|
|
1190
|
+
Not covered by the code login: a per-IP limit (the limits are per address),
|
|
1191
|
+
changing the address.
|
|
1192
|
+
|
|
1193
|
+
Storefront contract: DTOs, schemas and paths come from the Contracts package
|
|
1194
|
+
(`types/customer-auth-store`, `schemas/customer-auth`, `paths/customer-auth`);
|
|
1195
|
+
read `GET /store/plugin-features/customer_auth` and render only the enabled
|
|
1196
|
+
forms.
|
|
1197
|
+
|
|
928
1198
|
## Password policy
|
|
929
1199
|
|
|
930
1200
|
Medusa's `emailpass` provider accepts any non-empty password. The base runs a
|
|
931
1201
|
middleware on `POST /auth/:actor_type/emailpass/register` (customer sign-up,
|
|
932
|
-
Admin invite) and `POST /auth/:actor_type/emailpass/update` (password reset)
|
|
933
|
-
|
|
934
|
-
`
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1202
|
+
Admin invite) and `POST /auth/:actor_type/emailpass/update` (password reset).
|
|
1203
|
+
It is registered on `/auth/:actor_type/:auth_provider/:action` without
|
|
1204
|
+
`methods` and decides on the decoded, lowercased params, because Express
|
|
1205
|
+
matches literal path segments against the raw path: `/auth/customer/email%70ass/register`
|
|
1206
|
+
and `/auth/Customer/emailpass/register` reach Medusa's handler too, and
|
|
1207
|
+
`Customer` counts as the customer actor here.
|
|
1208
|
+
A password that breaks the rules of the actor is answered with
|
|
1209
|
+
`400 { type: "invalid_data", message }`; the one German message lists every
|
|
1210
|
+
unmet rule, e.g. `Das Passwort erfüllt nicht alle Regeln: mindestens 12
|
|
1211
|
+
Zeichen, eine Ziffer.` The check runs before Medusa's reset-token check, so a
|
|
1212
|
+
rejected password leaves the single-use reset link valid for a second attempt.
|
|
1213
|
+
|
|
1214
|
+
**Admin users** (every actor type except `customer`) always need at least 15
|
|
1215
|
+
characters, counted as Unicode code points, and no composition rules. This
|
|
1216
|
+
follows NIST SP 800-63B Rev. 4 (the US guideline for digital identity) for a
|
|
1217
|
+
password that is the only factor.
|
|
1218
|
+
|
|
1219
|
+
**Customers** follow the settings of the "Kunden-Login" plugin (group
|
|
1220
|
+
"Passwort-Regeln", Settings > "Hartl Services Plugins"). They apply only while
|
|
1221
|
+
`email_password` is on and take effect at once, without a cache:
|
|
1222
|
+
|
|
1223
|
+
| Key | Label | Default | Meaning |
|
|
1224
|
+
| ----------------------------- | ------------------------------------ | ------- | ------- |
|
|
1225
|
+
| `password_min_length` | Mindestlänge | 15 | 8 to 128 characters, whole numbers only (`integer`); NIST SP 800-63B Rev. 4 recommends at least 15 when the password is the only factor |
|
|
1226
|
+
| `password_require_mixed_case` | Groß- und Kleinbuchstaben verlangen | off | at least one uppercase and one lowercase letter |
|
|
1227
|
+
| `password_require_digit` | Ziffer verlangen | off | at least one digit |
|
|
1228
|
+
| `password_require_symbol` | Sonderzeichen verlangen | off | at least one punctuation mark or symbol (Unicode category P or S; whitespace, combining marks and invisible characters do not count) |
|
|
1229
|
+
|
|
1230
|
+
NIST SP 800-63B Rev. 4 advises against composition rules, because they lead to
|
|
1231
|
+
predictable passwords such as "Passwort1!"; a longer password protects better.
|
|
1232
|
+
The Admin texts say so. A missing or invalid stored value falls back to the
|
|
1233
|
+
default of that rule, and if the settings cannot be read at all the default
|
|
1234
|
+
policy (15 characters, no composition rules) applies and a warning is logged,
|
|
1235
|
+
so a registration is never blocked by a settings outage.
|
|
1236
|
+
|
|
1237
|
+
**Route.** `GET /store/customer-auth/password-policy` (publishable key only)
|
|
1238
|
+
answers `{ password_policy: { min_length, require_mixed_case, require_digit,
|
|
1239
|
+
require_symbol } }`, the effective customer rules, and 404 while
|
|
1240
|
+
`email_password` is off. The storefront shows them in its forms and checks
|
|
1241
|
+
them with the same functions as the backend. The types, the path
|
|
1242
|
+
(`customerAuthStorePaths.get.passwordPolicy`) and the rules
|
|
1243
|
+
(`evaluatePasswordPolicy`, `isPasswordAllowed`, `DEFAULT_CUSTOMER_PASSWORD_POLICY`,
|
|
1244
|
+
`ADMIN_PASSWORD_POLICY` in `schemas/password-policy`) come from the Contracts
|
|
1245
|
+
package.
|
|
1246
|
+
|
|
1247
|
+
**Admin passwords and the shared identity.** An `emailpass` identity is shared
|
|
1248
|
+
by all actor types: someone invited as an Admin could register through
|
|
1249
|
+
`/auth/customer/emailpass/register` with the (weaker) customer rules, log in at
|
|
1250
|
+
`/auth/user/emailpass` and accept the invite with that token, which would bind
|
|
1251
|
+
the weak password to an Admin. So the Admin login (`POST` and `GET`
|
|
1252
|
+
`/auth/user/emailpass`, decoded and lowercased like above) checks the body
|
|
1253
|
+
password against the Admin rules and answers the same `400` when the identity
|
|
1254
|
+
has no Admin yet (`app_metadata.user_id` unset). The login of an identity that
|
|
1255
|
+
already belongs to an Admin is never checked. Remaining limit, deliberately not
|
|
1256
|
+
closed: a customer could register through the Admin path with 15 characters and
|
|
1257
|
+
no composition rules although the shop asks for more; that only weakens their
|
|
1258
|
+
own password.
|
|
1259
|
+
|
|
1260
|
+
Login of a customer is not checked, so existing passwords keep working until their next
|
|
1261
|
+
reset, whatever the rules are. Accounts created without HTTP (`medusa user`,
|
|
1262
|
+
seed scripts calling the Auth module) are not checked either.
|
|
951
1263
|
|
|
952
1264
|
## Cart-promotion link repair
|
|
953
1265
|
|
|
@@ -1176,7 +1488,7 @@ feature is switched on again.
|
|
|
1176
1488
|
| --------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
|
|
1177
1489
|
| (plugin switch) | E-Mail | on | every route under `/admin/email/*` (mail log, resends, attachments; 404); jobs `email-reconcile-attempts`, `email-reconcile-pending-messages`, `email-retry-messages`; every feature below | checkout and orders; already queued messages stay stored |
|
|
1178
1490
|
| `order_mails` | Bestell-Mails | on | subscribers `order-placed`, `email-shipment-created`, `email-order-canceled`, `return-requested`, `return-received`, `documents-shipment-invoice-assignment-decided`, `documents-order-cancellation-ready`, `documents-refund-ready`; route `/admin/draft-orders/:id/order-confirmation` (404) and its draft-order widget | jobs, Admin routes, already created messages |
|
|
1179
|
-
| `account_mails` | Konto-Mails | on | subscribers `password-reset`, `user-invite`, `customer-verification` (the last one only with `customer_verification` on as well)
|
|
1491
|
+
| `account_mails` | Konto-Mails | on | subscribers `password-reset`, `user-invite`, `customer-verification` (the last one only with `customer_verification` on as well); not the login code mail, which depends on neither this feature nor the plugin switch (see "Kunden-Login") | jobs, Admin routes, already created messages |
|
|
1180
1492
|
| `customer_verification` | Kontobestätigung per E-Mail | off | Medusa's e-mail verification of customer logins (see "Kontobestätigung per E-Mail"), route `POST /store/customer-verification/resend` (404 while off), subscriber `customer-verification`; child of `account_mails` | checkout and orders; confirmed accounts stay confirmed, already created messages |
|
|
1181
1493
|
| `affiliate_reminders` | Affiliate-Mails | on | subscribers `affiliate-consignment-reminder-due` (reminders to affiliates) and `affiliate-application-submitted` (notification to the shop about a new partner application) | jobs, Admin routes, already created messages |
|
|
1182
1494
|
|
|
@@ -1190,11 +1502,13 @@ it) or fails the job run.
|
|
|
1190
1502
|
The plugin declares no settings. Its logo and colors come from the
|
|
1191
1503
|
Shop-Stammdaten (section "Erscheinungsbild").
|
|
1192
1504
|
|
|
1193
|
-
Admin surfaces of a disabled feature are hidden. The E-Mail
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1505
|
+
Admin surfaces of a disabled feature are hidden. The E-Mail overview page
|
|
1506
|
+
shows the plugin switch and the mail feature switches (section "Mails") and
|
|
1507
|
+
stays reachable while the plugin is off, so it can be switched back on; its
|
|
1508
|
+
rows are dimmed and their switches disabled then. All E-Mail sub-pages
|
|
1509
|
+
(group pages, templates, attachments, mail log) are gated on the plugin
|
|
1510
|
+
switch and show `PluginFeatureDisabledNotice` while it is off (the backend
|
|
1511
|
+
answers `404` for `/admin/email/*` then).
|
|
1198
1512
|
|
|
1199
1513
|
### Kontobestätigung per E-Mail
|
|
1200
1514
|
|
|
@@ -1462,8 +1776,8 @@ has at most 35 characters. Email runtime-validates every contract before use.
|
|
|
1462
1776
|
- The accepted kinds are exactly `order.received`, `order.received.admin`, `order.shipped`,
|
|
1463
1777
|
`order.canceled`, `order.return_requested`, `order.return_received`,
|
|
1464
1778
|
`order.refunded`, `auth.password_reset`, `auth.verification`,
|
|
1465
|
-
`
|
|
1466
|
-
and `user.invite`.
|
|
1779
|
+
`auth.login_code`, `affiliate.consignment_report_due`,
|
|
1780
|
+
`affiliate.application_submitted.admin` and `user.invite`.
|
|
1467
1781
|
- Every automatic message has a deterministic business key.
|
|
1468
1782
|
- Every placed Order creates independent customer and Shop intents. The customer
|
|
1469
1783
|
intent honors `no_notification`; the Shop intent always targets the static
|
|
@@ -1621,6 +1935,14 @@ that the mail can be ignored. The link comes from `verifyAccountUrl` plus
|
|
|
1621
1935
|
`#token=`; without that builder the message fails with `render_failed`. See
|
|
1622
1936
|
"Kontobestätigung per E-Mail".
|
|
1623
1937
|
|
|
1938
|
+
The login code (`auth.login_code`) has the subject "Dein Anmeldecode für
|
|
1939
|
+
<displayName>", the six digits in large type, "Der Code ist 10 Minuten gültig."
|
|
1940
|
+
and a note that the mail can be ignored if it was not requested. The code is
|
|
1941
|
+
passed as the ephemeral `code` (and `valid_minutes`) next to the empty
|
|
1942
|
+
persisted data and is never stored or logged; the message is ephemeral like
|
|
1943
|
+
the reset and confirmation mails. It is not gated by `account_mails` or by the
|
|
1944
|
+
plugin switch, see "Kunden-Login".
|
|
1945
|
+
|
|
1624
1946
|
The affiliate reminder (`affiliate.consignment_report_due`) greets the
|
|
1625
1947
|
affiliate by `affiliate_display_name`, names the report month ("September
|
|
1626
1948
|
2026", Austrian month names) and the due date ("10.10.2026") as calendar
|
|
@@ -1798,49 +2120,67 @@ a new UUID only after a new confirmation.
|
|
|
1798
2120
|
|
|
1799
2121
|
### Admin UI
|
|
1800
2122
|
|
|
1801
|
-
The E-Mail module has
|
|
2123
|
+
The E-Mail module has an Admin area under `/settings/hartl-services/email`
|
|
1802
2124
|
(**Settings → Hartl Services Plugins → E-Mail → Öffnen**; the descriptor's
|
|
1803
|
-
`admin_page` points the hub there) instead of a top-level sidebar item.
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
"
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2125
|
+
`admin_page` points the hub there) instead of a top-level sidebar item. The
|
|
2126
|
+
overview page shows the plugin summary with an Aktiv/Aus badge and two
|
|
2127
|
+
sections: **Mails** (the "Plugin eingeschaltet" row, then one row each for
|
|
2128
|
+
Bestell-Mails, Konto-Mails and Affiliate-Mails with its number of templates,
|
|
2129
|
+
its switch and "Öffnen" to the group page) and **Verwaltung** (links to
|
|
2130
|
+
**Anhänge** and **E-Mail-Protokoll**). Sub-pages, each with a breadcrumb:
|
|
2131
|
+
|
|
2132
|
+
- `/email/order`, `/email/account`, `/email/affiliate`: one group page each,
|
|
2133
|
+
listing the group's templates ("Standard"/"angepasst", "im Code
|
|
2134
|
+
überschrieben", "Bearbeiten" opens the template editor, see "Mail
|
|
2135
|
+
templates"). The Konto-Mails page also holds the switch "Kontobestätigung
|
|
2136
|
+
per E-Mail" (`customer_verification`, locked while `account_mails` is off).
|
|
2137
|
+
The login code mail (`auth.login_code`) is edited there too but is not
|
|
2138
|
+
stopped by `account_mails`.
|
|
2139
|
+
- `/email/attachments` (table of the documents) and
|
|
2140
|
+
`/email/attachments/:id` (detail page), see below.
|
|
2141
|
+
- `/email/log` and `/email/log/:id`: the mail log. The list provides bounded
|
|
2142
|
+
server pagination, allowlisted status/kind filters and a search by the
|
|
2143
|
+
exact resource ID (e.g. `order_…`, `cus_…`, as pasted from elsewhere);
|
|
2144
|
+
filters and page live in the URL search params, and the entry page's back
|
|
2145
|
+
link returns to the same view. The entry page shows the safe message
|
|
2146
|
+
details, the delivery attempts and an explicitly confirmed resend action.
|
|
2147
|
+
Recipient, template, and payload data are intentionally absent.
|
|
1816
2148
|
|
|
1817
2149
|
The draft-order detail page carries the **Bestellbestätigung** widget (zone
|
|
1818
2150
|
`draft_order.details.side.after`, hidden while `order_mails` is off). Its
|
|
1819
|
-
switch "
|
|
2151
|
+
switch row "Bestellbestätigung an Kunden senden" (with an info tooltip) starts off and decides whether the customer receives
|
|
1820
2152
|
the order confirmation when the draft is converted (see "Draft orders and
|
|
1821
2153
|
0 € orders").
|
|
1822
2154
|
|
|
1823
|
-
The sub-
|
|
1824
|
-
attachment library
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2155
|
+
The sub-pages under `/settings/hartl-services/email/attachments` manage the
|
|
2156
|
+
attachment library. The list is a table (name, assigned mails, current
|
|
2157
|
+
version); "Neuer Anhang" opens a full-screen form that creates a document
|
|
2158
|
+
(name, assigned mails, first PDF with its effective date). The detail page
|
|
2159
|
+
`/attachments/:id` has a **Speichern** button for name and assigned mails
|
|
2160
|
+
(no autosave): a save first reloads the attachments and applies only what
|
|
2161
|
+
the user changed, so assignments toggled meanwhile in the template editor
|
|
2162
|
+
are not overwritten, and the name is sent only when it changed. Below it the
|
|
2163
|
+
**Versionen** table ("Neue Version" opens a full-screen form; PDF of at most
|
|
2164
|
+
5 MiB) lists the versions newest effective first, marked "aktuell" or
|
|
2165
|
+
"geplant", with a download button. Every create and upload sends a fresh
|
|
2166
|
+
request UUID; downloads use the configured authenticated Admin SDK and never
|
|
2167
|
+
expose storage file IDs or URLs in the UI. There is no delete: removing every
|
|
2168
|
+
assigned mail lets a document rest.
|
|
1832
2169
|
|
|
1833
2170
|
### Mail templates
|
|
1834
2171
|
|
|
1835
|
-
All
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2172
|
+
All 13 mails (`EMAIL_EDITABLE_KINDS`) are block templates an Admin edits
|
|
2173
|
+
from the group pages of the E-Mail area (Bestell-, Konto- and Affiliate-Mails
|
|
2174
|
+
→ editor page `/settings/hartl-services/email/templates/:kind`, breadcrumb
|
|
2175
|
+
E-Mail › group › template).
|
|
2176
|
+
The editor has the subject and, for the six customer order mails, an
|
|
2177
|
+
**Anhänge** multi select on top (every tick is saved at once through
|
|
2178
|
+
`POST /admin/email/attachments/:id/mail-kinds`, one kind of one document at a
|
|
2179
|
+
time; a document being saved is locked), and below it the text editor with a
|
|
2180
|
+
toolbar, slash commands ("/" lists what the mail allows) and a drag handle to
|
|
2181
|
+
move top-level blocks, next to a live preview; a test mail can be sent from
|
|
2182
|
+
the header. The attachment detail page saves the name only when it changed
|
|
2183
|
+
and then each changed mail kind on its own.
|
|
1844
2184
|
|
|
1845
2185
|
- **Document:** a subset of the editor's ProseMirror JSON
|
|
1846
2186
|
(`EmailTemplateDocumentSchema` in Contracts): paragraphs with bold, italic,
|
|
@@ -1851,12 +2191,13 @@ and then each changed mail kind on its own).
|
|
|
1851
2191
|
`item_list` (items without prices, optional heading), `address`,
|
|
1852
2192
|
`tracking`, `button` (the mail's action link: target fixed per kind from
|
|
1853
2193
|
the data or the configuration, label editable; nothing without a URL),
|
|
1854
|
-
`link_fallback` (the plain link, HTML only), `validity
|
|
1855
|
-
partner application's text). Dynamic blocks render the mail's real data at
|
|
2194
|
+
`link_fallback` (the plain link, HTML only), `validity`, `message` (the
|
|
2195
|
+
partner application's text) and `login_code` (the six digits, large). Dynamic blocks render the mail's real data at
|
|
1856
2196
|
send time and render nothing without it. Conditions: `invoice_attached`,
|
|
1857
2197
|
`cancellation_attached`, `more_to_follow`, `return_number`. Password reset,
|
|
1858
|
-
account confirmation and admin invite must contain the `button` block
|
|
1859
|
-
(`required_blocks`); a template
|
|
2198
|
+
account confirmation and admin invite must contain the `button` block, the
|
|
2199
|
+
login code mail the `login_code` block (`required_blocks`); a template
|
|
2200
|
+
without it is rejected.
|
|
1860
2201
|
- **Placeholders** (`shop_name`, `order_number`, `customer_name`,
|
|
1861
2202
|
`return_number`, `refund_amount`, `affiliate_name`, `report_month`,
|
|
1862
2203
|
`applicant_name`, per kind as in the catalog) are escaped in
|
|
@@ -2383,8 +2724,10 @@ not import either the plugin implementation or generated `.medusa` output.
|
|
|
2383
2724
|
### Admin order detail
|
|
2384
2725
|
|
|
2385
2726
|
When Documents is enabled, the Medusa Admin order detail shows a
|
|
2386
|
-
**Dokumente** widget. It lists the issued documents for that order and
|
|
2387
|
-
|
|
2727
|
+
**Dokumente** widget. It lists the issued documents for that order (kind and
|
|
2728
|
+
number, issue date and total) with a regenerate icon button and "Ansehen" per
|
|
2729
|
+
row, which opens a private PDF preview; the explanation sits in an info
|
|
2730
|
+
tooltip next to the title. The widget obtains the PDF through the Medusa JS SDK, so
|
|
2388
2731
|
both session and bearer-token Admin authentication work without exposing a
|
|
2389
2732
|
File-provider URL.
|
|
2390
2733
|
|
|
@@ -2401,9 +2744,12 @@ different order returns `404` from the download endpoint.
|
|
|
2401
2744
|
#### Internal packing lists
|
|
2402
2745
|
|
|
2403
2746
|
The separate **Packlisten** order widget lists every physical fulfillment in
|
|
2404
|
-
creation order
|
|
2405
|
-
|
|
2406
|
-
|
|
2747
|
+
creation order, each with a status badge (Nicht erstellt, Ausstehend, Wird
|
|
2748
|
+
erstellt, Bereit, Fehlgeschlagen, Storniert), the stock location and, for a
|
|
2749
|
+
failed one, the shortened error (full text in a tooltip). An entry with a
|
|
2750
|
+
generated PDF (ready or canceled) can be previewed as a private PDF; missing
|
|
2751
|
+
or failed entries can be generated again through the same idempotent workflow
|
|
2752
|
+
used by the automatic subscriber.
|
|
2407
2753
|
|
|
2408
2754
|
- `GET /admin/orders/:id/packing-lists`
|
|
2409
2755
|
- `POST /admin/orders/:id/packing-lists/:fulfillment_id/retry`
|
|
@@ -2421,8 +2767,9 @@ queries cannot expose them accidentally.
|
|
|
2421
2767
|
|
|
2422
2768
|
#### Regenerating an invoice
|
|
2423
2769
|
|
|
2424
|
-
|
|
2425
|
-
|
|
2770
|
+
The widget offers **Rechnung ausstellen** only while the order has no
|
|
2771
|
+
invoice; once one exists, every document row has a regenerate icon button
|
|
2772
|
+
(tooltip "Regenerieren"). For an invoice this action retains the document ID, document number
|
|
2426
2773
|
and original issue date, but replaces its persisted PDF and snapshot with a
|
|
2427
2774
|
fresh render. A prior artifact that is locked by a committed communication fact
|
|
2428
2775
|
and its private File are retained permanently. Only an unlocked superseded
|
|
@@ -2902,8 +3249,8 @@ helpers above for environment parsing. It is a separate subpath because it
|
|
|
2902
3249
|
loads the Sentry logger wrapper and with it `@sentry/node`; `/config` stays
|
|
2903
3250
|
free of it.
|
|
2904
3251
|
|
|
2905
|
-
The preset registers the base (with its E-Mail, Documents and `/files`
|
|
2906
|
-
plus five feature plugins, in this order: `medusa-base`,
|
|
3252
|
+
The preset registers the base (with its E-Mail, Documents and `/files`
|
|
3253
|
+
options) plus five feature plugins, in this order: `medusa-base`,
|
|
2907
3254
|
`medusa-tax-compliance`, `medusa-free-shipping`, `medusa-batch-management`,
|
|
2908
3255
|
`medusa-affiliate`, `medusa-payload-sync`.
|
|
2909
3256
|
|
|
@@ -3012,4 +3359,6 @@ bus job policy (3 attempts, exponential backoff from 1 s), S3 cache defaults
|
|
|
3012
3359
|
automatic payment methods, the 24 h session lifetime, Admin path `/app`,
|
|
3013
3360
|
consignment reminders due on day 10, the Documents module always being part of the base, and
|
|
3014
3361
|
`http.authVerificationsPerActor.customer` (a getter that follows the E-Mail `customer_verification` toggle, see
|
|
3015
|
-
"Kontobestätigung per E-Mail").
|
|
3362
|
+
"Kontobestätigung per E-Mail"), `http.authMethodsPerActor` (customers: a getter that follows the `customer_auth` switches; Admin users: `["emailpass"]`) and the auth module entry
|
|
3363
|
+
(providers `emailpass` and `email-otp`, the latter with `hmacSecret` = `JWT_SECRET`, the same value as `http.jwtSecret`; see
|
|
3364
|
+
"Kunden-Login").
|