@main12/auth-login 0.3.0 → 0.3.2
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/README.md +254 -43
- package/dist/auth/application/hooks/useLoginFlow.js +18 -2
- package/dist/auth/application/hooks/useVerifyOtpFlow.js +17 -2
- package/dist/auth/application/services/authService.d.ts +1 -0
- package/dist/auth/application/services/authService.js +2 -1
- package/dist/components/AuthClientInit.d.ts +27 -0
- package/dist/components/AuthClientInit.js +29 -0
- package/dist/components/AuthLayout.js +8 -2
- package/dist/components/AuthPages.d.ts +11 -1
- package/dist/components/AuthPages.js +20 -2
- package/dist/components/AuthPagesServer.d.ts +17 -0
- package/dist/components/AuthPagesServer.js +30 -0
- package/dist/components/pages/ForgotPasswordPageHero.js +1 -1
- package/dist/components/pages/LoginPageHero.js +2 -2
- package/dist/components/pages/LoginPageTailwind.js +25 -6
- package/dist/components/pages/SetPasswordPageHero.js +4 -2
- package/dist/components/pages/SignupPageTailwind.js +23 -4
- package/dist/components/pages/VerifyOtpPageHero.js +1 -0
- package/dist/components/ui/index.js +4 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +5 -1
- package/dist/endpoints/authEndpoints.js +113 -95
- package/dist/exports/client.d.ts +2 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/rsc.d.ts +2 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/index.d.ts +35 -1
- package/dist/index.js +105 -19
- package/dist/proxy.js +18 -3
- package/package.json +23 -50
- package/dist/endpoints/googleOAuth.d.ts +0 -10
- package/dist/endpoints/googleOAuth.js +0 -144
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ plugins: [
|
|
|
10
10
|
authLoginPlugin({
|
|
11
11
|
projectName: 'My SaaS',
|
|
12
12
|
domain: 'https://myapp.com',
|
|
13
|
-
logo: 'https://myapp.com/logo.png',
|
|
14
|
-
style: '
|
|
13
|
+
logo: 'https://myapp.com/logo.png',
|
|
14
|
+
style: 'tailwind',
|
|
15
|
+
routeRedirects: true,
|
|
15
16
|
}),
|
|
16
17
|
]
|
|
17
18
|
```
|
|
@@ -21,12 +22,19 @@ plugins: [
|
|
|
21
22
|
## Features
|
|
22
23
|
|
|
23
24
|
- **5 auth pages** — login (multi-step), signup, forgot password, verify OTP, set password
|
|
25
|
+
- **Single catch-all route** — one file handles all auth pages (`AuthPages` component)
|
|
26
|
+
- **Route redirects** — automatic `/login` → `/auth/login`, `/admin/login` → `/auth/login` via Next.js 16 proxy
|
|
24
27
|
- **Multi-style** — `tailwind` (zero UI deps) or `hero-ui` (HeroUI + framer-motion). Set once in config
|
|
28
|
+
- **Google OAuth** — optional, enable via `providers` config (hidden by default)
|
|
25
29
|
- **5 API endpoints** — `check-email`, `otp/send`, `otp/verify`, `set-password`, `signup`
|
|
26
30
|
- **OTP engine** — SHA-256 hashing + `timingSafeEqual` comparison, 10-min expiry, 3 attempts
|
|
27
31
|
- **Email templates** — welcome, OTP login, password reset, password changed (EN/ES)
|
|
28
32
|
- **Powered by Main 12** — bundled inline SVG, linked to main12.com by default (URL overridable)
|
|
29
33
|
- **Global logo** — pass once in plugin config, automatically shown on all pages and email headers
|
|
34
|
+
- **Configurable login methods** — `passwordLogin` and `otpLogin` options for flexible auth flows
|
|
35
|
+
- **Auto-verify OTP** — automatically verifies when all 6 digits are entered
|
|
36
|
+
- **Smart redirects** — logged-in users are redirected away from auth pages
|
|
37
|
+
- **Google account picker** — always shows account selection by default (`prompt: 'select_account'`)
|
|
30
38
|
- **Zero runtime deps** (Tailwind mode) — Next.js + React are peer dependencies
|
|
31
39
|
|
|
32
40
|
---
|
|
@@ -41,6 +49,20 @@ pnpm add @main12/auth-login
|
|
|
41
49
|
|
|
42
50
|
No extra dependencies needed. The Tailwind style uses standard utility classes that work in any Tailwind project, including ShadCN-based ones.
|
|
43
51
|
|
|
52
|
+
If you are using Tailwind CSS in your host app, add the plugin to your Tailwind source scanning so custom utility classes compile properly:
|
|
53
|
+
|
|
54
|
+
- **Tailwind v4** (in your `globals.css`):
|
|
55
|
+
```css
|
|
56
|
+
@source './node_modules/@main12/auth-login/**/*.{js,ts,jsx,tsx}';
|
|
57
|
+
```
|
|
58
|
+
- **Tailwind v3** (in your `tailwind.config.js`):
|
|
59
|
+
```js
|
|
60
|
+
content: [
|
|
61
|
+
'./node_modules/@main12/auth-login/**/*.{js,ts,jsx,tsx}',
|
|
62
|
+
'./src/**/*.{js,ts,jsx,tsx}',
|
|
63
|
+
]
|
|
64
|
+
```
|
|
65
|
+
|
|
44
66
|
### HeroUI mode
|
|
45
67
|
|
|
46
68
|
```bash
|
|
@@ -49,11 +71,14 @@ pnpm add @heroui/react framer-motion @iconify/react
|
|
|
49
71
|
|
|
50
72
|
---
|
|
51
73
|
|
|
52
|
-
## Quick Start
|
|
74
|
+
## Quick Start (Simplified Setup)
|
|
75
|
+
|
|
76
|
+
The fastest way to get all auth pages running with just **2 files**.
|
|
53
77
|
|
|
54
78
|
### 1. Add the plugin + Users collection
|
|
55
79
|
|
|
56
80
|
```ts
|
|
81
|
+
// payload.config.ts
|
|
57
82
|
import { authLoginPlugin } from '@main12/auth-login'
|
|
58
83
|
|
|
59
84
|
export default buildConfig({
|
|
@@ -63,9 +88,6 @@ export default buildConfig({
|
|
|
63
88
|
auth: { tokenExpiration: 7200, verify: false, maxLoginAttempts: 5 },
|
|
64
89
|
fields: [
|
|
65
90
|
{ name: 'name', type: 'text' },
|
|
66
|
-
{ name: 'otpHash', type: 'text', admin: { hidden: true } },
|
|
67
|
-
{ name: 'otpAttempts', type: 'number', admin: { hidden: true } },
|
|
68
|
-
{ name: 'otpExpiresAt', type: 'text', admin: { hidden: true } },
|
|
69
91
|
],
|
|
70
92
|
},
|
|
71
93
|
],
|
|
@@ -74,45 +96,52 @@ export default buildConfig({
|
|
|
74
96
|
projectName: 'My App',
|
|
75
97
|
domain: 'https://myapp.com',
|
|
76
98
|
logo: '/logo.png',
|
|
77
|
-
style: '
|
|
99
|
+
style: 'tailwind',
|
|
100
|
+
routeRedirects: true, // enables /login → /auth/login redirects
|
|
78
101
|
}),
|
|
79
102
|
],
|
|
80
103
|
})
|
|
81
104
|
```
|
|
82
105
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```tsx
|
|
86
|
-
// src/app/(frontend)/(auth)/login/page.tsx
|
|
87
|
-
export { LoginPage as default } from '@main12/auth-login/client'
|
|
106
|
+
> **Note:** OTP data is stored in a hidden `auth-otps` collection that the plugin registers automatically. No OTP fields needed on your `users` collection.
|
|
88
107
|
|
|
89
|
-
|
|
90
|
-
export { SignupPage as default } from '@main12/auth-login/client'
|
|
108
|
+
### 2. Create the catch-all auth route (1 file)
|
|
91
109
|
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
```tsx
|
|
111
|
+
// src/app/(frontend)/(auth)/auth/[...slug]/page.tsx
|
|
112
|
+
'use client'
|
|
94
113
|
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
import { use } from 'react'
|
|
115
|
+
import { AuthPages } from '@main12/auth-login/client'
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
export default function Page({ params }: { params: Promise<{ slug: string[] }> }) {
|
|
118
|
+
const { slug } = use(params)
|
|
119
|
+
return <AuthPages slug={slug} />
|
|
120
|
+
}
|
|
100
121
|
```
|
|
101
122
|
|
|
102
|
-
|
|
123
|
+
This single file handles all routes:
|
|
124
|
+
- `/auth/login`
|
|
125
|
+
- `/auth/signup`
|
|
126
|
+
- `/auth/forgot-password`
|
|
127
|
+
- `/auth/verify-otp`
|
|
128
|
+
- `/auth/set-password`
|
|
103
129
|
|
|
104
|
-
|
|
105
|
-
// login/page.tsx
|
|
106
|
-
'use client'
|
|
107
|
-
import { LoginPage } from '@main12/auth-login/client'
|
|
108
|
-
import { useAuth } from '@/providers/Auth'
|
|
130
|
+
### 3. Add the proxy for route redirects (1 file)
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
132
|
+
```ts
|
|
133
|
+
// src/proxy.ts (Next.js 16+)
|
|
134
|
+
export { proxy, config } from '@main12/auth-login/proxy'
|
|
114
135
|
```
|
|
115
136
|
|
|
137
|
+
This redirects:
|
|
138
|
+
- `/login` → `/auth/login`
|
|
139
|
+
- `/signup` → `/auth/signup`
|
|
140
|
+
- `/forgot-password` → `/auth/forgot-password`
|
|
141
|
+
- `/verify-otp` → `/auth/verify-otp`
|
|
142
|
+
- `/set-password` → `/auth/set-password`
|
|
143
|
+
- `/admin/login` → `/auth/login` (**always**, regardless of `routeRedirects` setting)
|
|
144
|
+
|
|
116
145
|
### 4. Visit `/login` — done.
|
|
117
146
|
|
|
118
147
|
---
|
|
@@ -121,26 +150,163 @@ export default function Page() {
|
|
|
121
150
|
|
|
122
151
|
```ts
|
|
123
152
|
authLoginPlugin({
|
|
153
|
+
// Core
|
|
124
154
|
projectName: 'My App', // Email subjects + footers
|
|
125
155
|
contactEmail: 'hi@myapp.com', // Email footer contact
|
|
126
156
|
domain: 'https://myapp.com', // Links in emails
|
|
127
157
|
logo: '/logo.png', // All auth pages + email headers
|
|
128
158
|
style: 'tailwind', // 'tailwind' | 'hero-ui'
|
|
129
159
|
enabled: true,
|
|
160
|
+
|
|
161
|
+
// Login methods
|
|
162
|
+
passwordLogin: true, // Allow password-based login (default: true)
|
|
163
|
+
otpLogin: true, // Allow OTP-based login (default: true)
|
|
164
|
+
|
|
165
|
+
// Route redirects
|
|
166
|
+
routeRedirects: true, // Enable /login → /auth/login redirects
|
|
167
|
+
// or with custom base path:
|
|
168
|
+
// routeRedirects: { basePath: '/auth' },
|
|
169
|
+
|
|
170
|
+
// OAuth providers (optional — hidden by default)
|
|
171
|
+
providers: {
|
|
172
|
+
google: {
|
|
173
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
174
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
175
|
+
prompt: 'select_account', // Always show account picker (default)
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Route Redirects
|
|
182
|
+
|
|
183
|
+
| Value | Behavior |
|
|
184
|
+
|-------|----------|
|
|
185
|
+
| `false` (default) | No redirects. You manage your own routes. |
|
|
186
|
+
| `true` | Redirects `/login`, `/signup`, etc. → `/auth/login`, `/auth/signup`, etc. |
|
|
187
|
+
| `{ basePath: '/myauth' }` | Same, but redirects to `/myauth/login`, etc. |
|
|
188
|
+
|
|
189
|
+
> **Note:** `/admin/login` is **always** redirected to the plugin login page when the proxy is active, regardless of the `routeRedirects` setting.
|
|
190
|
+
|
|
191
|
+
### Google OAuth (Providers)
|
|
192
|
+
|
|
193
|
+
Google OAuth is **hidden by default**. To enable it, just add `providers.google` — the plugin handles everything (UI buttons + server-side OAuth flow via `payload-oauth2` under the hood):
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
authLoginPlugin({
|
|
197
|
+
providers: {
|
|
198
|
+
google: {
|
|
199
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
200
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
201
|
+
successRedirect: '/dashboard', // optional, defaults to '/admin'
|
|
202
|
+
failureRedirect: '/login?error=failed', // optional
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
That's it. No extra packages to install, no extra plugins to configure.
|
|
209
|
+
|
|
210
|
+
| Config | Behavior |
|
|
211
|
+
|--------|----------|
|
|
212
|
+
| Omitted / not set | Google OAuth hidden |
|
|
213
|
+
| `google: true` | Auto-detect `GOOGLE_CLIENT_ID` + `GOOGLE_CLIENT_SECRET` from env |
|
|
214
|
+
| `google: { clientId, clientSecret }` | Enable with explicit credentials |
|
|
215
|
+
| `google: false` | Force disable |
|
|
216
|
+
|
|
217
|
+
#### Google Account Picker
|
|
218
|
+
|
|
219
|
+
By default, Google always shows the account selection screen (`prompt: 'select_account'`). This prevents confusion when users have multiple Google accounts.
|
|
220
|
+
|
|
221
|
+
| `prompt` value | Behavior |
|
|
222
|
+
|----------------|----------|
|
|
223
|
+
| `'select_account'` (default) | Always show account picker |
|
|
224
|
+
| `'consent'` | Prompt for consent every time |
|
|
225
|
+
| `'none'` | Skip picker, use existing session |
|
|
226
|
+
|
|
227
|
+
### Login Methods
|
|
228
|
+
|
|
229
|
+
Control which login methods are available:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
authLoginPlugin({
|
|
233
|
+
passwordLogin: false, // Disable password login
|
|
234
|
+
otpLogin: true, // Enable OTP login
|
|
130
235
|
})
|
|
131
236
|
```
|
|
132
237
|
|
|
238
|
+
| `passwordLogin` | `otpLogin` | User has password | Behavior |
|
|
239
|
+
|---|---|---|---|
|
|
240
|
+
| ✅ | ✅ | Yes | Password step |
|
|
241
|
+
| ✅ | ✅ | No | OTP → straight to app |
|
|
242
|
+
| ❌ | ✅ | Any | OTP → straight to app |
|
|
243
|
+
| ✅ | ❌ | No | OTP → set password prompt |
|
|
244
|
+
| ✅ | ❌ | Yes | Password step |
|
|
245
|
+
|
|
246
|
+
When `otpLogin` is enabled, users are never prompted to set a password after OTP verification — they go straight to the app. The set-password prompt only appears when `passwordLogin` is enabled and `otpLogin` is disabled.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## AuthPages Props
|
|
251
|
+
|
|
252
|
+
The `AuthPages` component accepts these props for customization:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
<AuthPages
|
|
256
|
+
slug={slug} // Required — from catch-all route params
|
|
257
|
+
redirectTo="/dashboard" // Where to go after login/set-password (default: '/admin')
|
|
258
|
+
logo={<MyLogo />} // Custom logo component
|
|
259
|
+
basePath="/auth" // Base path for sibling links (default: '/auth')
|
|
260
|
+
showGoogleOAuth={true} // Override Google OAuth visibility
|
|
261
|
+
onPasswordLogin={customLogin} // Custom login handler
|
|
262
|
+
onSignup={customSignup} // Custom signup handler
|
|
263
|
+
/>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Individual Page Setup (Advanced)
|
|
269
|
+
|
|
270
|
+
If you need full control over each page, create separate route files instead of using `AuthPages`:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
// login/page.tsx
|
|
274
|
+
'use client'
|
|
275
|
+
import { LoginPage } from '@main12/auth-login/client'
|
|
276
|
+
|
|
277
|
+
export default function Page() {
|
|
278
|
+
return (
|
|
279
|
+
<LoginPage
|
|
280
|
+
onPasswordLogin={async ({ email, password }) => {
|
|
281
|
+
const res = await fetch('/api/users/login', {
|
|
282
|
+
method: 'POST',
|
|
283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
284
|
+
body: JSON.stringify({ email, password }),
|
|
285
|
+
})
|
|
286
|
+
if (!res.ok) throw new Error('Login failed')
|
|
287
|
+
}}
|
|
288
|
+
redirectTo="/dashboard"
|
|
289
|
+
signupUrl="/signup"
|
|
290
|
+
/>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
133
295
|
### Per-page overrides
|
|
134
296
|
|
|
135
297
|
```tsx
|
|
136
298
|
<LoginPage
|
|
137
|
-
logo={<AppLogo width={180} />}
|
|
138
|
-
onPasswordLogin={login}
|
|
299
|
+
logo={<AppLogo width={180} />}
|
|
300
|
+
onPasswordLogin={login}
|
|
139
301
|
redirectTo="/dashboard"
|
|
140
302
|
showGoogleOAuth={true}
|
|
141
303
|
signupUrl="/signup"
|
|
142
|
-
poweredBy={{ enabled: true, logoUrl: '/custom.png', linkUrl: 'https://...' }}
|
|
143
304
|
/>
|
|
305
|
+
|
|
306
|
+
<SignupPage onSignup={signup} loginUrl="/login" />
|
|
307
|
+
<ForgotPasswordPage loginUrl="/login" />
|
|
308
|
+
<VerifyOtpPage loginUrl="/login" />
|
|
309
|
+
<SetPasswordPage redirectTo="/dashboard" />
|
|
144
310
|
```
|
|
145
311
|
|
|
146
312
|
---
|
|
@@ -289,17 +455,39 @@ await payload.sendEmail({
|
|
|
289
455
|
|
|
290
456
|
---
|
|
291
457
|
|
|
458
|
+
## Exports
|
|
459
|
+
|
|
460
|
+
| Import path | Contents |
|
|
461
|
+
|-------------|----------|
|
|
462
|
+
| `@main12/auth-login` | Plugin factory, types, server config |
|
|
463
|
+
| `@main12/auth-login/client` | Page components, `AuthPages`, hooks, services, UI utilities |
|
|
464
|
+
| `@main12/auth-login/rsc` | Email template generators, translations |
|
|
465
|
+
| `@main12/auth-login/proxy` | Next.js 16 proxy for route redirects |
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
292
469
|
## ShadCN Compatibility
|
|
293
470
|
|
|
294
471
|
The `tailwind` style works in ShadCN projects out of the box. For ShadCN components, build a [custom page](#building-custom-pages) — import the plugin's hooks and use your `@/components/ui/button`, `@/components/ui/input`, etc.
|
|
295
472
|
|
|
296
473
|
---
|
|
297
474
|
|
|
475
|
+
## Setup Comparison
|
|
476
|
+
|
|
477
|
+
| Approach | Files needed | Best for |
|
|
478
|
+
|----------|-------------|----------|
|
|
479
|
+
| **Catch-all + proxy** (recommended) | 2 files | Most projects — fastest setup |
|
|
480
|
+
| **Individual pages** | 5 files | Full control over each page |
|
|
481
|
+
| **Custom pages with hooks** | Your own files | Completely custom UI |
|
|
482
|
+
| **Backend only** (`routeRedirects: false`, no `AuthPages`) | 0 frontend files | Custom frontend using only the API endpoints + hooks |
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
298
486
|
## 🤖 AI Agent Prompts
|
|
299
487
|
|
|
300
488
|
Copy these prompts into Claude, Cursor, Copilot, or any AI agent.
|
|
301
489
|
|
|
302
|
-
### Prompt: Set up the auth plugin
|
|
490
|
+
### Prompt: Set up the auth plugin (simplified catch-all)
|
|
303
491
|
|
|
304
492
|
```
|
|
305
493
|
Add @main12/auth-login to this Payload project:
|
|
@@ -307,15 +495,38 @@ Add @main12/auth-login to this Payload project:
|
|
|
307
495
|
1. Install: pnpm add @main12/auth-login
|
|
308
496
|
2. In payload.config.ts, add:
|
|
309
497
|
- Users collection with auth enabled + otpHash, otpAttempts, otpExpiresAt fields
|
|
310
|
-
- Plugin: authLoginPlugin({ projectName: "<PROJECT>", domain: "<URL>", logo: "/logo.png", style: "
|
|
311
|
-
3. Create route
|
|
312
|
-
-
|
|
313
|
-
-
|
|
314
|
-
|
|
315
|
-
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
498
|
+
- Plugin: authLoginPlugin({ projectName: "<PROJECT>", domain: "<URL>", logo: "/logo.png", style: "tailwind", routeRedirects: true })
|
|
499
|
+
3. Create catch-all route: src/app/(frontend)/(auth)/auth/[...slug]/page.tsx
|
|
500
|
+
- 'use client', import { use } from 'react', import { AuthPages } from '@main12/auth-login/client'
|
|
501
|
+
- export default function Page({ params }) { const { slug } = use(params); return <AuthPages slug={slug} /> }
|
|
502
|
+
4. Create proxy: src/proxy.ts
|
|
503
|
+
- export { proxy, config } from '@main12/auth-login/proxy'
|
|
504
|
+
5. Verify: visit /login → should redirect to /auth/login
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Prompt: Set up with Google OAuth
|
|
508
|
+
|
|
509
|
+
```
|
|
510
|
+
Add @main12/auth-login with Google OAuth to this Payload project:
|
|
511
|
+
|
|
512
|
+
1. Install: pnpm add @main12/auth-login
|
|
513
|
+
2. In payload.config.ts, add:
|
|
514
|
+
- Users collection with auth enabled + otpHash, otpAttempts, otpExpiresAt fields
|
|
515
|
+
- Plugin: authLoginPlugin({
|
|
516
|
+
projectName: "<PROJECT>",
|
|
517
|
+
domain: "<URL>",
|
|
518
|
+
style: "tailwind",
|
|
519
|
+
routeRedirects: true,
|
|
520
|
+
providers: {
|
|
521
|
+
google: {
|
|
522
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
523
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
})
|
|
527
|
+
3. Create catch-all route + proxy (same as simplified setup)
|
|
528
|
+
4. Add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to .env
|
|
529
|
+
5. Verify: visit /login → should show Google OAuth button
|
|
319
530
|
```
|
|
320
531
|
|
|
321
532
|
### Prompt: Build a custom login page with HeroUI components
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useState, useCallback } from 'react';
|
|
3
3
|
import { useRouter } from 'next/navigation';
|
|
4
4
|
import { checkEmail, sendOtp, initiateGoogleLogin } from '../services/authService.js';
|
|
5
|
+
import { pluginConfig } from '../../../config.js';
|
|
5
6
|
/**
|
|
6
7
|
* State machine for the multi-step login flow: email → password | otp-prompt.
|
|
7
8
|
* The page component owns the UI; this hook owns the logic.
|
|
@@ -25,14 +26,29 @@ import { checkEmail, sendOtp, initiateGoogleLogin } from '../services/authServic
|
|
|
25
26
|
setError('noAccountFound');
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
const { passwordLogin: passwordLoginEnabled, otpLogin: otpLoginEnabled } = pluginConfig;
|
|
30
|
+
if (data.hasPassword && passwordLoginEnabled) {
|
|
31
|
+
// Show password step if user has a password and password login is enabled
|
|
32
|
+
setStep('password');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// OTP login — send OTP immediately and redirect to verify page
|
|
36
|
+
const otpData = await sendOtp(email, 'login');
|
|
37
|
+
if (otpData.success) {
|
|
38
|
+
const redirectParam = redirectTo !== '/' ? `&redirect=${encodeURIComponent(redirectTo)}` : '';
|
|
39
|
+
router.push(`/verify-otp?email=${encodeURIComponent(email.trim())}${redirectParam}`);
|
|
40
|
+
} else {
|
|
41
|
+
setError(otpData.message || 'otpSendFailed');
|
|
42
|
+
}
|
|
29
43
|
} catch {
|
|
30
44
|
setError('genericError');
|
|
31
45
|
} finally{
|
|
32
46
|
setIsLoading(false);
|
|
33
47
|
}
|
|
34
48
|
}, [
|
|
35
|
-
email
|
|
49
|
+
email,
|
|
50
|
+
redirectTo,
|
|
51
|
+
router
|
|
36
52
|
]);
|
|
37
53
|
const handlePasswordSubmit = useCallback(async (e)=>{
|
|
38
54
|
e.preventDefault();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useState, useCallback, useEffect } from 'react';
|
|
3
3
|
import { useRouter } from 'next/navigation';
|
|
4
4
|
import { verifyOtp, sendOtp } from '../services/authService.js';
|
|
5
|
+
import { pluginConfig } from '../../../config.js';
|
|
5
6
|
/**
|
|
6
7
|
* State machine for OTP verification: input → verify → redirect | resend.
|
|
7
8
|
*/ export function useVerifyOtpFlow({ email, purpose, redirectTo = '/' }) {
|
|
@@ -10,7 +11,8 @@ import { verifyOtp, sendOtp } from '../services/authService.js';
|
|
|
10
11
|
const [isLoading, setIsLoading] = useState(false);
|
|
11
12
|
const [error, setError] = useState(null);
|
|
12
13
|
const [isResending, setIsResending] = useState(false);
|
|
13
|
-
|
|
14
|
+
// Start with 30s cooldown since a code was just sent before arriving here
|
|
15
|
+
const [resendCooldown, setResendCooldown] = useState(30);
|
|
14
16
|
useEffect(()=>{
|
|
15
17
|
if (resendCooldown > 0) {
|
|
16
18
|
const timer = setTimeout(()=>setResendCooldown((c)=>c - 1), 1000);
|
|
@@ -26,11 +28,15 @@ import { verifyOtp, sendOtp } from '../services/authService.js';
|
|
|
26
28
|
try {
|
|
27
29
|
const data = await verifyOtp(email, otp);
|
|
28
30
|
if (data.success) {
|
|
31
|
+
const { passwordLogin: passwordLoginEnabled, otpLogin: otpLoginEnabled } = pluginConfig;
|
|
29
32
|
if (purpose === 'password-reset') {
|
|
33
|
+
// Always allow setting password from explicit password-reset flow
|
|
30
34
|
router.push(`/set-password?redirect=${encodeURIComponent(redirectTo)}`);
|
|
31
|
-
} else if (data.isNewUser) {
|
|
35
|
+
} else if (!otpLoginEnabled && passwordLoginEnabled && data.isNewUser) {
|
|
36
|
+
// Only prompt for password if OTP login is disabled and password login is enabled
|
|
32
37
|
router.push(`/set-password?redirect=${encodeURIComponent(redirectTo)}`);
|
|
33
38
|
} else {
|
|
39
|
+
// OTP login is enabled — go straight to the app
|
|
34
40
|
window.location.href = redirectTo;
|
|
35
41
|
}
|
|
36
42
|
} else {
|
|
@@ -50,6 +56,15 @@ import { verifyOtp, sendOtp } from '../services/authService.js';
|
|
|
50
56
|
redirectTo,
|
|
51
57
|
router
|
|
52
58
|
]);
|
|
59
|
+
// Auto-verify when all 6 digits are entered
|
|
60
|
+
useEffect(()=>{
|
|
61
|
+
if (otp.length === 6 && !isLoading) {
|
|
62
|
+
handleSubmit();
|
|
63
|
+
}
|
|
64
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
65
|
+
}, [
|
|
66
|
+
otp
|
|
67
|
+
]);
|
|
53
68
|
const handleResendCode = useCallback(async ()=>{
|
|
54
69
|
if (resendCooldown > 0) return;
|
|
55
70
|
setIsResending(true);
|
|
@@ -22,5 +22,6 @@ export declare function setUserPassword(password: string, confirmPassword: strin
|
|
|
22
22
|
export declare function signup(name: string, email: string): Promise<SignupResponse>;
|
|
23
23
|
/**
|
|
24
24
|
* Redirect the browser to the Google OAuth login endpoint.
|
|
25
|
+
* Uses payload-oauth2 plugin's route at /api/users/oauth/google.
|
|
25
26
|
*/
|
|
26
27
|
export declare function initiateGoogleLogin(redirectTo?: string): void;
|
|
@@ -80,10 +80,11 @@ const API_PREFIX = '/api/auth';
|
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* Redirect the browser to the Google OAuth login endpoint.
|
|
83
|
+
* Uses payload-oauth2 plugin's route at /api/users/oauth/google.
|
|
83
84
|
*/ export function initiateGoogleLogin(redirectTo = '/') {
|
|
84
85
|
const params = new URLSearchParams();
|
|
85
86
|
if (redirectTo !== '/') {
|
|
86
|
-
params.set('
|
|
87
|
+
params.set('state', redirectTo);
|
|
87
88
|
}
|
|
88
89
|
const qs = params.toString();
|
|
89
90
|
window.location.href = `/api/users/oauth/google${qs ? `?${qs}` : ''}`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type AuthStyle } from '../config';
|
|
2
|
+
export interface AuthClientInitProps {
|
|
3
|
+
/** Override UI style. If omitted, reads from plugin config. */
|
|
4
|
+
style?: AuthStyle;
|
|
5
|
+
/** Override Google OAuth visibility. If omitted, reads from plugin config. */
|
|
6
|
+
googleOAuthEnabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Drop this component into your root layout to sync the server-side
|
|
10
|
+
* plugin config to client components. No props needed — it reads
|
|
11
|
+
* everything from the plugin configuration automatically.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // app/(frontend)/layout.tsx
|
|
15
|
+
* import { AuthClientInit } from '@main12/auth-login/client'
|
|
16
|
+
*
|
|
17
|
+
* export default function Layout({ children }) {
|
|
18
|
+
* return (
|
|
19
|
+
* <html><body>
|
|
20
|
+
* <AuthClientInit />
|
|
21
|
+
* {children}
|
|
22
|
+
* </body></html>
|
|
23
|
+
* )
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function AuthClientInit({ style, googleOAuthEnabled }?: AuthClientInitProps): null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { pluginConfig, initClientConfig } from '../config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Drop this component into your root layout to sync the server-side
|
|
5
|
+
* plugin config to client components. No props needed — it reads
|
|
6
|
+
* everything from the plugin configuration automatically.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // app/(frontend)/layout.tsx
|
|
10
|
+
* import { AuthClientInit } from '@main12/auth-login/client'
|
|
11
|
+
*
|
|
12
|
+
* export default function Layout({ children }) {
|
|
13
|
+
* return (
|
|
14
|
+
* <html><body>
|
|
15
|
+
* <AuthClientInit />
|
|
16
|
+
* {children}
|
|
17
|
+
* </body></html>
|
|
18
|
+
* )
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/ export function AuthClientInit({ style, googleOAuthEnabled } = {}) {
|
|
22
|
+
initClientConfig({
|
|
23
|
+
style: style ?? pluginConfig.style,
|
|
24
|
+
googleOAuthEnabled: googleOAuthEnabled ?? pluginConfig.googleOAuthEnabled,
|
|
25
|
+
passwordLogin: pluginConfig.passwordLogin,
|
|
26
|
+
otpLogin: pluginConfig.otpLogin
|
|
27
|
+
});
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
@@ -41,7 +41,10 @@ export const AuthLayout = ({ children, logo, title, subtitle, footer, poweredBy,
|
|
|
41
41
|
className: `flex flex-col min-h-screen ${backgroundClass}`,
|
|
42
42
|
children: [
|
|
43
43
|
/*#__PURE__*/ _jsxs("div", {
|
|
44
|
-
className: "md:hidden flex flex-col",
|
|
44
|
+
className: "md:hidden flex flex-col w-full max-w-md mx-auto",
|
|
45
|
+
style: {
|
|
46
|
+
maxWidth: '400px'
|
|
47
|
+
},
|
|
45
48
|
children: [
|
|
46
49
|
header,
|
|
47
50
|
/*#__PURE__*/ _jsx("div", {
|
|
@@ -60,7 +63,10 @@ export const AuthLayout = ({ children, logo, title, subtitle, footer, poweredBy,
|
|
|
60
63
|
/*#__PURE__*/ _jsx("div", {
|
|
61
64
|
className: "hidden md:flex min-h-screen items-center justify-center px-4 py-12",
|
|
62
65
|
children: /*#__PURE__*/ _jsxs("div", {
|
|
63
|
-
className: "w-full max-w-[400px] animate-[fadeIn_0.5s_ease-out]",
|
|
66
|
+
className: "w-full max-w-md max-w-[400px] animate-[fadeIn_0.5s_ease-out]",
|
|
67
|
+
style: {
|
|
68
|
+
maxWidth: '400px'
|
|
69
|
+
},
|
|
64
70
|
children: [
|
|
65
71
|
/*#__PURE__*/ _jsxs(Card, {
|
|
66
72
|
className: cardClassName,
|
|
@@ -20,5 +20,15 @@ export interface AuthPagesProps {
|
|
|
20
20
|
basePath?: string;
|
|
21
21
|
/** Show Google OAuth buttons. If omitted, reads from plugin config. */
|
|
22
22
|
showGoogleOAuth?: boolean;
|
|
23
|
+
/** Override the page background. Defaults to 'bg-white md:bg-[#191919]'. */
|
|
24
|
+
backgroundClass?: string;
|
|
25
|
+
/** UI style override. If omitted, reads from plugin config. */
|
|
26
|
+
style?: 'tailwind' | 'hero-ui';
|
|
27
|
+
/** Allow new user signups. When false, hides signup page and signup links. Defaults to true. */
|
|
28
|
+
allowSignup?: boolean;
|
|
29
|
+
/** Allow password-based login. @default true */
|
|
30
|
+
passwordLogin?: boolean;
|
|
31
|
+
/** Allow OTP-based login. @default true */
|
|
32
|
+
otpLogin?: boolean;
|
|
23
33
|
}
|
|
24
|
-
export default function AuthPages({ slug, redirectTo, logo, onPasswordLogin, onSignup, basePath, showGoogleOAuth, }: AuthPagesProps): import("react/jsx-runtime").JSX.Element;
|
|
34
|
+
export default function AuthPages({ slug, redirectTo, logo, onPasswordLogin, onSignup, basePath, showGoogleOAuth, backgroundClass, style, allowSignup, passwordLogin, otpLogin, }: AuthPagesProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { pluginConfig, initClientConfig } from '../config.js';
|
|
3
4
|
import LoginPage from './pages/LoginPage.js';
|
|
4
5
|
import SignupPage from './pages/SignupPage.js';
|
|
5
6
|
import ForgotPasswordPage from './pages/ForgotPasswordPage.js';
|
|
@@ -37,13 +38,23 @@ const defaultSignup = async ({ name, email })=>{
|
|
|
37
38
|
throw new Error(err.message || 'Signup failed');
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
|
-
export default function AuthPages({ slug, redirectTo = '/admin', logo, onPasswordLogin = defaultPasswordLogin, onSignup = defaultSignup, basePath = '/auth', showGoogleOAuth }) {
|
|
41
|
+
export default function AuthPages({ slug, redirectTo = '/admin', logo, onPasswordLogin = defaultPasswordLogin, onSignup = defaultSignup, basePath = '/auth', showGoogleOAuth, backgroundClass, style, allowSignup = true, passwordLogin = true, otpLogin = true }) {
|
|
42
|
+
// Sync server plugin config to client using props (which come from the server via AuthPagesServer)
|
|
43
|
+
initClientConfig({
|
|
44
|
+
style: style ?? pluginConfig.style,
|
|
45
|
+
googleOAuthEnabled: showGoogleOAuth ?? pluginConfig.googleOAuthEnabled,
|
|
46
|
+
passwordLogin,
|
|
47
|
+
otpLogin
|
|
48
|
+
});
|
|
41
49
|
const page = slug?.[0] ?? 'login';
|
|
42
50
|
const base = basePath.replace(/\/$/, '');
|
|
43
51
|
const shared = {
|
|
44
52
|
logo,
|
|
45
53
|
...showGoogleOAuth !== undefined ? {
|
|
46
54
|
showGoogleOAuth
|
|
55
|
+
} : {},
|
|
56
|
+
...backgroundClass !== undefined ? {
|
|
57
|
+
backgroundClass
|
|
47
58
|
} : {}
|
|
48
59
|
};
|
|
49
60
|
switch(page){
|
|
@@ -51,10 +62,17 @@ export default function AuthPages({ slug, redirectTo = '/admin', logo, onPasswor
|
|
|
51
62
|
return /*#__PURE__*/ _jsx(LoginPage, {
|
|
52
63
|
onPasswordLogin: onPasswordLogin,
|
|
53
64
|
redirectTo: redirectTo,
|
|
54
|
-
signupUrl: `${base}/signup
|
|
65
|
+
signupUrl: allowSignup ? `${base}/signup` : undefined,
|
|
55
66
|
...shared
|
|
56
67
|
});
|
|
57
68
|
case 'signup':
|
|
69
|
+
if (!allowSignup) {
|
|
70
|
+
return /*#__PURE__*/ _jsx(LoginPage, {
|
|
71
|
+
onPasswordLogin: onPasswordLogin,
|
|
72
|
+
redirectTo: redirectTo,
|
|
73
|
+
...shared
|
|
74
|
+
});
|
|
75
|
+
}
|
|
58
76
|
return /*#__PURE__*/ _jsx(SignupPage, {
|
|
59
77
|
onSignup: onSignup,
|
|
60
78
|
loginUrl: `${base}/login`,
|