@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.
- package/README.md +68 -2
- package/dist/{chunk-CTASTCWI.js → chunk-3HNFZJ7S.js} +6 -6
- package/dist/chunk-CBNSTIX6.js +17 -0
- package/dist/chunk-F2G7XJIZ.js +17 -0
- package/dist/frontend/astro.d.ts +6 -0
- package/dist/frontend/astro.js +9 -0
- package/dist/frontend/client.d.ts +54 -18
- package/dist/frontend/clients/astro-client.d.ts +3134 -0
- package/dist/frontend/clients/astro-client.js +9 -0
- package/dist/frontend/clients/vue-client.d.ts +3520 -0
- package/dist/frontend/clients/vue-client.js +9 -0
- package/dist/frontend/components/astro/AuthForm.astro +389 -0
- package/dist/frontend/components/astro/ForgotPasswordForm.astro +127 -0
- package/dist/frontend/components/astro/Organization.astro +254 -0
- package/dist/frontend/components/astro/PasswordChanger.astro +128 -0
- package/dist/frontend/components/astro/ProfileEditor.astro +133 -0
- package/dist/frontend/components/astro/ResetPasswordForm.astro +172 -0
- package/dist/frontend/components/vue/AuthForm.vue +337 -0
- package/dist/frontend/components/vue/ForgotPasswordForm.vue +108 -0
- package/dist/frontend/components/vue/Organization.vue +215 -0
- package/dist/frontend/components/vue/PasswordChanger.vue +115 -0
- package/dist/frontend/components/vue/ProfileEditor.vue +112 -0
- package/dist/frontend/components/vue/ResetPasswordForm.vue +150 -0
- package/dist/frontend/index.js +1 -1
- package/dist/frontend/vue.d.ts +12 -0
- package/dist/frontend/vue.js +23 -0
- package/dist/index.js +1 -1
- package/package.json +31 -5
|
@@ -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>
|