@main12/auth-login 0.3.1 → 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 +56 -3
- package/dist/auth/application/hooks/useVerifyOtpFlow.js +9 -0
- package/dist/components/AuthLayout.js +8 -2
- package/dist/components/AuthPages.d.ts +5 -1
- package/dist/components/AuthPages.js +4 -2
- package/dist/components/AuthPagesServer.js +5 -1
- package/dist/components/ui/index.js +4 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -0
- package/dist/proxy.js +15 -1
- package/package.json +17 -50
package/README.md
CHANGED
|
@@ -31,6 +31,10 @@ plugins: [
|
|
|
31
31
|
- **Email templates** — welcome, OTP login, password reset, password changed (EN/ES)
|
|
32
32
|
- **Powered by Main 12** — bundled inline SVG, linked to main12.com by default (URL overridable)
|
|
33
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'`)
|
|
34
38
|
- **Zero runtime deps** (Tailwind mode) — Next.js + React are peer dependencies
|
|
35
39
|
|
|
36
40
|
---
|
|
@@ -45,6 +49,20 @@ pnpm add @main12/auth-login
|
|
|
45
49
|
|
|
46
50
|
No extra dependencies needed. The Tailwind style uses standard utility classes that work in any Tailwind project, including ShadCN-based ones.
|
|
47
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
|
+
|
|
48
66
|
### HeroUI mode
|
|
49
67
|
|
|
50
68
|
```bash
|
|
@@ -70,9 +88,6 @@ export default buildConfig({
|
|
|
70
88
|
auth: { tokenExpiration: 7200, verify: false, maxLoginAttempts: 5 },
|
|
71
89
|
fields: [
|
|
72
90
|
{ name: 'name', type: 'text' },
|
|
73
|
-
{ name: 'otpHash', type: 'text', admin: { hidden: true } },
|
|
74
|
-
{ name: 'otpAttempts', type: 'number', admin: { hidden: true } },
|
|
75
|
-
{ name: 'otpExpiresAt', type: 'text', admin: { hidden: true } },
|
|
76
91
|
],
|
|
77
92
|
},
|
|
78
93
|
],
|
|
@@ -88,6 +103,8 @@ export default buildConfig({
|
|
|
88
103
|
})
|
|
89
104
|
```
|
|
90
105
|
|
|
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.
|
|
107
|
+
|
|
91
108
|
### 2. Create the catch-all auth route (1 file)
|
|
92
109
|
|
|
93
110
|
```tsx
|
|
@@ -141,6 +158,10 @@ authLoginPlugin({
|
|
|
141
158
|
style: 'tailwind', // 'tailwind' | 'hero-ui'
|
|
142
159
|
enabled: true,
|
|
143
160
|
|
|
161
|
+
// Login methods
|
|
162
|
+
passwordLogin: true, // Allow password-based login (default: true)
|
|
163
|
+
otpLogin: true, // Allow OTP-based login (default: true)
|
|
164
|
+
|
|
144
165
|
// Route redirects
|
|
145
166
|
routeRedirects: true, // Enable /login → /auth/login redirects
|
|
146
167
|
// or with custom base path:
|
|
@@ -151,6 +172,7 @@ authLoginPlugin({
|
|
|
151
172
|
google: {
|
|
152
173
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
153
174
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
175
|
+
prompt: 'select_account', // Always show account picker (default)
|
|
154
176
|
},
|
|
155
177
|
},
|
|
156
178
|
})
|
|
@@ -192,6 +214,37 @@ That's it. No extra packages to install, no extra plugins to configure.
|
|
|
192
214
|
| `google: { clientId, clientSecret }` | Enable with explicit credentials |
|
|
193
215
|
| `google: false` | Force disable |
|
|
194
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
|
|
235
|
+
})
|
|
236
|
+
```
|
|
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
|
+
|
|
195
248
|
---
|
|
196
249
|
|
|
197
250
|
## AuthPages Props
|
|
@@ -56,6 +56,15 @@ import { pluginConfig } from '../../../config.js';
|
|
|
56
56
|
redirectTo,
|
|
57
57
|
router
|
|
58
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
|
+
]);
|
|
59
68
|
const handleResendCode = useCallback(async ()=>{
|
|
60
69
|
if (resendCooldown > 0) return;
|
|
61
70
|
setIsResending(true);
|
|
@@ -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,
|
|
@@ -26,5 +26,9 @@ export interface AuthPagesProps {
|
|
|
26
26
|
style?: 'tailwind' | 'hero-ui';
|
|
27
27
|
/** Allow new user signups. When false, hides signup page and signup links. Defaults to true. */
|
|
28
28
|
allowSignup?: boolean;
|
|
29
|
+
/** Allow password-based login. @default true */
|
|
30
|
+
passwordLogin?: boolean;
|
|
31
|
+
/** Allow OTP-based login. @default true */
|
|
32
|
+
otpLogin?: boolean;
|
|
29
33
|
}
|
|
30
|
-
export default function AuthPages({ slug, redirectTo, logo, onPasswordLogin, onSignup, basePath, showGoogleOAuth, backgroundClass, style, allowSignup, }: 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;
|
|
@@ -38,11 +38,13 @@ const defaultSignup = async ({ name, email })=>{
|
|
|
38
38
|
throw new Error(err.message || 'Signup failed');
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
|
-
export default function AuthPages({ slug, redirectTo = '/admin', logo, onPasswordLogin = defaultPasswordLogin, onSignup = defaultSignup, basePath = '/auth', showGoogleOAuth, backgroundClass, style, allowSignup = true }) {
|
|
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
42
|
// Sync server plugin config to client using props (which come from the server via AuthPagesServer)
|
|
43
43
|
initClientConfig({
|
|
44
44
|
style: style ?? pluginConfig.style,
|
|
45
|
-
googleOAuthEnabled: showGoogleOAuth ?? pluginConfig.googleOAuthEnabled
|
|
45
|
+
googleOAuthEnabled: showGoogleOAuth ?? pluginConfig.googleOAuthEnabled,
|
|
46
|
+
passwordLogin,
|
|
47
|
+
otpLogin
|
|
46
48
|
});
|
|
47
49
|
const page = slug?.[0] ?? 'login';
|
|
48
50
|
const base = basePath.replace(/\/$/, '');
|
|
@@ -17,10 +17,14 @@ import AuthPagesClient from './AuthPages.js';
|
|
|
17
17
|
const googleOAuthEnabled = process.env.AUTH_PLUGIN_GOOGLE_OAUTH === 'true';
|
|
18
18
|
const style = process.env.AUTH_PLUGIN_STYLE || 'tailwind';
|
|
19
19
|
const allowSignup = process.env.AUTH_PLUGIN_ALLOW_SIGNUP !== 'false';
|
|
20
|
+
const passwordLogin = process.env.AUTH_PLUGIN_PASSWORD_LOGIN !== 'false';
|
|
21
|
+
const otpLogin = process.env.AUTH_PLUGIN_OTP_LOGIN !== 'false';
|
|
20
22
|
return /*#__PURE__*/ _jsx(AuthPagesClient, {
|
|
21
23
|
...props,
|
|
22
24
|
showGoogleOAuth: props.showGoogleOAuth ?? googleOAuthEnabled,
|
|
23
25
|
style: props.style ?? style,
|
|
24
|
-
allowSignup: props.allowSignup ?? allowSignup
|
|
26
|
+
allowSignup: props.allowSignup ?? allowSignup,
|
|
27
|
+
passwordLogin: passwordLogin,
|
|
28
|
+
otpLogin: otpLogin
|
|
25
29
|
});
|
|
26
30
|
}
|
|
@@ -51,7 +51,10 @@ export const Input = ({ type = 'text', label, value, onChange, onValueChange, is
|
|
|
51
51
|
]
|
|
52
52
|
});
|
|
53
53
|
export const Card = ({ children, className = '' })=>/*#__PURE__*/ _jsx("div", {
|
|
54
|
-
className: `bg-white rounded-2xl shadow-2xl ${className}`,
|
|
54
|
+
className: `bg-white rounded-2xl shadow-2xl w-full ${className}`,
|
|
55
|
+
style: {
|
|
56
|
+
maxWidth: '400px'
|
|
57
|
+
},
|
|
55
58
|
children: children
|
|
56
59
|
});
|
|
57
60
|
export const CardHeader = ({ children, className = '' })=>/*#__PURE__*/ _jsx("div", {
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ export interface GoogleOAuthConfig {
|
|
|
11
11
|
successRedirect?: string;
|
|
12
12
|
/** Where to redirect after failed Google login. Defaults to '/login?error=Google login failed' */
|
|
13
13
|
failureRedirect?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Google OAuth prompt behavior.
|
|
16
|
+
* - 'select_account' — always show account picker (recommended)
|
|
17
|
+
* - 'consent' — prompt for consent every time
|
|
18
|
+
* - 'none' — no prompt, use existing session
|
|
19
|
+
* @default 'select_account'
|
|
20
|
+
*/
|
|
21
|
+
prompt?: 'select_account' | 'consent' | 'none';
|
|
14
22
|
}
|
|
15
23
|
export interface AuthProvidersConfig {
|
|
16
24
|
google?: GoogleOAuthConfig | boolean;
|
package/dist/index.js
CHANGED
|
@@ -138,6 +138,7 @@ export const authLoginPlugin = (options = {})=>async (config)=>{
|
|
|
138
138
|
],
|
|
139
139
|
authorizePath: '/oauth/google',
|
|
140
140
|
callbackPath: '/oauth/google/callback',
|
|
141
|
+
prompt: googleOpts.prompt || 'select_account',
|
|
141
142
|
getUserInfo: async (accessToken)=>{
|
|
142
143
|
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
|
|
143
144
|
headers: {
|
package/dist/proxy.js
CHANGED
|
@@ -35,6 +35,19 @@ const AUTH_ROUTES = [
|
|
|
35
35
|
return NextResponse.next();
|
|
36
36
|
}
|
|
37
37
|
const { pathname } = request.nextUrl;
|
|
38
|
+
// If user is already logged in, redirect away from auth pages (login, signup, etc.)
|
|
39
|
+
const token = request.cookies.get('payload-token')?.value;
|
|
40
|
+
if (token) {
|
|
41
|
+
const segment = pathname.replace(/^\//, '').replace(/\/$/, '');
|
|
42
|
+
const isAuthPage = routeSet.has(segment) || routeSet.has(pathname.replace(`${base}/`, ''));
|
|
43
|
+
if (isAuthPage || pathname.startsWith(`${base}/login`) || pathname.startsWith(`${base}/signup`)) {
|
|
44
|
+
const redirect = request.nextUrl.searchParams.get('redirect') || '/';
|
|
45
|
+
const url = request.nextUrl.clone();
|
|
46
|
+
url.pathname = redirect;
|
|
47
|
+
url.search = '';
|
|
48
|
+
return NextResponse.redirect(url);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
38
51
|
// Always redirect /admin/login to the plugin login page
|
|
39
52
|
if (pathname === '/admin/login' || pathname === '/admin/login/') {
|
|
40
53
|
const url = request.nextUrl.clone();
|
|
@@ -58,6 +71,7 @@ export const config = {
|
|
|
58
71
|
'/signup',
|
|
59
72
|
'/forgot-password',
|
|
60
73
|
'/verify-otp',
|
|
61
|
-
'/set-password'
|
|
74
|
+
'/set-password',
|
|
75
|
+
'/auth/:path*'
|
|
62
76
|
]
|
|
63
77
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@main12/auth-login",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Reusable Payload CMS auth plugin — login, signup, OTP, forgot password, branded emails, Powered by Main12",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,11 +19,6 @@
|
|
|
19
19
|
"import": "./dist/exports/rsc.js",
|
|
20
20
|
"types": "./dist/exports/rsc.d.ts",
|
|
21
21
|
"default": "./dist/exports/rsc.js"
|
|
22
|
-
},
|
|
23
|
-
"./proxy": {
|
|
24
|
-
"import": "./dist/proxy.js",
|
|
25
|
-
"types": "./dist/proxy.d.ts",
|
|
26
|
-
"default": "./dist/proxy.js"
|
|
27
22
|
}
|
|
28
23
|
},
|
|
29
24
|
"main": "./dist/index.js",
|
|
@@ -31,21 +26,6 @@
|
|
|
31
26
|
"files": [
|
|
32
27
|
"dist"
|
|
33
28
|
],
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc && pnpm build:fix-esm-imports",
|
|
36
|
-
"build:fix-esm-imports": "node ./scripts/fix-esm-imports.mjs",
|
|
37
|
-
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
38
|
-
"build:types": "tsc --outDir dist",
|
|
39
|
-
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
40
|
-
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
41
|
-
"dev": "next dev dev --turbo",
|
|
42
|
-
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
43
|
-
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
44
|
-
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
45
|
-
"lint": "eslint",
|
|
46
|
-
"lint:fix": "eslint ./src --fix",
|
|
47
|
-
"test": "vitest"
|
|
48
|
-
},
|
|
49
29
|
"devDependencies": {
|
|
50
30
|
"@eslint/eslintrc": "^3.2.0",
|
|
51
31
|
"@heroui/react": "^3.2.2",
|
|
@@ -95,39 +75,26 @@
|
|
|
95
75
|
"engines": {
|
|
96
76
|
"node": ">=18.20.2"
|
|
97
77
|
},
|
|
98
|
-
"publishConfig": {
|
|
99
|
-
"exports": {
|
|
100
|
-
".": {
|
|
101
|
-
"import": "./dist/index.js",
|
|
102
|
-
"types": "./dist/index.d.ts",
|
|
103
|
-
"default": "./dist/index.js"
|
|
104
|
-
},
|
|
105
|
-
"./client": {
|
|
106
|
-
"import": "./dist/exports/client.js",
|
|
107
|
-
"types": "./dist/exports/client.d.ts",
|
|
108
|
-
"default": "./dist/exports/client.js"
|
|
109
|
-
},
|
|
110
|
-
"./rsc": {
|
|
111
|
-
"import": "./dist/exports/rsc.js",
|
|
112
|
-
"types": "./dist/exports/rsc.d.ts",
|
|
113
|
-
"default": "./dist/exports/rsc.js"
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
"main": "./dist/index.js",
|
|
117
|
-
"types": "./dist/index.d.ts"
|
|
118
|
-
},
|
|
119
|
-
"pnpm": {
|
|
120
|
-
"onlyBuiltDependencies": [
|
|
121
|
-
"@swc/core",
|
|
122
|
-
"sharp",
|
|
123
|
-
"esbuild"
|
|
124
|
-
]
|
|
125
|
-
},
|
|
126
78
|
"registry": "https://registry.npmjs.org/",
|
|
127
79
|
"dependencies": {
|
|
128
80
|
"@main12/brevo-adapter": "^0.1.1",
|
|
129
81
|
"axios": "^1.20.0",
|
|
130
82
|
"jose": "^6.2.12",
|
|
131
83
|
"payload-oauth2": "^1.0.21"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc && pnpm build:fix-esm-imports",
|
|
87
|
+
"build:fix-esm-imports": "node ./scripts/fix-esm-imports.mjs",
|
|
88
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
89
|
+
"build:types": "tsc --outDir dist",
|
|
90
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
91
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
92
|
+
"dev": "next dev dev --turbo",
|
|
93
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
94
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
95
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
96
|
+
"lint": "eslint",
|
|
97
|
+
"lint:fix": "eslint ./src --fix",
|
|
98
|
+
"test": "vitest"
|
|
132
99
|
}
|
|
133
|
-
}
|
|
100
|
+
}
|