@contentgrowth/content-auth 0.4.9 → 0.5.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.
@@ -0,0 +1,9 @@
1
+ import {
2
+ authClient,
3
+ createClient
4
+ } from "../../chunk-F2G7XJIZ.js";
5
+ import "../../chunk-R5U7XKVJ.js";
6
+ export {
7
+ authClient,
8
+ createClient
9
+ };
@@ -0,0 +1,389 @@
1
+ ---
2
+
3
+ export interface Props {
4
+ view?: 'signin' | 'signup';
5
+ baseUrl?: string;
6
+ socialProviders?: string[];
7
+ socialLayout?: 'row' | 'column';
8
+ title?: string;
9
+ width?: 'default' | 'compact' | 'wide';
10
+ layout?: 'default' | 'split';
11
+ socialPosition?: 'top' | 'bottom';
12
+ defaultEmail?: string;
13
+ lockEmail?: boolean;
14
+ forgotPasswordUrl?: string;
15
+ turnstileSiteKey?: string;
16
+ redirectUrl?: string;
17
+ }
18
+
19
+ const {
20
+ view = 'signin',
21
+ baseUrl,
22
+ socialProviders = [],
23
+ socialLayout = 'row',
24
+ title,
25
+ width = 'default',
26
+ layout = 'default',
27
+ socialPosition = 'top',
28
+ defaultEmail = '',
29
+ lockEmail = false,
30
+ forgotPasswordUrl,
31
+ turnstileSiteKey,
32
+ redirectUrl
33
+ } = Astro.props;
34
+
35
+ const isLogin = view !== 'signup';
36
+ const widthClass = width === 'compact' ? 'ca-width-compact' : width === 'wide' ? 'ca-width-wide' : 'ca-width-default';
37
+ const containerClass = `ca-container ${layout === 'split' ? 'ca-layout-split' : ''} ${widthClass}`;
38
+ const socialClass = socialLayout === 'column' ? 'ca-social-column' : 'ca-social-grid';
39
+ const defaultTitle = isLogin ? 'Welcome Back' : 'Create Account';
40
+
41
+ const googleSvg = `<svg class="ca-icon" viewBox="0 0 24 24" width="20" height="20" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1, 0, 0, 1, 27.009001, -39.238998)"><path fill="#4285F4" d="M -3.264 51.509 C -3.264 50.719 -3.334 49.969 -3.454 49.239 L -14.754 49.239 L -14.754 53.749 L -8.284 53.749 C -8.574 55.229 -9.424 56.479 -10.684 57.329 L -10.684 60.329 L -6.824 60.329 C -4.564 58.239 -3.264 55.159 -3.264 51.509 Z" /><path fill="#34A853" d="M -14.754 63.239 C -11.514 63.239 -8.804 62.159 -6.824 60.329 L -10.684 57.329 C -11.764 58.049 -13.134 58.489 -14.754 58.489 C -17.884 58.489 -20.534 56.379 -21.484 53.529 L -25.464 53.529 L -25.464 56.619 C -23.494 60.539 -19.444 63.239 -14.754 63.239 Z" /><path fill="#FBBC05" d="M -21.484 53.529 C -21.734 52.809 -21.864 52.039 -21.864 51.239 C -21.864 50.439 -21.734 49.669 -21.484 48.949 L -21.484 45.859 L -25.464 45.859 C -26.284 47.479 -26.754 49.299 -26.754 51.239 C -26.754 53.179 -26.284 54.999 -25.464 56.619 L -21.484 53.529 Z" /><path fill="#EA4335" d="M -14.754 43.989 C -12.984 43.989 -11.424 44.599 -10.174 45.789 L -6.744 42.359 C -8.804 40.429 -11.514 39.239 -14.754 39.239 C -19.444 39.239 -23.494 41.939 -25.464 45.859 L -21.484 48.949 C -20.534 46.099 -17.884 43.989 -14.754 43.989 Z" /></g></svg>`;
42
+ const githubSvg = `<svg class="ca-icon" viewBox="0 0 24 24" width="20" height="20" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.026A9.564 9.564 0 0 1 12 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0 0 22 12c0-5.523-4.477-10-10-10z" fill="currentColor" /></svg>`;
43
+ ---
44
+
45
+ <div class={containerClass} data-auth-form data-base-url={baseUrl} data-view={view} data-redirect-url={redirectUrl} data-lock-email={lockEmail} data-turnstile-site-key={turnstileSiteKey}>
46
+ <h2 class="ca-title" data-title>{title || defaultTitle}</h2>
47
+
48
+ {layout === 'split' ? (
49
+ <div class="ca-split-body">
50
+ <div class="ca-split-main">
51
+ <form class="ca-form" data-auth-form-element>
52
+ {!isLogin && (
53
+ <div class="ca-input-group" data-name-group>
54
+ <label class="ca-label" for="name">Name</label>
55
+ <input id="name" type="text" class="ca-input" name="name" required />
56
+ </div>
57
+ )}
58
+
59
+ <div class="ca-input-group">
60
+ <label class="ca-label" for="email">Email</label>
61
+ <input
62
+ id="email"
63
+ type="email"
64
+ class={`ca-input ${lockEmail ? 'ca-input-locked' : ''}`}
65
+ name="email"
66
+ value={defaultEmail}
67
+ readonly={lockEmail}
68
+ required
69
+ />
70
+ </div>
71
+
72
+ <div class="ca-input-group">
73
+ <div class="ca-label-row">
74
+ <label class="ca-label" for="password">Password</label>
75
+ {isLogin && forgotPasswordUrl && (
76
+ <a href={forgotPasswordUrl} class="ca-forgot-link">Forgot password?</a>
77
+ )}
78
+ </div>
79
+ <input id="password" type="password" class="ca-input" name="password" required />
80
+ </div>
81
+
82
+ {turnstileSiteKey && !isLogin && (
83
+ <div class="ca-turnstile" data-turnstile-container>
84
+ <p class="ca-turnstile-hint">Please complete the security check above</p>
85
+ </div>
86
+ )}
87
+
88
+ <div class="ca-error" data-error-message style="display: none;"></div>
89
+
90
+ <button type="submit" class="ca-button" data-submit-button>
91
+ {isLogin ? 'Sign In' : 'Sign Up'}
92
+ </button>
93
+ </form>
94
+
95
+ <div class="ca-footer">
96
+ <span data-mode-text>{isLogin ? "Don't have an account? " : "Already have an account? "}</span>
97
+ <button class="ca-link" type="button" data-switch-mode>
98
+ {isLogin ? 'Sign up' : 'Sign in'}
99
+ </button>
100
+ </div>
101
+ </div>
102
+ {socialProviders.length > 0 && !lockEmail && (
103
+ <>
104
+ <div class="ca-split-divider">
105
+ <span class="ca-split-divider-text">Or</span>
106
+ </div>
107
+ <div class="ca-split-social">
108
+ <h3 class="ca-social-header" data-social-header>
109
+ {isLogin ? 'Sign In With' : 'Sign Up With'}
110
+ </h3>
111
+ <div class={socialClass}>
112
+ {socialProviders.map(provider => (
113
+ <button
114
+ type="button"
115
+ class={`ca-button ca-button-social ca-button-${provider}`}
116
+ data-social-provider={provider}
117
+ >
118
+ <Fragment set:html={provider === 'google' ? googleSvg : githubSvg} />
119
+ <span class="ca-btn-text">{provider === 'github' ? 'GitHub' : 'Google'}</span>
120
+ </button>
121
+ ))}
122
+ </div>
123
+ </div>
124
+ </>
125
+ )}
126
+ </div>
127
+ ) : (
128
+ <>
129
+ {socialPosition === 'top' && socialProviders.length > 0 && !lockEmail && (
130
+ <>
131
+ <div class={socialClass}>
132
+ {socialProviders.map(provider => (
133
+ <button
134
+ type="button"
135
+ class={`ca-button ca-button-social ca-button-${provider}`}
136
+ data-social-provider={provider}
137
+ >
138
+ <Fragment set:html={provider === 'google' ? googleSvg : githubSvg} />
139
+ <span class="ca-btn-text">{provider === 'github' ? 'GitHub' : 'Google'}</span>
140
+ </button>
141
+ ))}
142
+ </div>
143
+ <div class="ca-divider">
144
+ <span class="ca-divider-text">Or continue with</span>
145
+ </div>
146
+ </>
147
+ )}
148
+
149
+ <form class="ca-form" data-auth-form-element>
150
+ {!isLogin && (
151
+ <div class="ca-input-group" data-name-group>
152
+ <label class="ca-label" for="name">Name</label>
153
+ <input id="name" type="text" class="ca-input" name="name" required />
154
+ </div>
155
+ )}
156
+
157
+ <div class="ca-input-group">
158
+ <label class="ca-label" for="email">Email</label>
159
+ <input
160
+ id="email"
161
+ type="email"
162
+ class={`ca-input ${lockEmail ? 'ca-input-locked' : ''}`}
163
+ name="email"
164
+ value={defaultEmail}
165
+ readonly={lockEmail}
166
+ required
167
+ />
168
+ </div>
169
+
170
+ <div class="ca-input-group">
171
+ <div class="ca-label-row">
172
+ <label class="ca-label" for="password">Password</label>
173
+ {isLogin && forgotPasswordUrl && (
174
+ <a href={forgotPasswordUrl} class="ca-forgot-link">Forgot password?</a>
175
+ )}
176
+ </div>
177
+ <input id="password" type="password" class="ca-input" name="password" required />
178
+ </div>
179
+
180
+ {turnstileSiteKey && !isLogin && (
181
+ <div class="ca-turnstile" data-turnstile-container>
182
+ <p class="ca-turnstile-hint">Please complete the security check above</p>
183
+ </div>
184
+ )}
185
+
186
+ <div class="ca-error" data-error-message style="display: none;"></div>
187
+
188
+ <button type="submit" class="ca-button" data-submit-button>
189
+ {isLogin ? 'Sign In' : 'Sign Up'}
190
+ </button>
191
+ </form>
192
+
193
+ {socialPosition === 'bottom' && socialProviders.length > 0 && !lockEmail && (
194
+ <>
195
+ <div class="ca-divider">
196
+ <span class="ca-divider-text">Or continue with</span>
197
+ </div>
198
+ <div class={socialClass}>
199
+ {socialProviders.map(provider => (
200
+ <button
201
+ type="button"
202
+ class={`ca-button ca-button-social ca-button-${provider}`}
203
+ data-social-provider={provider}
204
+ >
205
+ <Fragment set:html={provider === 'google' ? googleSvg : githubSvg} />
206
+ <span class="ca-btn-text">{provider === 'github' ? 'GitHub' : 'Google'}</span>
207
+ </button>
208
+ ))}
209
+ </div>
210
+ </>
211
+ )}
212
+
213
+ <div class="ca-footer">
214
+ <span data-mode-text>{isLogin ? "Don't have an account? " : "Already have an account? "}</span>
215
+ <button class="ca-link" type="button" data-switch-mode>
216
+ {isLogin ? 'Sign up' : 'Sign in'}
217
+ </button>
218
+ </div>
219
+ </>
220
+ )}
221
+ </div>
222
+
223
+ <script>
224
+ import { createClient } from '../../clients/astro-client';
225
+
226
+ document.querySelectorAll('[data-auth-form]').forEach((container) => {
227
+ const baseUrl = container.getAttribute('data-base-url') || undefined;
228
+ const client = createClient(baseUrl);
229
+ let isLogin = container.getAttribute('data-view') !== 'signup';
230
+ const redirectUrl = container.getAttribute('data-redirect-url');
231
+ const lockEmail = container.getAttribute('data-lock-email') === 'true';
232
+ const turnstileSiteKey = container.getAttribute('data-turnstile-site-key');
233
+
234
+ const form = container.querySelector('[data-auth-form-element]') as HTMLFormElement;
235
+ const submitBtn = container.querySelector('[data-submit-button]') as HTMLButtonElement;
236
+ const errorDiv = container.querySelector('[data-error-message]') as HTMLDivElement;
237
+ const switchBtn = container.querySelector('[data-switch-mode]') as HTMLButtonElement;
238
+ const modeText = container.querySelector('[data-mode-text]') as HTMLSpanElement;
239
+ const titleEl = container.querySelector('[data-title]') as HTMLHeadingElement;
240
+ const nameGroup = container.querySelector('[data-name-group]') as HTMLDivElement | null;
241
+ const socialHeader = container.querySelector('[data-social-header]') as HTMLHeadingElement | null;
242
+ const socialButtons = container.querySelectorAll('[data-social-provider]');
243
+ const turnstileContainer = container.querySelector('[data-turnstile-container]') as HTMLDivElement | null;
244
+
245
+ let turnstileToken: string | null = null;
246
+ let turnstileWidget: any = null;
247
+
248
+ // Load Turnstile if needed
249
+ if (turnstileSiteKey && turnstileContainer) {
250
+ const script = document.createElement('script');
251
+ script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback';
252
+ script.async = true;
253
+ script.defer = true;
254
+ document.head.appendChild(script);
255
+
256
+ (window as any).onloadTurnstileCallback = () => {
257
+ if (!isLogin && turnstileContainer) {
258
+ turnstileWidget = (window as any).turnstile.render(turnstileContainer, {
259
+ sitekey: turnstileSiteKey,
260
+ callback: (token: string) => { turnstileToken = token; },
261
+ 'error-callback': () => { turnstileToken = null; },
262
+ 'expired-callback': () => {
263
+ turnstileToken = null;
264
+ if (turnstileWidget) (window as any).turnstile.reset(turnstileWidget);
265
+ }
266
+ });
267
+ }
268
+ };
269
+ }
270
+
271
+ const showError = (message: string) => {
272
+ errorDiv.textContent = message;
273
+ errorDiv.style.display = 'block';
274
+ };
275
+
276
+ const hideError = () => {
277
+ errorDiv.style.display = 'none';
278
+ };
279
+
280
+ const setLoading = (loading: boolean) => {
281
+ submitBtn.disabled = loading;
282
+ submitBtn.textContent = loading ? 'Loading...' : (isLogin ? 'Sign In' : 'Sign Up');
283
+ };
284
+
285
+ const updateMode = () => {
286
+ if (titleEl) titleEl.textContent = isLogin ? 'Welcome Back' : 'Create Account';
287
+ if (modeText) modeText.textContent = isLogin ? "Don't have an account? " : "Already have an account? ";
288
+ if (switchBtn) switchBtn.textContent = isLogin ? 'Sign up' : 'Sign in';
289
+ if (submitBtn) submitBtn.textContent = isLogin ? 'Sign In' : 'Sign Up';
290
+ if (nameGroup) nameGroup.style.display = isLogin ? 'none' : 'flex';
291
+ if (socialHeader) socialHeader.textContent = isLogin ? 'Sign In With' : 'Sign Up With';
292
+ if (turnstileContainer) {
293
+ turnstileContainer.style.display = isLogin ? 'none' : 'flex';
294
+ if (!isLogin && turnstileWidget) {
295
+ (window as any).turnstile?.reset(turnstileWidget);
296
+ turnstileToken = null;
297
+ }
298
+ }
299
+ };
300
+
301
+ // Handle mode switch
302
+ switchBtn?.addEventListener('click', () => {
303
+ isLogin = !isLogin;
304
+ updateMode();
305
+ hideError();
306
+ });
307
+
308
+ // Handle social login
309
+ socialButtons.forEach(btn => {
310
+ btn.addEventListener('click', async () => {
311
+ const provider = btn.getAttribute('data-social-provider');
312
+ if (!provider) return;
313
+
314
+ setLoading(true);
315
+ try {
316
+ await client.signIn.social({
317
+ provider: provider as any,
318
+ callbackURL: redirectUrl || window.location.href
319
+ });
320
+ } catch (err: any) {
321
+ showError(err.message || `Failed to sign in with ${provider}`);
322
+ setLoading(false);
323
+ }
324
+ });
325
+ });
326
+
327
+ // Handle form submit
328
+ form?.addEventListener('submit', async (e) => {
329
+ e.preventDefault();
330
+ hideError();
331
+
332
+ const formData = new FormData(form);
333
+ const email = formData.get('email') as string;
334
+ const password = formData.get('password') as string;
335
+ const name = formData.get('name') as string;
336
+
337
+ if (!isLogin && turnstileSiteKey && !turnstileToken) {
338
+ showError('Please complete the security challenge');
339
+ return;
340
+ }
341
+
342
+ setLoading(true);
343
+
344
+ try {
345
+ let response: any;
346
+ if (isLogin) {
347
+ response = await client.signIn.email({ email, password });
348
+ if (response.error) throw response.error;
349
+ } else {
350
+ const signupData: any = { email, password, name };
351
+ if (turnstileToken) signupData.turnstileToken = turnstileToken;
352
+ response = await client.signUp.email(signupData);
353
+ if (response.error) throw response.error;
354
+ }
355
+
356
+ if (redirectUrl) {
357
+ window.location.href = redirectUrl;
358
+ }
359
+ } catch (err: any) {
360
+ const errorMessage = err.message || '';
361
+ let friendlyMessage = 'An error occurred. Please try again.';
362
+
363
+ if (errorMessage.includes('TURNSTILE') || errorMessage.includes('security challenge')) {
364
+ friendlyMessage = 'Security verification failed. Please complete the challenge and try again.';
365
+ } else if (errorMessage.includes('EMAIL_EXISTS') || errorMessage.includes('already exists')) {
366
+ friendlyMessage = 'An account with this email already exists. Try signing in instead.';
367
+ } else if (errorMessage.includes('Invalid email') || errorMessage.includes('invalid email')) {
368
+ friendlyMessage = 'Please enter a valid email address.';
369
+ } else if (errorMessage.includes('Invalid password') || errorMessage.includes('incorrect')) {
370
+ friendlyMessage = 'Invalid email or password. Please check your credentials.';
371
+ } else if (errorMessage.includes('User not found')) {
372
+ friendlyMessage = 'No account found with this email. Try signing up instead.';
373
+ } else if (errorMessage.includes('too short') || errorMessage.includes('password')) {
374
+ friendlyMessage = 'Password must be at least 8 characters long.';
375
+ } else if (err.message) {
376
+ friendlyMessage = err.message;
377
+ }
378
+
379
+ showError(friendlyMessage);
380
+ if (turnstileWidget) {
381
+ (window as any).turnstile?.reset(turnstileWidget);
382
+ turnstileToken = null;
383
+ }
384
+ } finally {
385
+ setLoading(false);
386
+ }
387
+ });
388
+ });
389
+ </script>
@@ -0,0 +1,127 @@
1
+ ---
2
+ export interface Props {
3
+ baseUrl?: string;
4
+ backToLoginUrl?: string;
5
+ title?: string;
6
+ width?: 'default' | 'compact' | 'wide';
7
+ }
8
+
9
+ const {
10
+ baseUrl,
11
+ backToLoginUrl,
12
+ title = 'Reset Password',
13
+ width = 'default'
14
+ } = Astro.props;
15
+
16
+ const widthClass = width === 'compact' ? 'ca-width-compact' : width === 'wide' ? 'ca-width-wide' : 'ca-width-default';
17
+ ---
18
+
19
+ <div class={`ca-container ${widthClass}`} data-forgot-password-form data-base-url={baseUrl}>
20
+ <h2 class="ca-title">{title}</h2>
21
+ <p class="ca-subtitle">
22
+ Enter your email address and we'll send you a link to reset your password.
23
+ </p>
24
+
25
+ <form class="ca-form" data-form-element>
26
+ <div class="ca-input-group">
27
+ <label class="ca-label" for="email">Email</label>
28
+ <input
29
+ id="email"
30
+ type="email"
31
+ class="ca-input"
32
+ name="email"
33
+ placeholder="you@example.com"
34
+ required
35
+ />
36
+ </div>
37
+
38
+ <div class="ca-error" data-error-message style="display: none;"></div>
39
+
40
+ <button type="submit" class="ca-button" data-submit-button>
41
+ Send Reset Link
42
+ </button>
43
+ </form>
44
+
45
+ {backToLoginUrl && (
46
+ <div class="ca-footer">
47
+ <a href={backToLoginUrl} class="ca-link">Back to Sign In</a>
48
+ </div>
49
+ )}
50
+
51
+ <!-- Success state (hidden initially) -->
52
+ <div data-success-state style="display: none;">
53
+ <div class="ca-success-message">
54
+ <svg class="ca-success-icon" viewBox="0 0 24 24" width="48" height="48">
55
+ <circle cx="12" cy="12" r="10" fill="#10B981" />
56
+ <path d="M8 12l2.5 2.5L16 9" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
57
+ </svg>
58
+ <h3 class="ca-success-title">Check your email</h3>
59
+ <p class="ca-success-text">
60
+ We've sent a password reset link to <strong data-email-display></strong>.
61
+ Please check your inbox and click the link to reset your password.
62
+ </p>
63
+ </div>
64
+ {backToLoginUrl && (
65
+ <div class="ca-footer">
66
+ <a href={backToLoginUrl} class="ca-link">Back to Sign In</a>
67
+ </div>
68
+ )}
69
+ </div>
70
+ </div>
71
+
72
+ <script>
73
+ import { createClient } from '../../clients/astro-client';
74
+
75
+ document.querySelectorAll('[data-forgot-password-form]').forEach((container) => {
76
+ const baseUrl = container.getAttribute('data-base-url') || undefined;
77
+ const client = createClient(baseUrl);
78
+
79
+ const form = container.querySelector('[data-form-element]') as HTMLFormElement;
80
+ const submitBtn = container.querySelector('[data-submit-button]') as HTMLButtonElement;
81
+ const errorDiv = container.querySelector('[data-error-message]') as HTMLDivElement;
82
+ const successState = container.querySelector('[data-success-state]') as HTMLDivElement;
83
+ const emailDisplay = container.querySelector('[data-email-display]') as HTMLElement;
84
+ const formElements = container.querySelectorAll('.ca-form, .ca-subtitle, .ca-title');
85
+
86
+ const showError = (message: string) => {
87
+ errorDiv.textContent = message;
88
+ errorDiv.style.display = 'block';
89
+ };
90
+
91
+ const hideError = () => {
92
+ errorDiv.style.display = 'none';
93
+ };
94
+
95
+ const setLoading = (loading: boolean) => {
96
+ submitBtn.disabled = loading;
97
+ submitBtn.textContent = loading ? 'Sending...' : 'Send Reset Link';
98
+ };
99
+
100
+ form?.addEventListener('submit', async (e) => {
101
+ e.preventDefault();
102
+ hideError();
103
+
104
+ const formData = new FormData(form);
105
+ const email = formData.get('email') as string;
106
+
107
+ setLoading(true);
108
+
109
+ try {
110
+ const { error } = await client.requestPasswordReset({
111
+ email,
112
+ redirectTo: window.location.origin + '/auth/reset-password'
113
+ });
114
+ if (error) throw error;
115
+
116
+ // Show success state
117
+ emailDisplay.textContent = email;
118
+ formElements.forEach(el => (el as HTMLElement).style.display = 'none');
119
+ successState.style.display = 'block';
120
+ } catch (err: any) {
121
+ showError(err.message || 'Failed to send reset email');
122
+ } finally {
123
+ setLoading(false);
124
+ }
125
+ });
126
+ });
127
+ </script>