@hartl-services/medusa-base 2.6.1 → 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.
Files changed (148) hide show
  1. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +3 -2
  2. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.js +4 -4
  3. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginActionsSection.js +4 -4
  4. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.d.ts +16 -0
  5. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.js +25 -0
  6. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginDetailPage.js +1 -1
  7. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +5 -4
  8. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.js +8 -5
  9. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +4 -4
  10. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.js +24 -27
  11. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +2 -2
  12. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +9 -9
  13. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.d.ts +5 -1
  14. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.js +11 -15
  15. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.d.ts +10 -4
  16. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.js +10 -5
  17. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +0 -1
  18. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.js +73 -26
  19. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -1
  20. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -6
  21. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.d.ts +11 -0
  22. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.js +15 -0
  23. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +36 -5
  24. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +144 -20
  25. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +36 -5
  26. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.d.ts +15 -0
  27. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.js +66 -0
  28. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +144 -20
  29. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +49 -0
  30. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.js +72 -0
  31. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.d.ts +1 -12
  32. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.js +4 -20
  33. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.d.ts +43 -0
  34. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.js +66 -0
  35. package/.medusa/server/src/admin/admin-components/i18n/texts.d.ts +22 -2
  36. package/.medusa/server/src/admin/admin-components/i18n/texts.js +29 -7
  37. package/.medusa/server/src/admin/admin-components/index.d.ts +12 -0
  38. package/.medusa/server/src/admin/admin-components/index.js +11 -0
  39. package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.d.ts +22 -0
  40. package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.js +8 -0
  41. package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.d.ts +26 -0
  42. package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.js +12 -0
  43. package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.d.ts +23 -0
  44. package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.js +20 -0
  45. package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.d.ts +21 -0
  46. package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.js +33 -0
  47. package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.d.ts +27 -0
  48. package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.js +13 -0
  49. package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.d.ts +13 -0
  50. package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.js +8 -0
  51. package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.d.ts +22 -0
  52. package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.js +14 -0
  53. package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.d.ts +10 -0
  54. package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.js +10 -0
  55. package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.d.ts +27 -0
  56. package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.js +15 -0
  57. package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.d.ts +15 -0
  58. package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.js +8 -0
  59. package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.d.ts +13 -0
  60. package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.js +16 -0
  61. package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.d.ts +13 -0
  62. package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.js +11 -0
  63. package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.d.ts +12 -0
  64. package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.js +29 -0
  65. package/.medusa/server/src/admin/index.js +3118 -2020
  66. package/.medusa/server/src/admin/index.mjs +3121 -2023
  67. package/.medusa/server/src/api/admin-first-login-password-policy.js +56 -0
  68. package/.medusa/server/src/api/answer-actor-type-casing-not-found.js +26 -0
  69. package/.medusa/server/src/api/answer-code-provider-route-not-found.js +34 -0
  70. package/.medusa/server/src/api/code-account-password-routes.js +92 -0
  71. package/.medusa/server/src/api/middlewares.js +82 -17
  72. package/.medusa/server/src/api/password-policy.js +55 -10
  73. package/.medusa/server/src/api/require-login-code-customer-email.js +40 -0
  74. package/.medusa/server/src/api/store/customer-auth/login-code/route.js +17 -0
  75. package/.medusa/server/src/api/store/customer-auth/password-policy/route.js +19 -0
  76. package/.medusa/server/src/customer-auth/application/prepare-code-account-password-reset.js +111 -0
  77. package/.medusa/server/src/customer-auth/application/request-login-code.js +145 -0
  78. package/.medusa/server/src/customer-auth/customer-auth-methods-snapshot.js +55 -0
  79. package/.medusa/server/src/customer-auth/customer-password-policy.js +40 -0
  80. package/.medusa/server/src/customer-auth/domain/account-linking.js +53 -0
  81. package/.medusa/server/src/customer-auth/domain/auth-methods.js +32 -0
  82. package/.medusa/server/src/customer-auth/domain/login-code.js +39 -0
  83. package/.medusa/server/src/customer-auth/login-code-accounts.js +146 -0
  84. package/.medusa/server/src/customer-auth/login-code-state.js +139 -0
  85. package/.medusa/server/src/customer-auth/plugin-features.js +80 -0
  86. package/.medusa/server/src/customer-auth/remove-provider-identity.js +18 -0
  87. package/.medusa/server/src/customer-auth/start-customer-auth-methods.js +20 -0
  88. package/.medusa/server/src/documents/plugin-features.js +8 -2
  89. package/.medusa/server/src/email/application/deliver-ephemeral-auth-mail.js +186 -0
  90. package/.medusa/server/src/email/application/deliver-login-code.js +89 -0
  91. package/.medusa/server/src/email/application/handle-customer-verification-event.js +29 -168
  92. package/.medusa/server/src/email/application/handle-password-reset-event.js +27 -166
  93. package/.medusa/server/src/email/customer-verification-snapshot.js +20 -108
  94. package/.medusa/server/src/email/domain/business-keys.js +15 -2
  95. package/.medusa/server/src/email/plugin-features.js +8 -3
  96. package/.medusa/server/src/email/rendering/default-templates.js +2 -1
  97. package/.medusa/server/src/email/templates/blocks/dynamic-blocks.js +19 -1
  98. package/.medusa/server/src/email/templates/defaults/documents.js +7 -1
  99. package/.medusa/server/src/email/templates/defaults/subjects.js +3 -1
  100. package/.medusa/server/src/email/templates/defaults/texts.js +5 -2
  101. package/.medusa/server/src/email/templates/render.js +2 -1
  102. package/.medusa/server/src/email/templates/sample-data.js +2 -1
  103. package/.medusa/server/src/email/templates/validate.js +8 -3
  104. package/.medusa/server/src/email/types/email-kinds.js +4 -2
  105. package/.medusa/server/src/email/types/template-data.js +9 -1
  106. package/.medusa/server/src/modules/customer-auth/index.js +14 -0
  107. package/.medusa/server/src/modules/customer-auth/service.js +35 -0
  108. package/.medusa/server/src/modules/email/migrations/Migration20261010090000.js +20 -0
  109. package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +4 -1
  110. package/.medusa/server/src/modules/plugin-features/service.js +26 -1
  111. package/.medusa/server/src/providers/email-otp/index.js +8 -0
  112. package/.medusa/server/src/providers/email-otp/service.js +96 -0
  113. package/.medusa/server/src/public/descriptor.js +117 -5
  114. package/.medusa/server/src/public/effective-features.js +22 -1
  115. package/.medusa/server/src/public/index.js +4 -2
  116. package/.medusa/server/src/public/plugin-icons.js +28 -0
  117. package/.medusa/server/src/public/shop-master-data.js +33 -14
  118. package/.medusa/server/src/shared/lib/index.js +2 -1
  119. package/.medusa/server/src/shared/lib/refreshing-snapshot.js +122 -0
  120. package/.medusa/server/src/shop-config/build-food-shop-config.js +38 -1
  121. package/README.md +436 -87
  122. package/dist/customer-auth/customer-auth-methods-snapshot.d.ts +25 -0
  123. package/dist/customer-auth/customer-auth-methods-snapshot.js +54 -0
  124. package/dist/customer-auth/domain/auth-methods.d.ts +16 -0
  125. package/dist/customer-auth/domain/auth-methods.js +31 -0
  126. package/dist/customer-auth/plugin-features.d.ts +2 -0
  127. package/dist/customer-auth/plugin-features.js +79 -0
  128. package/dist/email/customer-verification-snapshot.d.ts +3 -15
  129. package/dist/email/customer-verification-snapshot.js +19 -107
  130. package/dist/email/types/email-kinds.d.ts +4 -3
  131. package/dist/email/types/email-kinds.js +3 -1
  132. package/dist/email/types/template-data.d.ts +8 -0
  133. package/dist/email/types/template-data.js +8 -0
  134. package/dist/public/descriptor.js +116 -4
  135. package/dist/public/effective-features.d.ts +10 -0
  136. package/dist/public/effective-features.js +21 -0
  137. package/dist/public/index.d.ts +2 -1
  138. package/dist/public/index.js +3 -1
  139. package/dist/public/plugin-icons.d.ts +7 -0
  140. package/dist/public/plugin-icons.js +27 -0
  141. package/dist/public/shop-master-data.js +32 -13
  142. package/dist/public/types.d.ts +63 -1
  143. package/dist/shared/lib/index.d.ts +1 -0
  144. package/dist/shared/lib/index.js +1 -0
  145. package/dist/shared/lib/refreshing-snapshot.d.ts +52 -0
  146. package/dist/shared/lib/refreshing-snapshot.js +121 -0
  147. package/dist/shop-config/build-food-shop-config.js +37 -0
  148. 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 customers and Admin users
44
- (see "Password policy").
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`, so the list is one round trip for the hub.
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: one card per plugin with status chips, the plugin switch (not for
552
- `switchable: false`) and "Öffnen"; non-switchable plugins (Shop-Stammdaten)
553
- come first.
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 })` | Back link to the hub, the plugin's label, description and status chips, then `children`; loading, "nicht installiert" and error states instead of `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
- and answers a password shorter than 15 characters with
934
- `400 { type: "invalid_data", message: "Das Passwort muss mindestens 15 Zeichen lang sein." }`.
935
- It applies to every actor type, so customers and Admin users alike. The check
936
- runs before Medusa's reset-token check, so a rejected password leaves the
937
- single-use reset link valid for a second attempt.
938
-
939
- The rule follows NIST SP 800-63B Rev. 4 for a password that is the only
940
- factor: at least 15 characters, counted as Unicode code points, and no
941
- composition rules (digits and special characters are allowed, never required).
942
- The value and the check come from the Contracts package
943
- (`schemas/password-policy`: `PASSWORD_MIN_LENGTH`, `isPasswordLongEnough`), so
944
- a storefront validates its forms with the same rule.
945
-
946
- Login is not checked, so existing shorter passwords keep working until their
947
- next reset. Accounts created without HTTP (`medusa user`, seed scripts calling
948
- the Auth module) are not checked either.
949
-
950
- Nothing to configure.
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) | jobs, Admin routes, already created messages |
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 settings page
1194
- keeps `PluginFeatureSections` visible (so the plugin can be switched back on)
1195
- and gates the Anhänge card above it, the mail log section below it and the
1196
- Anhänge sub-page on the plugin switch, showing `PluginFeatureDisabledNotice`
1197
- in place of the mail log and the sub-page while it is off.
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
- `affiliate.consignment_report_due`, `affiliate.application_submitted.admin`
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 one Admin page, `/settings/hartl-services/email`
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. It
1804
- composes the base's `PluginPageShell` and `PluginFeatureSections` from
1805
- `@hartl-services/medusa-base/admin-components` (the toggles above). Above
1806
- them sits the **Anhänge** card (documents and the mails they are assigned to,
1807
- "Bearbeiten" opens the sub-page) and the **Vorlagen** card (the editable
1808
- mails with "Standard"/"angepasst", "Bearbeiten" opens the template editor,
1809
- see "Mail templates"); below them, as the last section, the
1810
- **E-Mail-Protokoll**. The mail
1811
- log provides bounded server pagination, allowlisted status/kind filters, a
1812
- search by the exact resource ID (e.g. `order_…`, `cus_…`, as pasted from
1813
- elsewhere), safe message and attempt status, and an explicitly confirmed
1814
- resend action.
1815
- Recipient, template, and payload data are intentionally absent from the page.
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 "An Kunden senden" starts off and decides whether the customer receives
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-page `/settings/hartl-services/email/attachments` manages the
1824
- attachment library: "Neuer Anhang" creates a document (name, assigned mails,
1825
- first PDF with its effective date), each document can be renamed and
1826
- reassigned ("Bearbeiten") and gets further versions ("Neue Version"; PDF of
1827
- at most 5 MiB). Versions are listed newest effective first, marked
1828
- "aktuell" or "geplant", and can be downloaded. Every create and upload sends
1829
- a fresh request UUID; downloads use the configured authenticated Admin SDK
1830
- and never expose storage file IDs or URLs in the UI. There is no delete:
1831
- removing every assigned mail lets a document rest.
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 twelve mails (`EMAIL_EDITABLE_KINDS`) are block templates an Admin edits
1836
- on the E-Mail page (card **Vorlagen**, grouped into Bestell-, Konto- and
1837
- Affiliate-Mails → editor page `/settings/hartl-services/email/templates/:kind`).
1838
- The editor offers a toolbar, slash commands ("/" lists what the mail
1839
- allows), a drag handle to move top-level blocks, a live preview, a test mail
1840
- and, for the six customer order mails, the attachment checkboxes of the
1841
- mail (`POST /admin/email/attachments/:id/mail-kinds`, one kind at a time;
1842
- the Anhänge page's edit dialog likewise saves the name only when it changed
1843
- and then each changed mail kind on its own).
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` and `message` (the
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 without it is rejected.
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 opens a
2387
- private PDF preview. The widget obtains the PDF through the Medusa JS SDK, so
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. A ready entry can be previewed and downloaded as a private PDF;
2405
- missing or failed entries can be generated again through the same idempotent
2406
- workflow used by the automatic subscriber.
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
- Once an invoice exists, the widget changes **Rechnung ausstellen** to
2425
- **Rechnung regenerieren**. This action retains the document ID, document number
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` options)
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").