@main12/auth-login 0.3.1 → 0.3.3

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 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,11 @@ 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 light [color-scheme:light]",
45
+ "data-theme": "light",
46
+ style: {
47
+ maxWidth: '400px'
48
+ },
45
49
  children: [
46
50
  header,
47
51
  /*#__PURE__*/ _jsx("div", {
@@ -60,7 +64,10 @@ export const AuthLayout = ({ children, logo, title, subtitle, footer, poweredBy,
60
64
  /*#__PURE__*/ _jsx("div", {
61
65
  className: "hidden md:flex min-h-screen items-center justify-center px-4 py-12",
62
66
  children: /*#__PURE__*/ _jsxs("div", {
63
- className: "w-full max-w-[400px] animate-[fadeIn_0.5s_ease-out]",
67
+ className: "w-full max-w-md max-w-[400px] animate-[fadeIn_0.5s_ease-out]",
68
+ style: {
69
+ maxWidth: '400px'
70
+ },
64
71
  children: [
65
72
  /*#__PURE__*/ _jsxs(Card, {
66
73
  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,11 @@ 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 text-gray-900 rounded-2xl shadow-2xl w-full light [color-scheme:light] ${className}`,
55
+ "data-theme": "light",
56
+ style: {
57
+ maxWidth: '400px'
58
+ },
55
59
  children: children
56
60
  });
57
61
  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.1",
3
+ "version": "0.3.3",
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
+ }