customer-registration 0.0.124 → 0.0.126
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/api/auth/customer/shared/complete-customer-login.js +1 -33
- package/.medusa/server/src/api/auth/customer/shared/customer-login-post.js +1 -6
- package/.medusa/server/src/api/auth/customer/shared/enforce-registration-verification.js +1 -5
- package/.medusa/server/src/api/middlewares.js +6 -2
- package/.medusa/server/src/api/store/customers/route.js +54 -18
- package/.medusa/server/src/api/store/customers/validators.js +18 -0
- package/.medusa/server/src/modules/otp-verification/service.js +1 -5
- package/README.md +588 -446
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,53 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# customer-registration
|
|
2
|
+
> Medusa v2 plugin that extends customer auth with OTP verification, phone login (`phonepass`), verified contact updates, referral linking, and account deletion workflows.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
## Plugin Overview
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
This plugin customizes Medusa customer authentication and profile flows with:
|
|
6
7
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- **Throttling & Rate Limiting**: Built-in protection against OTP spam
|
|
14
|
-
- **Database Migrations**: Automatic schema updates for verification columns
|
|
15
|
-
- **Account Deletion Request Flow**: Two-step OTP flow to request and confirm account deletion with optional cancel flow
|
|
16
|
-
- **Unified registration & login**: **`POST /auth/customer/emailpass/register`** creates an **emailpass** or **phonepass** identity (from JSON body + `registration.identifier`); **`POST /auth/customer/emailpass`** logs in with `{ email, password }` or `{ phone, password }` per `login.identifier`, or **`{ email_or_phone, password }`** (values containing `@` are treated as email; otherwise as phone)
|
|
17
|
-
- **Per-channel OTP at login**: When `registration.identifier` is **`"both"`** and `require_verification` is on, **email login** only requires **email** verified; **phone login** only requires **phone** verified — you can use either method after completing that channel’s OTP
|
|
18
|
-
- **Authenticated Contact Change**: Dedicated OTP-verified routes for updating phone or email — new value is embedded in the signed JWT, no metadata staging required
|
|
8
|
+
- OTP verification for email/phone with configurable channels (email/SMS)
|
|
9
|
+
- unified register/login routes for email and phone credentials
|
|
10
|
+
- secure contact change flow (`/store/customers/me/contact`) with OTP verification
|
|
11
|
+
- account deletion request/cancel flows with OTP and scheduled deletion job
|
|
12
|
+
- referral linkage at signup plus a customer-facing `my-referrals` endpoint
|
|
13
|
+
- password reset support for both email and phone channels
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
### Problem it solves
|
|
16
|
+
|
|
17
|
+
It adds production-oriented identity controls that are not available in default customer auth flows:
|
|
18
|
+
|
|
19
|
+
- enforce verified contact channels before login
|
|
20
|
+
- allow phone-based auth (`phonepass`) in Medusa v2
|
|
21
|
+
- avoid direct email/phone mutation without OTP proof
|
|
22
|
+
- formalize account deletion lifecycle with cancellation and delayed execution
|
|
23
|
+
|
|
24
|
+
### Medusa version
|
|
25
|
+
|
|
26
|
+
Built for **Medusa v2** (`@medusajs/framework`, `@medusajs/medusa`, `@medusajs/cli` pinned to `2.11.2`).
|
|
27
|
+
|
|
28
|
+
## Installation & Setup
|
|
29
|
+
|
|
30
|
+
### Install package
|
|
21
31
|
|
|
22
|
-
1. **Install the plugin**:
|
|
23
32
|
```bash
|
|
24
33
|
npm install customer-registration
|
|
25
34
|
```
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
or
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
yarn add customer-registration
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Register plugin in `medusa-config.ts`
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { defineConfig, Modules } from "@medusajs/framework/utils"
|
|
30
46
|
|
|
31
47
|
export default defineConfig({
|
|
48
|
+
modules: [
|
|
49
|
+
{
|
|
50
|
+
resolve: "@medusajs/medusa/auth",
|
|
51
|
+
options: {
|
|
52
|
+
providers: [
|
|
53
|
+
{
|
|
54
|
+
resolve: "customer-registration/providers/phonepass",
|
|
55
|
+
id: "phonepass",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
32
61
|
plugins: [
|
|
33
62
|
{
|
|
34
63
|
resolve: "customer-registration",
|
|
35
64
|
options: {
|
|
36
65
|
storefrontUrl: "http://localhost:8000",
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
login: {
|
|
43
|
-
identifier: "phone", // defaults to registration.identifier
|
|
44
|
-
},
|
|
45
|
-
|
|
66
|
+
registration: { identifier: "both", require_verification: true },
|
|
67
|
+
login: { identifier: "both" },
|
|
68
|
+
email_verification: { channel: "email", subject: "Verify your email" },
|
|
69
|
+
phone_verification: { channel: "sms" },
|
|
46
70
|
password_reset: {
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
template: "src/templates/emails/reset-password.html",
|
|
72
|
+
subject: "Reset Your Password",
|
|
73
|
+
sms_body: "Reset password: {{reset_url}}",
|
|
49
74
|
},
|
|
50
|
-
|
|
51
75
|
account_deletion_request: {
|
|
52
76
|
template: "src/templates/emails/account-deletion-request.html",
|
|
53
77
|
subject: "Confirm your account deletion request",
|
|
@@ -57,539 +81,657 @@ export default defineConfig({
|
|
|
57
81
|
template: "src/templates/emails/account-deletion-cancel.html",
|
|
58
82
|
subject: "Confirm cancellation of account deletion",
|
|
59
83
|
},
|
|
60
|
-
|
|
61
|
-
email_verification: {
|
|
62
|
-
channel: "email",
|
|
63
|
-
subject: "Verify your email",
|
|
64
|
-
},
|
|
65
|
-
phone_verification: {
|
|
66
|
-
channel: "sms",
|
|
67
|
-
},
|
|
68
84
|
},
|
|
69
85
|
},
|
|
70
86
|
],
|
|
71
87
|
})
|
|
72
88
|
```
|
|
73
89
|
|
|
74
|
-
|
|
90
|
+
### Run migrations
|
|
91
|
+
|
|
75
92
|
```bash
|
|
76
93
|
npx medusa db:migrate
|
|
77
94
|
```
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
---
|
|
96
|
+
## Configuration
|
|
82
97
|
|
|
83
|
-
|
|
98
|
+
### Plugin options (`options`)
|
|
99
|
+
|
|
100
|
+
| Option | Type | Required | Default | Description |
|
|
101
|
+
|---|---|---:|---|---|
|
|
102
|
+
| `storefrontUrl` | `string` | No | `undefined` | Base storefront URL used for password reset link generation. Required when `password_reset` is configured. |
|
|
103
|
+
| `registration.identifier` | `"email" \| "phone" \| "both"` | No | `"email"` | Which identifier is used at registration. |
|
|
104
|
+
| `registration.require_verification` | `boolean` | No | `true` | Whether login requires verification flags. |
|
|
105
|
+
| `login.identifier` | `"email" \| "phone" \| "both"` | No | `registration.identifier` | Which login method(s) are accepted at `/auth/customer/emailpass`. |
|
|
106
|
+
| `password_reset.template` | `string` | Conditional | `undefined` | Email HTML template path for reset email. Required if login channel includes email. |
|
|
107
|
+
| `password_reset.subject` | `string` | No | `"Reset Your Password"` | Password reset email subject. |
|
|
108
|
+
| `password_reset.sms_body` | `string` | Conditional | `undefined` | SMS template body for reset flow; supports `{{token}}`, `{{reset_url}}`, `{{phone}}`. Required if login channel includes phone. |
|
|
109
|
+
| `account_deletion_request.template` | `string` | Yes when section used | none | Template for account deletion request OTP notification. |
|
|
110
|
+
| `account_deletion_request.subject` | `string` | No | `"Confirm your account deletion request"` | Subject for request OTP notification. |
|
|
111
|
+
| `account_deletion_request.scheduled_days` | `number` | No | `7` | Days from confirmation until deletion is due. |
|
|
112
|
+
| `account_deletion_cancel.template` | `string` | Yes when section used | none | Template for cancellation OTP notification. |
|
|
113
|
+
| `account_deletion_cancel.subject` | `string` | No | `"Confirm cancellation of account deletion"` | Subject for cancellation OTP notification. |
|
|
114
|
+
|
|
115
|
+
### OTP module options (supported by OTP service)
|
|
116
|
+
|
|
117
|
+
> ⚠️ Note: these options are implemented in `OtpVerificationService`/`OtpConfig`, but are not fully declared in `CustomerRegistrationPluginOptions` type. Confirm your expected typing strategy before publishing.
|
|
118
|
+
|
|
119
|
+
| Option | Type | Required | Default | Description |
|
|
120
|
+
|---|---|---:|---|---|
|
|
121
|
+
| `otpLength` | `number` | No | `6` | OTP length. |
|
|
122
|
+
| `otpCharset` | `"numeric" \| "alphanumeric"` | No | `"numeric"` | OTP charset. |
|
|
123
|
+
| `otpExpiryMinutes` | `number` | No | `15` | OTP expiry in minutes. |
|
|
124
|
+
| `maxAttempts` | `number` | No | `5` | Max verify attempts before lockout. |
|
|
125
|
+
| `email_verification.channel` | `string` | Yes for email OTP | none | Notification channel (usually `email`). |
|
|
126
|
+
| `email_verification.template` | `string \| null` | No | `null` | Template path. |
|
|
127
|
+
| `email_verification.subject` | `string` | No | provider default | Subject for email. |
|
|
128
|
+
| `email_verification.resendThrottleSeconds` | `number` | No | `90` | Resend throttle. |
|
|
129
|
+
| `email_verification.autoSendOnRegistration` | `boolean` | No | `undefined` | Auto-send toggle (if used by caller flow). |
|
|
130
|
+
| `phone_verification.channel` | `string` | Yes for phone OTP | none | Notification channel (usually `sms`). |
|
|
131
|
+
| `phone_verification.template` | `string \| null` | No | `null` | SMS template path. |
|
|
132
|
+
| `phone_verification.resendThrottleSeconds` | `number` | No | `90` | Resend throttle. |
|
|
133
|
+
| `account_deletion_request.channel` | `string` | Yes for strict validation | fallback `email` | Channel for request OTP notifications. |
|
|
134
|
+
| `account_deletion_request.template` | `string \| null` | Yes | none | Template path. |
|
|
135
|
+
| `account_deletion_request.subject` | `string` | No | provider default | Subject. |
|
|
136
|
+
| `account_deletion_request.resendThrottleSeconds` | `number` | No | `90` | Resend throttle. |
|
|
137
|
+
| `account_deletion_cancel.channel` | `string` | Yes for strict validation | fallback `email` | Channel for cancellation OTP notifications. |
|
|
138
|
+
| `account_deletion_cancel.template` | `string \| null` | Yes | none | Template path. |
|
|
139
|
+
| `account_deletion_cancel.subject` | `string` | No | provider default | Subject. |
|
|
140
|
+
| `account_deletion_cancel.resendThrottleSeconds` | `number` | No | `90` | Resend throttle. |
|
|
141
|
+
|
|
142
|
+
### Complete config example
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
{
|
|
146
|
+
resolve: "customer-registration",
|
|
147
|
+
options: {
|
|
148
|
+
storefrontUrl: "https://store.example.com",
|
|
149
|
+
registration: {
|
|
150
|
+
identifier: "both",
|
|
151
|
+
require_verification: true,
|
|
152
|
+
},
|
|
153
|
+
login: {
|
|
154
|
+
identifier: "both",
|
|
155
|
+
},
|
|
156
|
+
otpLength: 6,
|
|
157
|
+
otpCharset: "numeric",
|
|
158
|
+
otpExpiryMinutes: 15,
|
|
159
|
+
maxAttempts: 5,
|
|
160
|
+
email_verification: {
|
|
161
|
+
channel: "email",
|
|
162
|
+
template: "src/templates/emails/otp-verification.html",
|
|
163
|
+
subject: "Verify your email",
|
|
164
|
+
resendThrottleSeconds: 90,
|
|
165
|
+
},
|
|
166
|
+
phone_verification: {
|
|
167
|
+
channel: "sms",
|
|
168
|
+
template: "src/templates/sms/otp.txt",
|
|
169
|
+
resendThrottleSeconds: 90,
|
|
170
|
+
},
|
|
171
|
+
password_reset: {
|
|
172
|
+
template: "src/templates/emails/reset-password.html",
|
|
173
|
+
subject: "Reset Your Password",
|
|
174
|
+
sms_body: "Reset password: {{reset_url}}",
|
|
175
|
+
},
|
|
176
|
+
account_deletion_request: {
|
|
177
|
+
channel: "email",
|
|
178
|
+
template: "src/templates/emails/account-deletion-request.html",
|
|
179
|
+
subject: "Confirm your account deletion request",
|
|
180
|
+
scheduled_days: 7,
|
|
181
|
+
},
|
|
182
|
+
account_deletion_cancel: {
|
|
183
|
+
channel: "email",
|
|
184
|
+
template: "src/templates/emails/account-deletion-cancel.html",
|
|
185
|
+
subject: "Confirm cancellation",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
```
|
|
84
190
|
|
|
85
|
-
|
|
191
|
+
## Environment Variables
|
|
86
192
|
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
cd plugins/customer-registration
|
|
90
|
-
npx medusa plugin:publish
|
|
91
|
-
```
|
|
193
|
+
Only one direct environment variable usage exists in plugin source:
|
|
92
194
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
npx medusa plugin:add customer-registration
|
|
97
|
-
```
|
|
195
|
+
| Variable | Required | Example | Purpose |
|
|
196
|
+
|---|---:|---|---|
|
|
197
|
+
| `NODE_ENV` | No | `production` | Used for development-only OTP config debug logging in OTP service. |
|
|
98
198
|
|
|
99
|
-
|
|
199
|
+
> ⚠️ Note: JWT secret/expiry are read from Medusa config module (`projectConfig.http.jwtSecret` / `jwtExpiresIn`), not directly from `process.env` in this plugin.
|
|
100
200
|
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
cd plugins/customer-registration
|
|
104
|
-
npx medusa plugin:develop
|
|
105
|
-
```
|
|
201
|
+
## REST APIs / Routes
|
|
106
202
|
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
cd ../../test-medusa
|
|
110
|
-
yarn dev
|
|
111
|
-
```
|
|
203
|
+
### Auth routes
|
|
112
204
|
|
|
113
|
-
|
|
205
|
+
#### `POST /auth/customer/emailpass/register`
|
|
206
|
+
- Auth: Public
|
|
207
|
+
- Description: Registers customer identity through `emailpass` or `phonepass` based on `registration.identifier`.
|
|
114
208
|
|
|
115
|
-
|
|
209
|
+
Body:
|
|
116
210
|
|
|
117
|
-
|
|
211
|
+
| Field | Type | Required | Notes |
|
|
212
|
+
|---|---|---:|---|
|
|
213
|
+
| `password` | `string` | Yes | Required always. |
|
|
214
|
+
| `email` | `string` | Conditional | Required when mode resolves to email. |
|
|
215
|
+
| `phone` | `string` | Conditional | Required when mode resolves to phone. |
|
|
118
216
|
|
|
119
|
-
|
|
217
|
+
Response:
|
|
120
218
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
| `"phone"` | Phone required; no email needed; uses `phonepass` provider |
|
|
125
|
-
| `"both"` | Both email and phone required |
|
|
219
|
+
```json
|
|
220
|
+
{ "token": "..." }
|
|
221
|
+
```
|
|
126
222
|
|
|
127
|
-
|
|
223
|
+
```bash
|
|
224
|
+
curl -X POST http://localhost:9000/auth/customer/emailpass/register \
|
|
225
|
+
-H "Content-Type: application/json" \
|
|
226
|
+
-d '{"email":"alice@example.com","password":"Secret123!"}'
|
|
227
|
+
```
|
|
128
228
|
|
|
129
|
-
|
|
229
|
+
#### `POST /auth/customer/emailpass`
|
|
230
|
+
- Auth: Public
|
|
231
|
+
- Description: Unified login endpoint for email/phone credentials.
|
|
130
232
|
|
|
131
|
-
|
|
132
|
-
|---|---|
|
|
133
|
-
| `"email"` | `POST /auth/customer/emailpass` with `{ "email", "password" }` only |
|
|
134
|
-
| `"phone"` | Same endpoint with `{ "phone", "password" }` only |
|
|
135
|
-
| `"both"` | Same endpoint; **either** email **or** phone (not both) plus `password` in the JSON body |
|
|
233
|
+
Body/query params:
|
|
136
234
|
|
|
137
|
-
|
|
235
|
+
| Field | Type | Required | Notes |
|
|
236
|
+
|---|---|---:|---|
|
|
237
|
+
| `password` | `string` | Yes | Required. |
|
|
238
|
+
| `email` | `string` | XOR | Use with password. |
|
|
239
|
+
| `phone` | `string` | XOR | Use with password. |
|
|
240
|
+
| `email_or_phone` | `string` | Optional alternative | Must not be combined with `email`/`phone`; value with `@` becomes email. |
|
|
138
241
|
|
|
139
|
-
|
|
242
|
+
Response:
|
|
140
243
|
|
|
141
|
-
|
|
244
|
+
```json
|
|
245
|
+
{ "token": "..." }
|
|
246
|
+
```
|
|
142
247
|
|
|
143
|
-
|
|
248
|
+
#### `POST /auth/customer/emailpass/reset-password`
|
|
249
|
+
- Auth: Public
|
|
250
|
+
- Description: Requests password reset for email or phone channel depending on config.
|
|
144
251
|
|
|
145
|
-
|
|
252
|
+
Body:
|
|
146
253
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
254
|
+
| Field | Type | Required | Notes |
|
|
255
|
+
|---|---|---:|---|
|
|
256
|
+
| `identifier` | `string` | Conditional | Email alias field. |
|
|
257
|
+
| `email` | `string` | Conditional | Email lookup. |
|
|
258
|
+
| `phone` | `string` | Conditional | Phone lookup. |
|
|
259
|
+
| `email_or_phone` | `string` | Conditional | Auto-resolves by `@`. |
|
|
150
260
|
|
|
151
|
-
|
|
261
|
+
Response:
|
|
152
262
|
|
|
153
|
-
|
|
263
|
+
```json
|
|
264
|
+
{}
|
|
265
|
+
```
|
|
154
266
|
|
|
155
|
-
|
|
267
|
+
#### `POST /auth/customer/emailpass/update`
|
|
268
|
+
- Auth: Public with reset token
|
|
269
|
+
- Description: Completes password reset via `Authorization: Bearer <token>`.
|
|
156
270
|
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
resolve: "customer-registration",
|
|
160
|
-
options: {
|
|
161
|
-
storefrontUrl?: string, // Base URL for password reset links
|
|
271
|
+
Body:
|
|
162
272
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
login?: {
|
|
168
|
-
identifier?: "email" | "phone" | "both", // default: registration.identifier
|
|
169
|
-
},
|
|
273
|
+
| Field | Type | Required |
|
|
274
|
+
|---|---|---:|
|
|
275
|
+
| `password` | `string` | Yes |
|
|
276
|
+
| `identifier` or `email` or `phone` or `email_or_phone` | `string` | Yes (exactly one logical lookup) |
|
|
170
277
|
|
|
171
|
-
|
|
172
|
-
otpLength?: number, // default: 6
|
|
173
|
-
otpCharset?: "numeric" | "alphanumeric", // default: "numeric"
|
|
174
|
-
otpExpiryMinutes?: number, // default: 15
|
|
175
|
-
maxAttempts?: number, // default: 5
|
|
176
|
-
|
|
177
|
-
email_verification?: {
|
|
178
|
-
channel: string, // e.g. "email"
|
|
179
|
-
template?: string,
|
|
180
|
-
subject?: string,
|
|
181
|
-
resendThrottleSeconds?: number,
|
|
182
|
-
autoSendOnRegistration?: boolean,
|
|
183
|
-
},
|
|
184
|
-
phone_verification?: {
|
|
185
|
-
channel: string, // e.g. "sms"
|
|
186
|
-
template?: string,
|
|
187
|
-
resendThrottleSeconds?: number,
|
|
188
|
-
},
|
|
278
|
+
Response:
|
|
189
279
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
subject?: string,
|
|
193
|
-
sms_body?: string, // SMS text; supports {{token}}, {{reset_url}}, {{phone}} — required if login is "phone" or "both"
|
|
194
|
-
},
|
|
195
|
-
account_deletion_request?: {
|
|
196
|
-
template: string,
|
|
197
|
-
subject?: string,
|
|
198
|
-
scheduled_days?: number, // default: 7
|
|
199
|
-
},
|
|
200
|
-
account_deletion_cancel?: {
|
|
201
|
-
template: string,
|
|
202
|
-
subject?: string,
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
}
|
|
280
|
+
```json
|
|
281
|
+
{ "message": "Password reset successfully", "success": true }
|
|
206
282
|
```
|
|
207
283
|
|
|
208
|
-
|
|
284
|
+
### Store routes
|
|
209
285
|
|
|
210
|
-
|
|
286
|
+
#### `POST /store/customers`
|
|
287
|
+
- Auth: Pre-customer auth token (registration flow)
|
|
288
|
+
- Description: Overrides default customer creation; supports `phone` and referral linking.
|
|
211
289
|
|
|
212
|
-
|
|
290
|
+
Body:
|
|
213
291
|
|
|
214
|
-
|
|
|
215
|
-
|
|
216
|
-
|
|
|
217
|
-
|
|
|
218
|
-
|
|
|
292
|
+
| Field | Type | Required | Notes |
|
|
293
|
+
|---|---|---:|---|
|
|
294
|
+
| `email` | `string` | Conditional | Depends on `registration.identifier`. |
|
|
295
|
+
| `phone` | `string` | Conditional | Depends on `registration.identifier`. |
|
|
296
|
+
| `first_name` | `string` | No | |
|
|
297
|
+
| `last_name` | `string` | No | |
|
|
298
|
+
| `company_name` | `string` | No | |
|
|
299
|
+
| `metadata` | `object` | No | `metadata.referral_code` is consumed then removed. |
|
|
300
|
+
| `referral_code` | `string` | No | Referrer customer id. |
|
|
219
301
|
|
|
220
|
-
|
|
302
|
+
Response:
|
|
221
303
|
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
{
|
|
225
|
-
"customer_id": "cus_...",
|
|
226
|
-
"type": "phone_verification" # or "email_verification"
|
|
227
|
-
}
|
|
228
|
-
# Response: { "token": "...", "expires_at": "..." }
|
|
304
|
+
```json
|
|
305
|
+
{ "customer": { "id": "cus_...", "...": "..." } }
|
|
229
306
|
```
|
|
230
307
|
|
|
231
|
-
####
|
|
308
|
+
#### `GET /store/customers/me`
|
|
309
|
+
- Auth: Customer JWT
|
|
310
|
+
- Description: Returns customer with appended `email_verified` and `phone_verified`.
|
|
232
311
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"code": "482917"
|
|
238
|
-
}
|
|
239
|
-
# Response: { "verified": true, "customer": {...}, "token": "<login jwt>", "needs_login": false }
|
|
240
|
-
```
|
|
312
|
+
#### `PATCH /store/customers/me`
|
|
313
|
+
- Auth: Customer JWT
|
|
314
|
+
- Description: Updates non-contact profile fields only.
|
|
315
|
+
- Rejects: `email`, `phone` with `NOT_ALLOWED`.
|
|
241
316
|
|
|
242
|
-
|
|
317
|
+
#### `POST /store/customers/me/contact`
|
|
318
|
+
- Auth: Customer JWT
|
|
319
|
+
- Description: Starts contact change OTP flow.
|
|
243
320
|
|
|
244
|
-
|
|
321
|
+
Body:
|
|
245
322
|
|
|
246
|
-
|
|
323
|
+
| Field | Type | Required | Notes |
|
|
324
|
+
|---|---|---:|---|
|
|
325
|
+
| `email` | `string` | XOR | Exactly one of `email`/`phone`. |
|
|
326
|
+
| `phone` | `string` | XOR | Exactly one of `email`/`phone`. |
|
|
247
327
|
|
|
248
|
-
|
|
249
|
-
|---|---|---|
|
|
250
|
-
| `/auth/customer/emailpass/register` | POST | **Register.** JSON: `password` plus identifier per `registration.identifier` — **`email`** only for `"email"` and `"both"` (for `"both"`, phone is added on `POST /store/customers`); **`phone`** only for `"phone"`. Dispatches to `register("emailpass")` or `register("phonepass")`. Response: `{ "token" }` (pre-customer JWT). See [`emailpass/register/route.ts`](src/api/auth/customer/emailpass/register/route.ts), [`parse-register-body.ts`](src/api/auth/customer/shared/parse-register-body.ts), [`complete-registration-token.ts`](src/api/auth/customer/shared/complete-registration-token.ts). |
|
|
251
|
-
| `/auth/customer/emailpass` | POST | **Login.** JSON: `password` plus **exactly one** of `email` or `phone` (XOR), **or** `email_or_phone` alone (expanded to email vs phone by `@`). Optional query params can fill missing keys (body wins). [`emailpass/route.ts`](src/api/auth/customer/emailpass/route.ts) → [`handleCustomerLogin`](src/api/auth/customer/shared/customer-login-post.ts). |
|
|
328
|
+
Response:
|
|
252
329
|
|
|
253
|
-
|
|
330
|
+
```json
|
|
331
|
+
{ "token": "...", "expires_at": "..." }
|
|
332
|
+
```
|
|
254
333
|
|
|
255
|
-
|
|
334
|
+
#### `POST /store/customers/me/contact/verify`
|
|
335
|
+
- Auth: Customer JWT
|
|
336
|
+
- Description: Verifies OTP and atomically updates contact + verification flags.
|
|
256
337
|
|
|
257
|
-
|
|
338
|
+
Body:
|
|
258
339
|
|
|
259
|
-
|
|
260
|
-
|
|
340
|
+
| Field | Type | Required |
|
|
341
|
+
|---|---|---:|
|
|
342
|
+
| `token` | `string` | Yes |
|
|
343
|
+
| `code` | `string` | Yes |
|
|
261
344
|
|
|
262
|
-
|
|
263
|
-
|---|---|---|---|
|
|
264
|
-
| `/store/customers/me/contact` | POST | Customer JWT | Request contact change — validates new value, sends OTP, returns token |
|
|
265
|
-
| `/store/customers/me/contact/verify` | POST | Customer JWT | Verify OTP and apply the change |
|
|
345
|
+
Response:
|
|
266
346
|
|
|
267
|
-
|
|
347
|
+
```json
|
|
348
|
+
{ "customer": { "id": "cus_..." }, "token": "..." }
|
|
349
|
+
```
|
|
268
350
|
|
|
269
|
-
|
|
351
|
+
#### `POST /store/customers/otp/send`
|
|
352
|
+
- Auth: Public
|
|
353
|
+
- Description: Sends OTP for verification/account deletion purpose.
|
|
270
354
|
|
|
271
|
-
|
|
272
|
-
2. `phone_verified` / `email_verified` is set to `true`
|
|
273
|
-
3. `provider_identity.entity_id` is updated for `phonepass` / `emailpass` **only when the changed field is the active `login.identifier`** — so login continues to work with the new value
|
|
355
|
+
Body:
|
|
274
356
|
|
|
275
|
-
|
|
357
|
+
| Field | Type | Required |
|
|
358
|
+
|---|---|---:|
|
|
359
|
+
| `customer_id` | `string` | Yes |
|
|
360
|
+
| `type` | `"email_verification" \| "phone_verification"` | Yes |
|
|
276
361
|
|
|
277
|
-
|
|
278
|
-
curl -X POST http://localhost:9000/store/customers/me/contact \
|
|
279
|
-
-H "Content-Type: application/json" \
|
|
280
|
-
-H "Authorization: Bearer $CUSTOMER_JWT" \
|
|
281
|
-
-d '{ "phone": "+15559998888" }'
|
|
362
|
+
Response:
|
|
282
363
|
|
|
283
|
-
|
|
284
|
-
{ "token": "
|
|
364
|
+
```json
|
|
365
|
+
{ "token": "...", "expires_at": "..." }
|
|
285
366
|
```
|
|
286
367
|
|
|
287
|
-
|
|
368
|
+
#### `POST /store/customers/otp/verify`
|
|
369
|
+
- Auth: Public
|
|
370
|
+
- Description: Verifies OTP and triggers verification workflow.
|
|
288
371
|
|
|
289
|
-
|
|
372
|
+
Body:
|
|
290
373
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
-d '{
|
|
296
|
-
"token": "<otp_token from step 1>",
|
|
297
|
-
"code": "482917"
|
|
298
|
-
}'
|
|
374
|
+
| Field | Type | Required |
|
|
375
|
+
|---|---|---:|
|
|
376
|
+
| `token` | `string` | Yes |
|
|
377
|
+
| `code` | `string` | Yes |
|
|
299
378
|
|
|
300
|
-
|
|
379
|
+
Response:
|
|
380
|
+
|
|
381
|
+
```json
|
|
301
382
|
{
|
|
302
|
-
"
|
|
303
|
-
"
|
|
383
|
+
"verified": true,
|
|
384
|
+
"customer": {},
|
|
385
|
+
"email_verified": true,
|
|
386
|
+
"token": null,
|
|
387
|
+
"needs_login": true
|
|
304
388
|
}
|
|
305
389
|
```
|
|
306
390
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
391
|
+
#### `POST /store/customers/change-password`
|
|
392
|
+
- Auth: Customer JWT
|
|
393
|
+
- Description: Changes password using old/new/confirm values.
|
|
310
394
|
|
|
311
|
-
|
|
312
|
-
- The verify route checks that `token.customer_id === auth_context.actor_id`, preventing one customer from applying another's OTP
|
|
313
|
-
- `provider_identity.entity_id` sync has a compensation function: if a later workflow step fails the `entity_id` is rolled back to the original value
|
|
314
|
-
- If the current phone/email is `null` (first-time contact assignment), the flow works identically — the field is set from null to the new value and marked verified
|
|
395
|
+
Body:
|
|
315
396
|
|
|
316
|
-
|
|
397
|
+
| Field | Type | Required |
|
|
398
|
+
|---|---|---:|
|
|
399
|
+
| `old_password` | `string` | Yes |
|
|
400
|
+
| `new_password` | `string` | Yes |
|
|
401
|
+
| `confirm_password` | `string` | Yes |
|
|
317
402
|
|
|
318
|
-
|
|
403
|
+
Response:
|
|
319
404
|
|
|
320
405
|
```json
|
|
321
|
-
{
|
|
322
|
-
"type": "not_allowed",
|
|
323
|
-
"message": "Phone changes require OTP verification. Use POST /store/customers/me/contact to request a change."
|
|
324
|
-
}
|
|
406
|
+
{ "message": "Password changed successfully", "customer_id": "cus_..." }
|
|
325
407
|
```
|
|
326
408
|
|
|
327
|
-
|
|
409
|
+
#### `POST /store/customers/account-deletion/request`
|
|
410
|
+
- Auth: Customer JWT
|
|
411
|
+
- Description: Sends OTP for deletion confirmation.
|
|
328
412
|
|
|
329
|
-
|
|
413
|
+
Body:
|
|
330
414
|
|
|
331
|
-
|
|
|
332
|
-
|
|
333
|
-
|
|
|
415
|
+
| Field | Type | Required |
|
|
416
|
+
|---|---|---:|
|
|
417
|
+
| `reason` | `string \| null` | No |
|
|
334
418
|
|
|
335
|
-
|
|
419
|
+
Response:
|
|
336
420
|
|
|
337
|
-
|
|
421
|
+
```json
|
|
422
|
+
{ "token": "...", "expires_at": "..." }
|
|
423
|
+
```
|
|
338
424
|
|
|
339
|
-
|
|
425
|
+
#### `POST /store/customers/account-deletion/confirm`
|
|
426
|
+
- Auth: Public
|
|
427
|
+
- Description: Confirms deletion request via OTP; creates confirmed request row.
|
|
340
428
|
|
|
341
|
-
|
|
342
|
-
|---|---|
|
|
343
|
-
| `email` | `email`, `identifier`, or `email_or_phone` containing `@` |
|
|
344
|
-
| `phone` | `phone` or `email_or_phone` without `@` |
|
|
345
|
-
| `both` | Any of the above |
|
|
429
|
+
Body:
|
|
346
430
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
431
|
+
| Field | Type | Required |
|
|
432
|
+
|---|---|---:|
|
|
433
|
+
| `token` | `string` | Yes |
|
|
434
|
+
| `code` | `string` | Yes |
|
|
350
435
|
|
|
351
|
-
|
|
352
|
-
|---|---|---|
|
|
353
|
-
| `/auth/customer/emailpass/reset-password` | POST | Request reset (email and/or SMS per channel) |
|
|
354
|
-
| `/auth/customer/emailpass/update` | POST | Set new password; `Authorization: Bearer <token>` + same lookup field shape as request + `password` |
|
|
436
|
+
Response:
|
|
355
437
|
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
POST /auth/customer/emailpass/update
|
|
367
|
-
Authorization: Bearer <reset_token>
|
|
368
|
-
{ "email": "customer@example.com", "password": "NewPassword123!" }
|
|
438
|
+
```json
|
|
439
|
+
{
|
|
440
|
+
"request": {
|
|
441
|
+
"id": "adr_...",
|
|
442
|
+
"customer_id": "cus_...",
|
|
443
|
+
"reason": null,
|
|
444
|
+
"deletion_scheduled_at": "...",
|
|
445
|
+
"status": "confirmed"
|
|
446
|
+
}
|
|
447
|
+
}
|
|
369
448
|
```
|
|
370
449
|
|
|
371
|
-
|
|
450
|
+
#### `POST /store/customers/account-deletion/cancel-request`
|
|
451
|
+
- Auth: Public (IP rate-limited)
|
|
452
|
+
- Description: Sends OTP for canceling an active deletion request.
|
|
372
453
|
|
|
373
|
-
|
|
454
|
+
Body:
|
|
374
455
|
|
|
375
|
-
|
|
|
376
|
-
|
|
377
|
-
|
|
|
378
|
-
|
|
|
379
|
-
| `/store/customers/account-deletion/cancel-request` | POST | — | Request cancellation (lookup by email/phone) |
|
|
380
|
-
| `/store/customers/account-deletion/cancel-confirm` | POST | — | Confirm cancellation with OTP code |
|
|
381
|
-
| `/admin/account-deletion-requests` | GET | Admin JWT | List deletion requests |
|
|
456
|
+
| Field | Type | Required |
|
|
457
|
+
|---|---|---:|
|
|
458
|
+
| `email` | `string` | Conditional |
|
|
459
|
+
| `phone` | `string` | Conditional |
|
|
382
460
|
|
|
383
|
-
|
|
384
|
-
# 1. Request deletion
|
|
385
|
-
curl -X POST http://localhost:9000/store/customers/account-deletion/request \
|
|
386
|
-
-H "Authorization: Bearer $CUSTOMER_JWT" \
|
|
387
|
-
-d '{"reason": "No longer need account"}'
|
|
388
|
-
# Response: { "token": "...", "expires_at": "..." }
|
|
389
|
-
|
|
390
|
-
# 2. Confirm deletion
|
|
391
|
-
curl -X POST http://localhost:9000/store/customers/account-deletion/confirm \
|
|
392
|
-
-d '{"token": "...", "code": "123456"}'
|
|
393
|
-
|
|
394
|
-
# 3. Request cancel (no auth; email lookup)
|
|
395
|
-
curl -X POST http://localhost:9000/store/customers/account-deletion/cancel-request \
|
|
396
|
-
-d '{"email": "customer@example.com"}'
|
|
397
|
-
|
|
398
|
-
# 4. Confirm cancel
|
|
399
|
-
curl -X POST http://localhost:9000/store/customers/account-deletion/cancel-confirm \
|
|
400
|
-
-d '{"token": "...", "code": "123456"}'
|
|
401
|
-
```
|
|
461
|
+
Response:
|
|
402
462
|
|
|
403
|
-
|
|
463
|
+
```json
|
|
464
|
+
{ "token": "...", "expires_at": "..." }
|
|
465
|
+
```
|
|
404
466
|
|
|
405
|
-
|
|
467
|
+
#### `POST /store/customers/account-deletion/cancel-confirm`
|
|
468
|
+
- Auth: Public
|
|
469
|
+
- Description: Confirms cancellation OTP and marks request canceled.
|
|
406
470
|
|
|
407
|
-
|
|
471
|
+
Body:
|
|
408
472
|
|
|
409
|
-
|
|
473
|
+
| Field | Type | Required |
|
|
474
|
+
|---|---|---:|
|
|
475
|
+
| `token` | `string` | Yes |
|
|
476
|
+
| `code` | `string` | Yes |
|
|
410
477
|
|
|
411
|
-
|
|
412
|
-
import { Modules } from "@medusajs/framework/utils"
|
|
478
|
+
Response:
|
|
413
479
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
],
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
plugins: [
|
|
429
|
-
{
|
|
430
|
-
resolve: "customer-registration",
|
|
431
|
-
options: {
|
|
432
|
-
registration: {
|
|
433
|
-
identifier: "phone",
|
|
434
|
-
require_verification: true,
|
|
435
|
-
},
|
|
436
|
-
login: {
|
|
437
|
-
identifier: "phone",
|
|
438
|
-
},
|
|
439
|
-
phone_verification: {
|
|
440
|
-
channel: "sms",
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
},
|
|
444
|
-
],
|
|
445
|
-
})
|
|
480
|
+
```json
|
|
481
|
+
{
|
|
482
|
+
"cancelled": true,
|
|
483
|
+
"request": {
|
|
484
|
+
"id": "adr_...",
|
|
485
|
+
"customer_id": "cus_...",
|
|
486
|
+
"status": "cancelled",
|
|
487
|
+
"cancelled_at": "..."
|
|
488
|
+
}
|
|
489
|
+
}
|
|
446
490
|
```
|
|
447
491
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
# Step 1 — create phonepass auth identity (unified register URL)
|
|
452
|
-
POST /auth/customer/emailpass/register
|
|
453
|
-
{ "phone": "+15551234567", "password": "SecretPass1!" }
|
|
454
|
-
# Response: { "token": "<pre-customer jwt>" }
|
|
455
|
-
|
|
456
|
-
# Step 2 — create customer record
|
|
457
|
-
POST /store/customers
|
|
458
|
-
Authorization: Bearer <token>
|
|
459
|
-
{ "phone": "+15551234567", "first_name": "Jane" }
|
|
460
|
-
|
|
461
|
-
# Step 3 — send phone OTP
|
|
462
|
-
POST /store/customers/otp/send
|
|
463
|
-
{ "customer_id": "cus_...", "type": "phone_verification" }
|
|
464
|
-
# Response: { "token": "<otp_token>", "expires_at": "..." }
|
|
465
|
-
|
|
466
|
-
# Step 4 — verify phone OTP (returns login token)
|
|
467
|
-
POST /store/customers/otp/verify
|
|
468
|
-
{ "token": "<otp_token>", "code": "482917" }
|
|
469
|
-
# Response: { "verified": true, "phone_verified": true, "token": "<login jwt>" }
|
|
470
|
-
```
|
|
492
|
+
#### `GET /store/my-referrals`
|
|
493
|
+
- Auth: Customer JWT
|
|
494
|
+
- Description: Returns customers referred by the authenticated customer.
|
|
471
495
|
|
|
472
|
-
|
|
496
|
+
Response:
|
|
473
497
|
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
498
|
+
```json
|
|
499
|
+
{
|
|
500
|
+
"children": [
|
|
501
|
+
{
|
|
502
|
+
"referral_link_id": "refl_...",
|
|
503
|
+
"referred_at": "...",
|
|
504
|
+
"parent_customers_by_level": { "1": "cus_referrer" },
|
|
505
|
+
"customer": {
|
|
506
|
+
"id": "cus_child",
|
|
507
|
+
"email": "child@example.com",
|
|
508
|
+
"first_name": null,
|
|
509
|
+
"last_name": null,
|
|
510
|
+
"phone": null,
|
|
511
|
+
"created_at": "..."
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
]
|
|
515
|
+
}
|
|
478
516
|
```
|
|
479
517
|
|
|
480
|
-
###
|
|
518
|
+
### Admin routes
|
|
481
519
|
|
|
482
|
-
|
|
520
|
+
#### `GET /admin/account-deletion-requests`
|
|
521
|
+
- Auth: Admin JWT
|
|
522
|
+
- Description: Lists account deletion requests with filtering/pagination.
|
|
483
523
|
|
|
484
|
-
|
|
485
|
-
POST /auth/customer/emailpass/reset-password
|
|
486
|
-
{ "phone": "+15551234567" }
|
|
524
|
+
Query params:
|
|
487
525
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
526
|
+
| Param | Type | Required | Default |
|
|
527
|
+
|---|---|---:|---|
|
|
528
|
+
| `status` | `pending \| confirmed \| cancelled \| completed` | No | none |
|
|
529
|
+
| `limit` | `number` | No | `20` |
|
|
530
|
+
| `offset` | `number` | No | `0` |
|
|
531
|
+
| `order` | `created_at \| updated_at \| customer_id` | No | `created_at` |
|
|
532
|
+
| `order_direction` | `ASC \| DESC` | No | `DESC` |
|
|
492
533
|
|
|
493
|
-
|
|
534
|
+
Response:
|
|
494
535
|
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
# Verify and apply
|
|
503
|
-
POST /store/customers/me/contact/verify
|
|
504
|
-
Authorization: Bearer <login jwt>
|
|
505
|
-
{ "token": "<otp_token>", "code": "739201" }
|
|
506
|
-
# Response: { "customer": { "phone": "+15559998888", ... }, "token": "<fresh jwt>" }
|
|
536
|
+
```json
|
|
537
|
+
{
|
|
538
|
+
"requests": [],
|
|
539
|
+
"count": 0,
|
|
540
|
+
"offset": 0,
|
|
541
|
+
"limit": 20
|
|
542
|
+
}
|
|
507
543
|
```
|
|
508
544
|
|
|
509
|
-
|
|
545
|
+
## Services
|
|
510
546
|
|
|
511
|
-
|
|
547
|
+
### `OtpVerificationService`
|
|
548
|
+
Manages OTP generation, validation, verification state updates, and channel config lookup.
|
|
512
549
|
|
|
513
|
-
|
|
550
|
+
Key methods:
|
|
551
|
+
- `generateOtpWithCode(input, jwtSecret)`
|
|
552
|
+
- `generateOtpForContactChange(input, jwtSecret, newValue, contactType)`
|
|
553
|
+
- `verifyOtp(input, jwtSecret)`
|
|
554
|
+
- `decodeContactChangeToken(token, jwtSecret)`
|
|
555
|
+
- `getCustomerVerificationByCustomerId(context, customerId)`
|
|
556
|
+
- `updateEmailVerified(context, customerId)`
|
|
557
|
+
- `updatePhoneVerified(context, customerId)`
|
|
514
558
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
3. **`account-deletion-request`** — Account deletion request lifecycle (pending → confirmed → completed/cancelled)
|
|
559
|
+
### `AccountDeletionRequestService`
|
|
560
|
+
Manages deletion lifecycle records and admin/job access patterns.
|
|
518
561
|
|
|
519
|
-
|
|
562
|
+
Key methods:
|
|
563
|
+
- `createConfirmed(customer_id, deletion_scheduled_at)`
|
|
564
|
+
- `cancelRequest(customer_id)`
|
|
565
|
+
- `getActiveByCustomerId(customer_id)`
|
|
566
|
+
- `hasPendingRequest(customerId)`
|
|
567
|
+
- `hasActiveRequest(customerId)`
|
|
568
|
+
- `listForAdmin(selector, listConfig)`
|
|
569
|
+
- `listDueForDeletion(limit)`
|
|
570
|
+
- `markCompleted(id)`
|
|
520
571
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
| `verify-email` | Sets `email_verified = true` on the customer record |
|
|
524
|
-
| `verify-phone` | Sets `phone_verified = true` on the customer record |
|
|
525
|
-
| `update-contact` | Updates `customer.phone` / `customer.email`, sets verified flag, syncs `provider_identity.entity_id` when applicable |
|
|
526
|
-
| `send-otp` | Resolves channel config, generates OTP, sends notification |
|
|
527
|
-
| `send-contact-change-otp` | Generates OTP with `new_value` embedded in JWT, sends to the new contact address |
|
|
528
|
-
| `change-password` | Updates customer password via the auth module |
|
|
572
|
+
### `ReferralLinkModuleService`
|
|
573
|
+
CRUD service for referral links (inherits generated methods from `MedusaService`).
|
|
529
574
|
|
|
530
|
-
|
|
575
|
+
### `PasswordManagementService`
|
|
576
|
+
Utility service for password hash lookup/verification/update against provider identities.
|
|
531
577
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
| `Migration20250120000000AddCustomerVerificationColumns` | Adds `email_verified` and `phone_verified` columns to the customer table |
|
|
535
|
-
| `Migration20250118001000CreateOtpVerificationTable` | Creates `otp_verification` table |
|
|
536
|
-
| `Migration20250221000000CreateAccountDeletionRequestTable` | Creates `account_deletion_request` table |
|
|
537
|
-
| `Migration20250221000000AddAccountDeletionOtpPurposes` | Extends `otp_verification.purpose` enum for account deletion flows |
|
|
578
|
+
### `CustomerRegistrationService`
|
|
579
|
+
Placeholder module service for registration-related extension points.
|
|
538
580
|
|
|
539
|
-
|
|
540
|
-
npx medusa db:migrate
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
## Password Reset (Helper Functions)
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
import {
|
|
547
|
-
requestPasswordReset,
|
|
548
|
-
completePasswordReset,
|
|
549
|
-
} from "customer-registration/helpers"
|
|
550
|
-
|
|
551
|
-
await requestPasswordReset(
|
|
552
|
-
{ email: "customer@example.com" },
|
|
553
|
-
{ baseUrl: "https://store.example.com", publishableApiKey: "pk_..." }
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
await requestPasswordReset(
|
|
557
|
-
{ phone: "+15551234567" },
|
|
558
|
-
{ baseUrl: "https://store.example.com", publishableApiKey: "pk_..." }
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
await completePasswordReset(
|
|
562
|
-
{
|
|
563
|
-
email: "customer@example.com",
|
|
564
|
-
password: "NewPassword123!",
|
|
565
|
-
token: "reset_token_from_email",
|
|
566
|
-
},
|
|
567
|
-
{ baseUrl: "https://store.example.com", publishableApiKey: "pk_..." }
|
|
568
|
-
)
|
|
569
|
-
```
|
|
581
|
+
## Workflows & Steps
|
|
570
582
|
|
|
571
|
-
|
|
583
|
+
### Workflows
|
|
572
584
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
-
|
|
576
|
-
-
|
|
577
|
-
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
585
|
+
| Workflow | Input | Output | Purpose |
|
|
586
|
+
|---|---|---|---|
|
|
587
|
+
| `send-otp` | `{ customer_id, type }` | `{ token, expires_at }` | Generic OTP send pipeline (customer lookup, config, token, notification). |
|
|
588
|
+
| `send-contact-change-otp` | `{ customer_id, new_value, contact_type, otp_type }` | `{ token, expires_at }` | Sends OTP to new email/phone; token embeds pending value. |
|
|
589
|
+
| `update-contact` | `{ customer_id, new_value, contact_type, login_identifier }` | `{ customer_id, customer }` | Updates contact, sets verified flags, syncs provider identity with compensation. |
|
|
590
|
+
| `verify-email` | `{ customer_id }` | `{ customer_id, email_verified, customer }` | Marks email verified and returns updated customer. |
|
|
591
|
+
| `verify-phone` | `{ customer_id, login_identifier? }` | `{ customer_id, phone_verified, customer }` | Marks phone verified and syncs `phonepass` entity id when needed. |
|
|
592
|
+
| `change-password` | `{ customer_id, old_password, new_password, confirm_password }` | `{ customer_id, success }` | Validates and updates password through auth provider. |
|
|
593
|
+
|
|
594
|
+
### Steps
|
|
595
|
+
|
|
596
|
+
- `retrieve-customer`
|
|
597
|
+
- `resolve-channel-config`
|
|
598
|
+
- `determine-contact-method`
|
|
599
|
+
- `generate-otp`
|
|
600
|
+
- `generate-contact-change-otp`
|
|
601
|
+
- `prepare-template-data`
|
|
602
|
+
- `load-template`
|
|
603
|
+
- `send-notification`
|
|
604
|
+
- `find-customer-by-email`
|
|
605
|
+
- `update-password`
|
|
606
|
+
- `sync-phonepass-entity-id`
|
|
607
|
+
|
|
608
|
+
## Subscribers / Event Hooks
|
|
609
|
+
|
|
610
|
+
### `auth.password_reset` subscriber
|
|
611
|
+
- File: `src/subscribers/password-reset.ts`
|
|
612
|
+
- Event: `auth.password_reset`
|
|
613
|
+
- Behavior: renders configured password reset template and sends email notification using notification module.
|
|
614
|
+
|
|
615
|
+
## Admin UI / Extensions
|
|
616
|
+
|
|
617
|
+
### Account Deletion Requests page
|
|
618
|
+
- File: `src/admin/routes/account-deletion-requests/page.tsx`
|
|
619
|
+
- Placement: Admin route labeled **Account Deletion Requests** (trash icon)
|
|
620
|
+
- Renders:
|
|
621
|
+
- filterable/paginated table
|
|
622
|
+
- status badges (`pending`, `confirmed`, `cancelled`, `completed`)
|
|
623
|
+
- refresh + load more interactions
|
|
624
|
+
- Data source: `GET /admin/account-deletion-requests`
|
|
625
|
+
|
|
626
|
+
## Models & Entities
|
|
627
|
+
|
|
628
|
+
### `otp_verification`
|
|
629
|
+
| Field | Type | Nullable |
|
|
630
|
+
|---|---|---:|
|
|
631
|
+
| `id` | `text` | No |
|
|
632
|
+
| `customer_id` | `text` | No |
|
|
633
|
+
| `purpose` | enum | No |
|
|
634
|
+
| `hashed_code` | `text` | No |
|
|
635
|
+
| `expires_at` | `datetime` | No |
|
|
636
|
+
| `attempts` | `number` | No |
|
|
637
|
+
| `verified_at` | `datetime` | Yes |
|
|
638
|
+
|
|
639
|
+
### `account_deletion_request`
|
|
640
|
+
| Field | Type | Nullable |
|
|
641
|
+
|---|---|---:|
|
|
642
|
+
| `id` | `text` | No |
|
|
643
|
+
| `customer_id` | `text` | No |
|
|
644
|
+
| `reason` | `text` | Yes |
|
|
645
|
+
| `deletion_scheduled_at` | `datetime` | Yes |
|
|
646
|
+
| `status` | enum | No |
|
|
647
|
+
| `cancelled_at` | `datetime` | Yes |
|
|
648
|
+
|
|
649
|
+
### `referral_link`
|
|
650
|
+
| Field | Type | Nullable |
|
|
651
|
+
|---|---|---:|
|
|
652
|
+
| `id` | `text` | No |
|
|
653
|
+
| `customer_id` | `text` | No |
|
|
654
|
+
| `referrer_id` | `text` | No |
|
|
655
|
+
| `parent_customers_by_level` | `json` | No |
|
|
656
|
+
|
|
657
|
+
### Core Medusa relationships
|
|
658
|
+
- `customer_id` fields link to Medusa `customer` table by convention.
|
|
659
|
+
- auth linkage is handled through `auth_identity.app_metadata.customer_id` and `provider_identity`.
|
|
660
|
+
|
|
661
|
+
## Jobs
|
|
662
|
+
|
|
663
|
+
### `process-account-deletions`
|
|
664
|
+
- Schedule: hourly (`0 * * * *`)
|
|
665
|
+
- Flow:
|
|
666
|
+
1. load due confirmed requests
|
|
667
|
+
2. delete auth identities for customer
|
|
668
|
+
3. delete customer
|
|
669
|
+
4. mark request as `completed`
|
|
670
|
+
|
|
671
|
+
## Use Cases & Examples
|
|
672
|
+
|
|
673
|
+
1. **Phone-first storefront signup**
|
|
674
|
+
Use `POST /auth/customer/emailpass/register` with phone+password, then `POST /store/customers`, then OTP send/verify routes.
|
|
675
|
+
|
|
676
|
+
2. **Secure email/phone change in customer profile**
|
|
677
|
+
Use `POST /store/customers/me/contact` + `POST /store/customers/me/contact/verify` instead of direct patch.
|
|
678
|
+
|
|
679
|
+
3. **Regulatory account deletion process**
|
|
680
|
+
Use request/confirm/cancel flows under `/store/customers/account-deletion/*` and rely on scheduled deletion job.
|
|
681
|
+
|
|
682
|
+
4. **Referral tree tracking for customers**
|
|
683
|
+
Pass `referral_code` on signup and fetch referrals via `GET /store/my-referrals`.
|
|
684
|
+
|
|
685
|
+
5. **Channel-aware password reset**
|
|
686
|
+
Use `/auth/customer/emailpass/reset-password` and `/auth/customer/emailpass/update` with email, phone, or `email_or_phone`.
|
|
687
|
+
|
|
688
|
+
## Troubleshooting
|
|
689
|
+
|
|
690
|
+
### `storefrontUrl is required when password_reset is configured`
|
|
691
|
+
- Cause: `password_reset` enabled without `storefrontUrl`.
|
|
692
|
+
- Fix: set `options.storefrontUrl`.
|
|
693
|
+
|
|
694
|
+
### OTP channel configuration not found
|
|
695
|
+
- Cause: missing `email_verification.channel`, `phone_verification.channel`, or account deletion channel/template.
|
|
696
|
+
- Fix: add channel config in plugin options.
|
|
697
|
+
|
|
698
|
+
### `Phone login is not enabled` / `Email login is not enabled`
|
|
699
|
+
- Cause: credential channel does not match `login.identifier`.
|
|
700
|
+
- Fix: align request payload with config, or change `login.identifier`.
|
|
701
|
+
|
|
702
|
+
### Password reset succeeds with empty `{}` but user gets no message
|
|
703
|
+
- Cause: security behavior intentionally hides account existence and may swallow notification delivery errors.
|
|
704
|
+
- Fix: verify notification module config, template path, and channel provider setup.
|
|
705
|
+
|
|
706
|
+
### Contact change verify returns unauthorized token mismatch
|
|
707
|
+
- Cause: OTP token used by different authenticated customer.
|
|
708
|
+
- Fix: ensure same customer JWT that initiated the contact-change request is used during verify.
|
|
709
|
+
|
|
710
|
+
### Account deletion routes blocked / login blocked
|
|
711
|
+
- Cause: customer has active (`pending`/`confirmed`) deletion request; guard middleware blocks most store routes.
|
|
712
|
+
- Fix: complete cancel flow (`cancel-request` + `cancel-confirm`) or allow job to complete deletion.
|
|
713
|
+
|
|
714
|
+
### `JWT secret is not configured`
|
|
715
|
+
- Cause: Medusa HTTP JWT config missing.
|
|
716
|
+
- Fix: configure `projectConfig.http.jwtSecret` in Medusa app config.
|
|
717
|
+
|
|
718
|
+
## Helper Utilities
|
|
719
|
+
|
|
720
|
+
The package exports helper functions from `customer-registration/helpers`:
|
|
721
|
+
|
|
722
|
+
- `requestPasswordReset(input, options)`
|
|
723
|
+
- `completePasswordReset(input, options)`
|
|
724
|
+
- `createRequestPasswordReset(options)`
|
|
725
|
+
- `createCompletePasswordReset(options)`
|
|
726
|
+
|
|
727
|
+
These support Medusa SDK client mode or direct fetch mode with optional `x-publishable-api-key`.
|
|
728
|
+
|
|
729
|
+
## Migrations Included
|
|
730
|
+
|
|
731
|
+
- `Migration20250118001000CreateOtpVerificationTable`
|
|
732
|
+
- `Migration20250120000000RemoveForgotPasswordFromOtpPurpose`
|
|
733
|
+
- `Migration20250221000000AddAccountDeletionOtpPurposes`
|
|
734
|
+
- `Migration20250120000000AddCustomerVerificationColumns`
|
|
735
|
+
- `Migration20250221000000CreateAccountDeletionRequestTable`
|
|
736
|
+
- `Migration20250221100000AddCompletedStatusToAccountDeletionRequest`
|
|
737
|
+
- `Migration20260502100000CreateReferralLinkTable`
|