@main12/auth-login 0.3.0 → 0.3.1

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.
@@ -21,7 +21,7 @@ function LoginContent({ onPasswordLogin, redirectTo = '/', showGoogleOAuth = tru
21
21
  poweredBy: poweredBy,
22
22
  cardClassName: cardClassName,
23
23
  backgroundClass: backgroundClass,
24
- footer: /*#__PURE__*/ _jsxs("p", {
24
+ footer: signupUrl ? /*#__PURE__*/ _jsxs("p", {
25
25
  className: "text-center text-gray-600 text-sm",
26
26
  children: [
27
27
  "Don't have an account?",
@@ -32,7 +32,7 @@ function LoginContent({ onPasswordLogin, redirectTo = '/', showGoogleOAuth = tru
32
32
  children: "Sign up"
33
33
  })
34
34
  ]
35
- }),
35
+ }) : undefined,
36
36
  children: [
37
37
  step === 'email' && /*#__PURE__*/ _jsxs("div", {
38
38
  className: "animate-[fadeIn_0.3s_ease-out]",
@@ -46,11 +46,30 @@ function LoginContent({ onPasswordLogin, redirectTo = '/', showGoogleOAuth = tru
46
46
  className: "mb-4",
47
47
  onPress: handleGoogleLogin,
48
48
  children: [
49
- /*#__PURE__*/ _jsx("span", {
50
- className: "text-lg",
51
- children: "G"
49
+ /*#__PURE__*/ _jsxs("svg", {
50
+ width: "20",
51
+ height: "20",
52
+ viewBox: "0 0 48 48",
53
+ children: [
54
+ /*#__PURE__*/ _jsx("path", {
55
+ fill: "#EA4335",
56
+ d: "M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
57
+ }),
58
+ /*#__PURE__*/ _jsx("path", {
59
+ fill: "#4285F4",
60
+ d: "M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
61
+ }),
62
+ /*#__PURE__*/ _jsx("path", {
63
+ fill: "#FBBC05",
64
+ d: "M10.53 28.59a14.5 14.5 0 0 1 0-9.18l-7.98-6.19a24.01 24.01 0 0 0 0 21.56l7.98-6.19z"
65
+ }),
66
+ /*#__PURE__*/ _jsx("path", {
67
+ fill: "#34A853",
68
+ d: "M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
69
+ })
70
+ ]
52
71
  }),
53
- " Continue with Google"
72
+ "Continue with Google"
54
73
  ]
55
74
  }),
56
75
  /*#__PURE__*/ _jsxs("div", {
@@ -50,7 +50,8 @@ export default function SetPasswordPageHero({ redirectTo = '/', logo, poweredBy,
50
50
  className: "relative",
51
51
  children: [
52
52
  /*#__PURE__*/ _jsx(Input, {
53
- className: "w-full pr-10 bg-white"
53
+ variant: "secondary",
54
+ className: "w-full pr-10"
54
55
  }),
55
56
  /*#__PURE__*/ _jsx("button", {
56
57
  type: "button",
@@ -84,7 +85,8 @@ export default function SetPasswordPageHero({ redirectTo = '/', logo, poweredBy,
84
85
  children: "Confirm Password"
85
86
  }),
86
87
  /*#__PURE__*/ _jsx(Input, {
87
- className: "w-full bg-white"
88
+ variant: "secondary",
89
+ className: "w-full"
88
90
  })
89
91
  ]
90
92
  }),
@@ -52,11 +52,30 @@ export default function SignupPage({ onSignup, showGoogleOAuth = true, loginUrl
52
52
  size: "lg",
53
53
  className: "mb-4",
54
54
  children: [
55
- /*#__PURE__*/ _jsx("span", {
56
- className: "text-lg",
57
- children: "G"
55
+ /*#__PURE__*/ _jsxs("svg", {
56
+ width: "20",
57
+ height: "20",
58
+ viewBox: "0 0 48 48",
59
+ children: [
60
+ /*#__PURE__*/ _jsx("path", {
61
+ fill: "#EA4335",
62
+ d: "M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
63
+ }),
64
+ /*#__PURE__*/ _jsx("path", {
65
+ fill: "#4285F4",
66
+ d: "M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
67
+ }),
68
+ /*#__PURE__*/ _jsx("path", {
69
+ fill: "#FBBC05",
70
+ d: "M10.53 28.59a14.5 14.5 0 0 1 0-9.18l-7.98-6.19a24.01 24.01 0 0 0 0 21.56l7.98-6.19z"
71
+ }),
72
+ /*#__PURE__*/ _jsx("path", {
73
+ fill: "#34A853",
74
+ d: "M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
75
+ })
76
+ ]
58
77
  }),
59
- " Continue with Google"
78
+ "Continue with Google"
60
79
  ]
61
80
  }),
62
81
  /*#__PURE__*/ _jsxs("div", {
@@ -49,6 +49,7 @@ function VerifyOtpHeroContent({ loginUrl = '/login', logo, poweredBy, cardClassN
49
49
  onChange: setOtp,
50
50
  isDisabled: isLoading,
51
51
  autoFocus: true,
52
+ variant: "secondary",
52
53
  children: [
53
54
  /*#__PURE__*/ _jsxs(InputOTP.Group, {
54
55
  children: [
package/dist/config.d.ts CHANGED
@@ -11,9 +11,13 @@ export declare const pluginConfig: {
11
11
  googleOAuthEnabled: boolean;
12
12
  routeRedirects: boolean;
13
13
  authBasePath: string;
14
+ passwordLogin: boolean;
15
+ otpLogin: boolean;
14
16
  };
15
17
  /** Call this client-side to mirror the server plugin config. */
16
18
  export declare function initClientConfig(opts: {
17
19
  style?: AuthStyle;
18
20
  googleOAuthEnabled?: boolean;
21
+ passwordLogin?: boolean;
22
+ otpLogin?: boolean;
19
23
  }): void;
package/dist/config.js CHANGED
@@ -2,9 +2,13 @@ export const pluginConfig = {
2
2
  style: 'tailwind',
3
3
  googleOAuthEnabled: false,
4
4
  routeRedirects: false,
5
- authBasePath: '/auth'
5
+ authBasePath: '/auth',
6
+ passwordLogin: true,
7
+ otpLogin: true
6
8
  };
7
9
  /** Call this client-side to mirror the server plugin config. */ export function initClientConfig(opts) {
8
10
  if (opts.style) pluginConfig.style = opts.style;
9
11
  if (opts.googleOAuthEnabled !== undefined) pluginConfig.googleOAuthEnabled = opts.googleOAuthEnabled;
12
+ if (opts.passwordLogin !== undefined) pluginConfig.passwordLogin = opts.passwordLogin;
13
+ if (opts.otpLogin !== undefined) pluginConfig.otpLogin = opts.otpLogin;
10
14
  }
@@ -1,9 +1,10 @@
1
+ import crypto from 'crypto';
1
2
  import { generateOtp, hashOtp, verifyOtp, getOtpExpiry, isOtpExpired } from '../auth/domain/otp.js';
2
3
  import { generateWelcomeEmail, generateOtpEmail } from '../components/email/index.js';
3
4
  /**
4
5
  * POST /api/auth/check-email — Check if user exists and has password
5
6
  */ export const checkEmailEndpoint = {
6
- path: '/api/auth/check-email',
7
+ path: '/auth/check-email',
7
8
  method: 'post',
8
9
  handler: async (req)=>{
9
10
  const { email } = req.json ? await req.json() : req.body || {};
@@ -47,7 +48,7 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
47
48
  /**
48
49
  * POST /api/auth/otp/send — Generate OTP, store it, send email via Payload
49
50
  */ export const sendOtpEndpoint = {
50
- path: '/api/auth/otp/send',
51
+ path: '/auth/otp/send',
51
52
  method: 'post',
52
53
  handler: async (req)=>{
53
54
  const body = req.json ? await req.json() : req.body || {};
@@ -81,33 +82,32 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
81
82
  const otp = generateOtp();
82
83
  const hashed = hashOtp(otp);
83
84
  const expiresAt = getOtpExpiry(10);
84
- // Store OTP in a custom collection or users doc
85
- // For simplicity, we'll use the users collection with an otp field
86
- // In production, use a dedicated `otps` collection
87
- try {
88
- await req.payload.update({
89
- collection: 'users',
90
- id: user.id,
91
- data: {
92
- otpHash: hashed,
93
- otpPurpose: purpose,
94
- otpAttempts: 0,
95
- otpExpiresAt: expiresAt.toISOString()
96
- }
97
- });
98
- } catch {
99
- // If the users collection doesn't have OTP fields, create a dedicated otps doc
100
- await req.payload.create({
101
- collection: 'otps',
102
- data: {
103
- email: email.toLowerCase().trim(),
104
- hash: hashed,
105
- purpose,
106
- attempts: 0,
107
- expiresAt: expiresAt.toISOString()
85
+ // Delete any existing OTPs for this email
86
+ const existingOtps = await req.payload.find({
87
+ collection: 'auth-otps',
88
+ where: {
89
+ email: {
90
+ equals: email.toLowerCase().trim()
108
91
  }
92
+ }
93
+ });
94
+ for (const doc of existingOtps.docs){
95
+ await req.payload.delete({
96
+ collection: 'auth-otps',
97
+ id: doc.id
109
98
  });
110
99
  }
100
+ // Store OTP in dedicated collection
101
+ await req.payload.create({
102
+ collection: 'auth-otps',
103
+ data: {
104
+ email: email.toLowerCase().trim(),
105
+ hash: hashed,
106
+ purpose,
107
+ attempts: 0,
108
+ expiresAt: expiresAt.toISOString()
109
+ }
110
+ });
111
111
  // Send email via Payload's configured adapter
112
112
  const emailResult = purpose === 'signup' ? generateOtpEmail({
113
113
  userName: user.name || user.email,
@@ -140,7 +140,7 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
140
140
  /**
141
141
  * POST /api/auth/otp/verify — Verify OTP and login the user
142
142
  */ export const verifyOtpEndpoint = {
143
- path: '/api/auth/otp/verify',
143
+ path: '/auth/otp/verify',
144
144
  method: 'post',
145
145
  handler: async (req)=>{
146
146
  const body = req.json ? await req.json() : req.body || {};
@@ -152,44 +152,19 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
152
152
  status: 400
153
153
  });
154
154
  try {
155
- // Find OTP record — try users collection first
156
- const users = await req.payload.find({
157
- collection: 'users',
155
+ // Find OTP record from dedicated collection
156
+ const otpDocs = await req.payload.find({
157
+ collection: 'auth-otps',
158
158
  where: {
159
159
  email: {
160
160
  equals: email.toLowerCase().trim()
161
161
  }
162
162
  },
163
+ sort: '-createdAt',
163
164
  limit: 1
164
165
  });
165
- let otpHash;
166
- let otpAttempts = 0;
167
- let otpExpiresAt;
168
- const user = users.docs?.[0];
169
- if (user?.otpHash) {
170
- otpHash = user.otpHash;
171
- otpAttempts = user.otpAttempts || 0;
172
- otpExpiresAt = user.otpExpiresAt;
173
- } else {
174
- // Try dedicated otps collection
175
- const otpDocs = await req.payload.find({
176
- collection: 'otps',
177
- where: {
178
- email: {
179
- equals: email.toLowerCase().trim()
180
- }
181
- },
182
- sort: '-createdAt',
183
- limit: 1
184
- });
185
- const otpDoc = otpDocs.docs?.[0];
186
- if (otpDoc) {
187
- otpHash = otpDoc.hash;
188
- otpAttempts = otpDoc.attempts || 0;
189
- otpExpiresAt = otpDoc.expiresAt;
190
- }
191
- }
192
- if (!otpHash) {
166
+ const otpDoc = otpDocs.docs?.[0];
167
+ if (!otpDoc?.hash) {
193
168
  return Response.json({
194
169
  success: false,
195
170
  error: 'No OTP found. Please request a new code.'
@@ -197,7 +172,11 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
197
172
  status: 400
198
173
  });
199
174
  }
200
- if (isOtpExpired(otpExpiresAt)) {
175
+ if (isOtpExpired(otpDoc.expiresAt)) {
176
+ await req.payload.delete({
177
+ collection: 'auth-otps',
178
+ id: otpDoc.id
179
+ });
201
180
  return Response.json({
202
181
  success: false,
203
182
  error: 'Code has expired. Please request a new one.'
@@ -205,7 +184,11 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
205
184
  status: 400
206
185
  });
207
186
  }
208
- if (otpAttempts >= 3) {
187
+ if ((otpDoc.attempts || 0) >= 3) {
188
+ await req.payload.delete({
189
+ collection: 'auth-otps',
190
+ id: otpDoc.id
191
+ });
209
192
  return Response.json({
210
193
  success: false,
211
194
  error: 'Too many attempts. Please request a new code.'
@@ -214,16 +197,14 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
214
197
  });
215
198
  }
216
199
  // Increment attempts
217
- if (user?.otpHash) {
218
- await req.payload.update({
219
- collection: 'users',
220
- id: user.id,
221
- data: {
222
- otpAttempts: otpAttempts + 1
223
- }
224
- });
225
- }
226
- if (!verifyOtp(otp, otpHash)) {
200
+ await req.payload.update({
201
+ collection: 'auth-otps',
202
+ id: otpDoc.id,
203
+ data: {
204
+ attempts: (otpDoc.attempts || 0) + 1
205
+ }
206
+ });
207
+ if (!verifyOtp(otp, otpDoc.hash)) {
227
208
  return Response.json({
228
209
  success: false,
229
210
  error: 'Invalid code. Please try again.'
@@ -231,35 +212,64 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
231
212
  status: 400
232
213
  });
233
214
  }
234
- // OTP is valid — log the user in
235
- // Payload requires password for login. We use the stored password
236
- // (auto-generated during signup or previously set by user).
237
- const userData = users.docs?.[0];
238
- const result = await req.payload.login({
215
+ // OTP is valid — delete it
216
+ await req.payload.delete({
217
+ collection: 'auth-otps',
218
+ id: otpDoc.id
219
+ });
220
+ // Find the user
221
+ const users = await req.payload.find({
239
222
  collection: 'users',
240
- data: {
241
- email: email.toLowerCase().trim(),
242
- password: userData?.password || ''
223
+ where: {
224
+ email: {
225
+ equals: email.toLowerCase().trim()
226
+ }
243
227
  },
244
- req: req
228
+ limit: 1
245
229
  });
246
- // Clean up OTP
247
- if (user?.otpHash) {
248
- await req.payload.update({
249
- collection: 'users',
250
- id: user.id,
251
- data: {
252
- otpHash: null,
253
- otpPurpose: null,
254
- otpAttempts: null,
255
- otpExpiresAt: null
256
- }
230
+ const user = users.docs?.[0];
231
+ if (!user) {
232
+ return Response.json({
233
+ success: false,
234
+ error: 'User not found'
235
+ }, {
236
+ status: 404
257
237
  });
258
238
  }
259
- return Response.json({
239
+ // Use temp password strategy (same as PaintPulse) — only touches password field
240
+ const tempPassword = crypto.randomBytes(32).toString('hex');
241
+ await req.payload.update({
242
+ collection: 'users',
243
+ id: user.id,
244
+ data: {
245
+ password: tempPassword
246
+ }
247
+ });
248
+ // Login with the temp password to get a proper JWT token
249
+ const loginResult = await req.payload.login({
250
+ collection: 'users',
251
+ data: {
252
+ email: email.toLowerCase().trim(),
253
+ password: tempPassword
254
+ }
255
+ });
256
+ if (!loginResult.user) {
257
+ throw new Error('Login failed after OTP verification');
258
+ }
259
+ // Set the auth cookie directly on the response
260
+ const response = Response.json({
260
261
  success: true,
261
- token: result.token,
262
- isNewUser: !user?.password
262
+ token: loginResult.token,
263
+ isNewUser: !user?.hasPassword
264
+ });
265
+ // Attach payload-token cookie
266
+ const cookieHeader = `payload-token=${loginResult.token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${7200}`;
267
+ return new Response(response.body, {
268
+ status: 200,
269
+ headers: {
270
+ 'Content-Type': 'application/json',
271
+ 'Set-Cookie': cookieHeader
272
+ }
263
273
  });
264
274
  } catch (err) {
265
275
  console.error('OTP verify error:', err);
@@ -275,7 +285,7 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
275
285
  /**
276
286
  * POST /api/auth/set-password — Set/update password for authenticated user
277
287
  */ export const setPasswordEndpoint = {
278
- path: '/api/auth/set-password',
288
+ path: '/auth/set-password',
279
289
  method: 'post',
280
290
  handler: async (req)=>{
281
291
  const body = req.json ? await req.json() : req.body || {};
@@ -339,9 +349,17 @@ import { generateWelcomeEmail, generateOtpEmail } from '../components/email/inde
339
349
  /**
340
350
  * POST /api/auth/signup — Create new user + send welcome email
341
351
  */ export const signupEndpoint = {
342
- path: '/api/auth/signup',
352
+ path: '/auth/signup',
343
353
  method: 'post',
344
354
  handler: async (req)=>{
355
+ if (process.env.AUTH_PLUGIN_ALLOW_SIGNUP === 'false') {
356
+ return Response.json({
357
+ success: false,
358
+ message: 'Signups are currently disabled'
359
+ }, {
360
+ status: 403
361
+ });
362
+ }
345
363
  const body = req.json ? await req.json() : req.body || {};
346
364
  const { name, email } = body;
347
365
  if (!email) {
@@ -1,4 +1,6 @@
1
1
  export { initClientConfig } from '../config';
2
+ export { AuthClientInit } from '../components/AuthClientInit';
3
+ export type { AuthClientInitProps } from '../components/AuthClientInit';
2
4
  export { useLoginFlow } from '../auth/application/hooks/useLoginFlow';
3
5
  export { useVerifyOtpFlow } from '../auth/application/hooks/useVerifyOtpFlow';
4
6
  export { useForgotPasswordFlow } from '../auth/application/hooks/useForgotPasswordFlow';
@@ -4,6 +4,7 @@
4
4
  // ============================================================
5
5
  // Plugin client config initializer
6
6
  export { initClientConfig } from '../config.js';
7
+ export { AuthClientInit } from '../components/AuthClientInit.js';
7
8
  // Auth hooks
8
9
  export { useLoginFlow } from '../auth/application/hooks/useLoginFlow.js';
9
10
  export { useVerifyOtpFlow } from '../auth/application/hooks/useVerifyOtpFlow.js';
@@ -1,3 +1,5 @@
1
+ export { default as AuthPages } from '../components/AuthPagesServer';
2
+ export type { AuthPagesProps } from '../components/AuthPagesServer';
1
3
  export { wrapInBaseTemplate, DEFAULT_COLORS, SOCIAL_ICONS, getBaseUrl, getSenderEmail, getEmailTranslations, generateOtpEmail, generateWelcomeEmail, generatePasswordResetEmail, generatePasswordChangedEmail, } from '../components/email/index';
2
4
  export type { BaseTemplateOptions, } from '../components/email/baseTemplate';
3
5
  export type { EmailColors, SocialLink, SocialPlatform, } from '../components/email/constants';
@@ -1,5 +1,7 @@
1
1
  // ============================================================
2
2
  // RSC (React Server Components) exports for @main12/auth-login/rsc
3
3
  // ============================================================
4
+ // Server component auth pages (reads plugin config automatically)
5
+ export { default as AuthPages } from '../components/AuthPagesServer.js';
4
6
  // Email template system (server-only — uses Node APIs)
5
7
  export { wrapInBaseTemplate, DEFAULT_COLORS, SOCIAL_ICONS, getBaseUrl, getSenderEmail, getEmailTranslations, generateOtpEmail, generateWelcomeEmail, generatePasswordResetEmail, generatePasswordChangedEmail } from '../components/email/index.js';
package/dist/index.d.ts CHANGED
@@ -7,6 +7,10 @@ export interface GoogleOAuthConfig {
7
7
  clientSecret?: string;
8
8
  /** Toggle provider on/off */
9
9
  enabled?: boolean;
10
+ /** Where to redirect after successful Google login. Defaults to '/admin' */
11
+ successRedirect?: string;
12
+ /** Where to redirect after failed Google login. Defaults to '/login?error=Google login failed' */
13
+ failureRedirect?: string;
10
14
  }
11
15
  export interface AuthProvidersConfig {
12
16
  google?: GoogleOAuthConfig | boolean;
@@ -40,6 +44,28 @@ export interface AuthLoginPluginOptions {
40
44
  routeRedirects?: boolean | {
41
45
  basePath?: string;
42
46
  };
47
+ /**
48
+ * Allow new users to sign up.
49
+ * When false, signup page is hidden, signup link is removed from login,
50
+ * and Google OAuth rejects unregistered users.
51
+ * @default true
52
+ */
53
+ allowSignup?: boolean;
54
+ /**
55
+ * Allow login via password.
56
+ * When true, users with passwords see the password step.
57
+ * When false, all logins go through OTP.
58
+ * @default true
59
+ */
60
+ passwordLogin?: boolean;
61
+ /**
62
+ * Allow login via OTP (email verification code).
63
+ * When true, users without passwords get OTP automatically,
64
+ * and after OTP verification they go straight to the app.
65
+ * When false, users are prompted to set a password after OTP.
66
+ * @default true
67
+ */
68
+ otpLogin?: boolean;
43
69
  }
44
- export declare const authLoginPlugin: (options?: AuthLoginPluginOptions) => (config: Config) => Config;
70
+ export declare const authLoginPlugin: (options?: AuthLoginPluginOptions) => (config: Config) => Promise<Config>;
45
71
  export { pluginConfig };