@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,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
// CreateOrganizationForm Component
|
|
3
|
+
export interface CreateOrganizationFormProps {
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// OrganizationSwitcher Component
|
|
9
|
+
export interface OrganizationSwitcherProps {
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
currentOrgId?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// InviteMemberForm Component
|
|
16
|
+
export interface InviteMemberFormProps {
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Props =
|
|
22
|
+
| { component: 'create'; props?: CreateOrganizationFormProps }
|
|
23
|
+
| { component: 'switcher'; props?: OrganizationSwitcherProps }
|
|
24
|
+
| { component: 'invite'; props?: InviteMemberFormProps };
|
|
25
|
+
|
|
26
|
+
const { component, props = {} } = Astro.props;
|
|
27
|
+
const baseUrl = (props as any).baseUrl || '';
|
|
28
|
+
const className = (props as any).className || '';
|
|
29
|
+
const currentOrgId = (props as any).currentOrgId || '';
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
{component === 'create' && (
|
|
33
|
+
<form class={`ca-form ${className}`} data-create-org-form data-base-url={baseUrl}>
|
|
34
|
+
<h3 class="ca-subtitle">Create Organization</h3>
|
|
35
|
+
<div class="ca-input-group">
|
|
36
|
+
<label class="ca-label">Organization Name</label>
|
|
37
|
+
<input
|
|
38
|
+
type="text"
|
|
39
|
+
class="ca-input"
|
|
40
|
+
name="name"
|
|
41
|
+
required
|
|
42
|
+
placeholder="Acme Corp"
|
|
43
|
+
data-name-input
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="ca-input-group">
|
|
47
|
+
<label class="ca-label">Slug</label>
|
|
48
|
+
<input
|
|
49
|
+
type="text"
|
|
50
|
+
class="ca-input"
|
|
51
|
+
name="slug"
|
|
52
|
+
required
|
|
53
|
+
placeholder="acme-corp"
|
|
54
|
+
data-slug-input
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="ca-error" data-error-message style="display: none;"></div>
|
|
58
|
+
<div class="ca-success-message" data-success-message style="display: none; padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
|
|
59
|
+
Organization created!
|
|
60
|
+
</div>
|
|
61
|
+
<button type="submit" class="ca-button" data-submit-button>
|
|
62
|
+
Create Organization
|
|
63
|
+
</button>
|
|
64
|
+
</form>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{component === 'switcher' && (
|
|
68
|
+
<div class={`ca-org-switcher ${className}`} data-org-switcher data-base-url={baseUrl} data-current-org-id={currentOrgId}>
|
|
69
|
+
<label class="ca-label">Select Organization</label>
|
|
70
|
+
<select class="ca-select" data-org-select disabled>
|
|
71
|
+
<option value="">Loading...</option>
|
|
72
|
+
</select>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{component === 'invite' && (
|
|
77
|
+
<form class={`ca-form ${className}`} data-invite-member-form data-base-url={baseUrl}>
|
|
78
|
+
<h3 class="ca-subtitle">Invite Member</h3>
|
|
79
|
+
<div class="ca-input-group">
|
|
80
|
+
<label class="ca-label">Email Address</label>
|
|
81
|
+
<input
|
|
82
|
+
type="email"
|
|
83
|
+
class="ca-input"
|
|
84
|
+
name="email"
|
|
85
|
+
required
|
|
86
|
+
placeholder="colleague@example.com"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="ca-input-group">
|
|
90
|
+
<label class="ca-label">Role</label>
|
|
91
|
+
<select class="ca-select" name="role">
|
|
92
|
+
<option value="member">Member</option>
|
|
93
|
+
<option value="admin">Admin</option>
|
|
94
|
+
<option value="owner">Owner</option>
|
|
95
|
+
</select>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="ca-error" data-error-message style="display: none;"></div>
|
|
98
|
+
<div class="ca-success-message" data-success-message style="display: none; padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
|
|
99
|
+
Invitation sent!
|
|
100
|
+
</div>
|
|
101
|
+
<button type="submit" class="ca-button" data-submit-button>
|
|
102
|
+
Send Invite
|
|
103
|
+
</button>
|
|
104
|
+
</form>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
<script>
|
|
108
|
+
import { createClient } from '../../clients/astro-client';
|
|
109
|
+
|
|
110
|
+
// Create Organization Form
|
|
111
|
+
document.querySelectorAll('[data-create-org-form]').forEach((form) => {
|
|
112
|
+
const baseUrl = form.getAttribute('data-base-url') || undefined;
|
|
113
|
+
const client = createClient(baseUrl);
|
|
114
|
+
|
|
115
|
+
const nameInput = form.querySelector('[data-name-input]') as HTMLInputElement;
|
|
116
|
+
const slugInput = form.querySelector('[data-slug-input]') as HTMLInputElement;
|
|
117
|
+
const submitBtn = form.querySelector('[data-submit-button]') as HTMLButtonElement;
|
|
118
|
+
const errorDiv = form.querySelector('[data-error-message]') as HTMLDivElement;
|
|
119
|
+
const successDiv = form.querySelector('[data-success-message]') as HTMLDivElement;
|
|
120
|
+
|
|
121
|
+
// Auto-slugify
|
|
122
|
+
nameInput?.addEventListener('input', () => {
|
|
123
|
+
if (!slugInput.value || slugInput.dataset.autoSlug !== 'false') {
|
|
124
|
+
slugInput.value = nameInput.value.toLowerCase().replace(/\s+/g, '-');
|
|
125
|
+
slugInput.dataset.autoSlug = 'true';
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
slugInput?.addEventListener('input', () => {
|
|
130
|
+
slugInput.dataset.autoSlug = 'false';
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const showError = (message: string) => {
|
|
134
|
+
errorDiv.textContent = message;
|
|
135
|
+
errorDiv.style.display = 'block';
|
|
136
|
+
successDiv.style.display = 'none';
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const showSuccess = () => {
|
|
140
|
+
successDiv.style.display = 'block';
|
|
141
|
+
errorDiv.style.display = 'none';
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const setLoading = (loading: boolean) => {
|
|
145
|
+
submitBtn.disabled = loading;
|
|
146
|
+
submitBtn.textContent = loading ? 'Creating...' : 'Create Organization';
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
form.addEventListener('submit', async (e) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
|
|
152
|
+
const formData = new FormData(form as HTMLFormElement);
|
|
153
|
+
const name = formData.get('name') as string;
|
|
154
|
+
const slug = formData.get('slug') as string;
|
|
155
|
+
|
|
156
|
+
setLoading(true);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const result = await client.organization.create({ name, slug });
|
|
160
|
+
if (result.error) throw result.error;
|
|
161
|
+
(form as HTMLFormElement).reset();
|
|
162
|
+
showSuccess();
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
showError(err.message || 'Failed to create organization');
|
|
165
|
+
} finally {
|
|
166
|
+
setLoading(false);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Organization Switcher
|
|
172
|
+
document.querySelectorAll('[data-org-switcher]').forEach(async (container) => {
|
|
173
|
+
const baseUrl = container.getAttribute('data-base-url') || undefined;
|
|
174
|
+
const currentOrgId = container.getAttribute('data-current-org-id') || '';
|
|
175
|
+
const client = createClient(baseUrl);
|
|
176
|
+
const select = container.querySelector('[data-org-select]') as HTMLSelectElement;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const { data } = await client.organization.list({});
|
|
180
|
+
if (data && data.length > 0) {
|
|
181
|
+
select.innerHTML = '<option value="" disabled>Select an organization</option>';
|
|
182
|
+
data.forEach((org: any) => {
|
|
183
|
+
const option = document.createElement('option');
|
|
184
|
+
option.value = org.id;
|
|
185
|
+
option.textContent = org.name;
|
|
186
|
+
if (org.id === currentOrgId) option.selected = true;
|
|
187
|
+
select.appendChild(option);
|
|
188
|
+
});
|
|
189
|
+
select.disabled = false;
|
|
190
|
+
} else {
|
|
191
|
+
select.innerHTML = '<option value="" disabled>No organizations</option>';
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
select.innerHTML = '<option value="" disabled>Failed to load</option>';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
select?.addEventListener('change', async () => {
|
|
198
|
+
const orgId = select.value;
|
|
199
|
+
if (!orgId) return;
|
|
200
|
+
await client.organization.setActive({ organizationId: orgId });
|
|
201
|
+
window.dispatchEvent(new CustomEvent('org-switched', { detail: { orgId } }));
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Invite Member Form
|
|
206
|
+
document.querySelectorAll('[data-invite-member-form]').forEach((form) => {
|
|
207
|
+
const baseUrl = form.getAttribute('data-base-url') || undefined;
|
|
208
|
+
const client = createClient(baseUrl);
|
|
209
|
+
|
|
210
|
+
const submitBtn = form.querySelector('[data-submit-button]') as HTMLButtonElement;
|
|
211
|
+
const errorDiv = form.querySelector('[data-error-message]') as HTMLDivElement;
|
|
212
|
+
const successDiv = form.querySelector('[data-success-message]') as HTMLDivElement;
|
|
213
|
+
|
|
214
|
+
const showError = (message: string) => {
|
|
215
|
+
errorDiv.textContent = message;
|
|
216
|
+
errorDiv.style.display = 'block';
|
|
217
|
+
successDiv.style.display = 'none';
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const showSuccess = () => {
|
|
221
|
+
successDiv.style.display = 'block';
|
|
222
|
+
errorDiv.style.display = 'none';
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const setLoading = (loading: boolean) => {
|
|
226
|
+
submitBtn.disabled = loading;
|
|
227
|
+
submitBtn.textContent = loading ? 'Sending Invite...' : 'Send Invite';
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
form.addEventListener('submit', async (e) => {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
|
|
233
|
+
const formData = new FormData(form as HTMLFormElement);
|
|
234
|
+
const email = formData.get('email') as string;
|
|
235
|
+
const role = formData.get('role') as string;
|
|
236
|
+
|
|
237
|
+
setLoading(true);
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const result = await client.organization.inviteMember({
|
|
241
|
+
email,
|
|
242
|
+
role: role as any
|
|
243
|
+
});
|
|
244
|
+
if (result.error) throw result.error;
|
|
245
|
+
(form as HTMLFormElement).reset();
|
|
246
|
+
showSuccess();
|
|
247
|
+
} catch (err: any) {
|
|
248
|
+
showError(err.message || 'Invitation failed');
|
|
249
|
+
} finally {
|
|
250
|
+
setLoading(false);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
</script>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface Props {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { baseUrl, className = '' } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<form class={`ca-form ${className}`} data-password-changer data-base-url={baseUrl}>
|
|
11
|
+
<div class="ca-input-group">
|
|
12
|
+
<label class="ca-label" for="current-password">Current Password</label>
|
|
13
|
+
<input
|
|
14
|
+
id="current-password"
|
|
15
|
+
type="password"
|
|
16
|
+
class="ca-input"
|
|
17
|
+
name="currentPassword"
|
|
18
|
+
required
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="ca-input-group">
|
|
23
|
+
<label class="ca-label" for="new-password">New Password</label>
|
|
24
|
+
<input
|
|
25
|
+
id="new-password"
|
|
26
|
+
type="password"
|
|
27
|
+
class="ca-input"
|
|
28
|
+
name="newPassword"
|
|
29
|
+
minlength={8}
|
|
30
|
+
required
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="ca-input-group">
|
|
35
|
+
<label class="ca-label" for="confirm-password">Confirm New Password</label>
|
|
36
|
+
<input
|
|
37
|
+
id="confirm-password"
|
|
38
|
+
type="password"
|
|
39
|
+
class="ca-input"
|
|
40
|
+
name="confirmPassword"
|
|
41
|
+
minlength={8}
|
|
42
|
+
required
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="ca-error" data-error-message style="display: none;"></div>
|
|
47
|
+
<div class="ca-success-message" data-success-message style="display: none; padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
|
|
48
|
+
Password updated successfully!
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<button type="submit" class="ca-button" data-submit-button>
|
|
52
|
+
Update Password
|
|
53
|
+
</button>
|
|
54
|
+
</form>
|
|
55
|
+
|
|
56
|
+
<script>
|
|
57
|
+
import { createClient } from '../../clients/astro-client';
|
|
58
|
+
|
|
59
|
+
document.querySelectorAll('[data-password-changer]').forEach((form) => {
|
|
60
|
+
const baseUrl = form.getAttribute('data-base-url') || undefined;
|
|
61
|
+
const client = createClient(baseUrl);
|
|
62
|
+
|
|
63
|
+
const submitBtn = form.querySelector('[data-submit-button]') as HTMLButtonElement;
|
|
64
|
+
const errorDiv = form.querySelector('[data-error-message]') as HTMLDivElement;
|
|
65
|
+
const successDiv = form.querySelector('[data-success-message]') as HTMLDivElement;
|
|
66
|
+
|
|
67
|
+
const showError = (message: string) => {
|
|
68
|
+
errorDiv.textContent = message;
|
|
69
|
+
errorDiv.style.display = 'block';
|
|
70
|
+
successDiv.style.display = 'none';
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const showSuccess = () => {
|
|
74
|
+
successDiv.style.display = 'block';
|
|
75
|
+
errorDiv.style.display = 'none';
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const hideMessages = () => {
|
|
79
|
+
errorDiv.style.display = 'none';
|
|
80
|
+
successDiv.style.display = 'none';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const setLoading = (loading: boolean) => {
|
|
84
|
+
submitBtn.disabled = loading;
|
|
85
|
+
submitBtn.textContent = loading ? 'Updating...' : 'Update Password';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
form.addEventListener('submit', async (e) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
hideMessages();
|
|
91
|
+
|
|
92
|
+
const formData = new FormData(form as HTMLFormElement);
|
|
93
|
+
const currentPassword = formData.get('currentPassword') as string;
|
|
94
|
+
const newPassword = formData.get('newPassword') as string;
|
|
95
|
+
const confirmPassword = formData.get('confirmPassword') as string;
|
|
96
|
+
|
|
97
|
+
if (newPassword !== confirmPassword) {
|
|
98
|
+
showError("New passwords don't match");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setLoading(true);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const res = await client.changePassword({
|
|
106
|
+
currentPassword,
|
|
107
|
+
newPassword
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (res?.error) {
|
|
111
|
+
throw new Error(res.error.message);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Clear form on success
|
|
115
|
+
(form as HTMLFormElement).reset();
|
|
116
|
+
showSuccess();
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
if (err?.code === 'CREDENTIAL_ACCOUNT_NOT_FOUND' || err?.message?.includes('Credential account not found')) {
|
|
119
|
+
showError("You are logged in via a social provider (e.g. GitHub, Google) and do not have a password set.");
|
|
120
|
+
} else {
|
|
121
|
+
showError(err.message || 'Failed to change password');
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
setLoading(false);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
</script>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface Props {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
defaultName?: string;
|
|
5
|
+
defaultImage?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
baseUrl,
|
|
11
|
+
defaultName = '',
|
|
12
|
+
defaultImage = '',
|
|
13
|
+
className = ''
|
|
14
|
+
} = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<form class={`ca-form ${className}`} data-profile-editor data-base-url={baseUrl}>
|
|
18
|
+
<div class="ca-input-group">
|
|
19
|
+
<label class="ca-label" for="profile-name">Name</label>
|
|
20
|
+
<input
|
|
21
|
+
id="profile-name"
|
|
22
|
+
type="text"
|
|
23
|
+
class="ca-input"
|
|
24
|
+
name="name"
|
|
25
|
+
placeholder="Your Name"
|
|
26
|
+
value={defaultName}
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="ca-input-group">
|
|
31
|
+
<label class="ca-label" for="profile-image">Avatar URL</label>
|
|
32
|
+
<div style="display: flex; gap: 10px; align-items: center;">
|
|
33
|
+
<input
|
|
34
|
+
id="profile-image"
|
|
35
|
+
type="url"
|
|
36
|
+
class="ca-input"
|
|
37
|
+
name="image"
|
|
38
|
+
placeholder="https://example.com/avatar.jpg"
|
|
39
|
+
value={defaultImage}
|
|
40
|
+
style="flex: 1;"
|
|
41
|
+
/>
|
|
42
|
+
<div
|
|
43
|
+
data-avatar-preview
|
|
44
|
+
style={`width: 40px; height: 40px; border-radius: 50%; overflow: hidden; flex-shrink: 0; border: 1px solid #eee; display: ${defaultImage ? 'block' : 'none'};`}
|
|
45
|
+
>
|
|
46
|
+
<img
|
|
47
|
+
src={defaultImage}
|
|
48
|
+
alt="Preview"
|
|
49
|
+
style="width: 100%; height: 100%; object-fit: cover;"
|
|
50
|
+
data-avatar-img
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="ca-error" data-error-message style="display: none;"></div>
|
|
57
|
+
<div class="ca-success-message" data-success-message style="display: none; padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
|
|
58
|
+
Profile updated successfully!
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<button type="submit" class="ca-button" data-submit-button>
|
|
62
|
+
Save Profile
|
|
63
|
+
</button>
|
|
64
|
+
</form>
|
|
65
|
+
|
|
66
|
+
<script>
|
|
67
|
+
import { createClient } from '../../clients/astro-client';
|
|
68
|
+
|
|
69
|
+
document.querySelectorAll('[data-profile-editor]').forEach((form) => {
|
|
70
|
+
const baseUrl = form.getAttribute('data-base-url') || undefined;
|
|
71
|
+
const client = createClient(baseUrl);
|
|
72
|
+
|
|
73
|
+
const submitBtn = form.querySelector('[data-submit-button]') as HTMLButtonElement;
|
|
74
|
+
const errorDiv = form.querySelector('[data-error-message]') as HTMLDivElement;
|
|
75
|
+
const successDiv = form.querySelector('[data-success-message]') as HTMLDivElement;
|
|
76
|
+
const imageInput = form.querySelector('#profile-image') as HTMLInputElement;
|
|
77
|
+
const avatarPreview = form.querySelector('[data-avatar-preview]') as HTMLDivElement;
|
|
78
|
+
const avatarImg = form.querySelector('[data-avatar-img]') as HTMLImageElement;
|
|
79
|
+
|
|
80
|
+
// Update avatar preview on input change
|
|
81
|
+
imageInput?.addEventListener('input', () => {
|
|
82
|
+
const url = imageInput.value;
|
|
83
|
+
if (url) {
|
|
84
|
+
avatarImg.src = url;
|
|
85
|
+
avatarPreview.style.display = 'block';
|
|
86
|
+
avatarImg.onerror = () => { avatarPreview.style.display = 'none'; };
|
|
87
|
+
} else {
|
|
88
|
+
avatarPreview.style.display = 'none';
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const showError = (message: string) => {
|
|
93
|
+
errorDiv.textContent = message;
|
|
94
|
+
errorDiv.style.display = 'block';
|
|
95
|
+
successDiv.style.display = 'none';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const showSuccess = () => {
|
|
99
|
+
successDiv.style.display = 'block';
|
|
100
|
+
errorDiv.style.display = 'none';
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const hideMessages = () => {
|
|
104
|
+
errorDiv.style.display = 'none';
|
|
105
|
+
successDiv.style.display = 'none';
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const setLoading = (loading: boolean) => {
|
|
109
|
+
submitBtn.disabled = loading;
|
|
110
|
+
submitBtn.textContent = loading ? 'Saving...' : 'Save Profile';
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
form.addEventListener('submit', async (e) => {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
hideMessages();
|
|
116
|
+
|
|
117
|
+
const formData = new FormData(form as HTMLFormElement);
|
|
118
|
+
const name = formData.get('name') as string;
|
|
119
|
+
const image = formData.get('image') as string;
|
|
120
|
+
|
|
121
|
+
setLoading(true);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await client.updateUser({ name, image });
|
|
125
|
+
showSuccess();
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
showError(err.message || 'Failed to update profile');
|
|
128
|
+
} finally {
|
|
129
|
+
setLoading(false);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
</script>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface Props {
|
|
3
|
+
token?: string | null;
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
backToLoginUrl?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
width?: 'default' | 'compact' | 'wide';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
token,
|
|
12
|
+
baseUrl,
|
|
13
|
+
backToLoginUrl,
|
|
14
|
+
title = 'Set New Password',
|
|
15
|
+
width = 'default'
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
|
|
18
|
+
const widthClass = width === 'compact' ? 'ca-width-compact' : width === 'wide' ? 'ca-width-wide' : 'ca-width-default';
|
|
19
|
+
const hasToken = !!token;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class={`ca-container ${widthClass}`} data-reset-password-form data-base-url={baseUrl} data-token={token || ''}>
|
|
23
|
+
<h2 class="ca-title">{title}</h2>
|
|
24
|
+
|
|
25
|
+
{!hasToken ? (
|
|
26
|
+
<!-- Missing/Invalid Token State -->
|
|
27
|
+
<div class="ca-error-message">
|
|
28
|
+
<svg class="ca-error-icon" viewBox="0 0 24 24" width="48" height="48">
|
|
29
|
+
<circle cx="12" cy="12" r="10" fill="#EF4444" />
|
|
30
|
+
<path d="M12 8v4M12 16h.01" stroke="white" stroke-width="2" stroke-linecap="round" />
|
|
31
|
+
</svg>
|
|
32
|
+
<h3 class="ca-error-title">Invalid or Missing Token</h3>
|
|
33
|
+
<p class="ca-error-text">
|
|
34
|
+
The password reset link is invalid or has expired.
|
|
35
|
+
Please request a new password reset.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
) : (
|
|
39
|
+
<>
|
|
40
|
+
<p class="ca-subtitle">Enter your new password below.</p>
|
|
41
|
+
|
|
42
|
+
<form class="ca-form" data-form-element>
|
|
43
|
+
<div class="ca-input-group">
|
|
44
|
+
<label class="ca-label" for="password">New Password</label>
|
|
45
|
+
<input
|
|
46
|
+
id="password"
|
|
47
|
+
type="password"
|
|
48
|
+
class="ca-input"
|
|
49
|
+
name="password"
|
|
50
|
+
placeholder="At least 8 characters"
|
|
51
|
+
minlength={8}
|
|
52
|
+
required
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="ca-input-group">
|
|
57
|
+
<label class="ca-label" for="confirmPassword">Confirm Password</label>
|
|
58
|
+
<input
|
|
59
|
+
id="confirmPassword"
|
|
60
|
+
type="password"
|
|
61
|
+
class="ca-input"
|
|
62
|
+
name="confirmPassword"
|
|
63
|
+
placeholder="Confirm your password"
|
|
64
|
+
required
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="ca-error" data-error-message style="display: none;"></div>
|
|
69
|
+
|
|
70
|
+
<button type="submit" class="ca-button" data-submit-button>
|
|
71
|
+
Reset Password
|
|
72
|
+
</button>
|
|
73
|
+
</form>
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{backToLoginUrl && (
|
|
78
|
+
<div class="ca-footer" data-footer>
|
|
79
|
+
<a href={backToLoginUrl} class="ca-link">{hasToken ? 'Back to Sign In' : 'Back to Sign In'}</a>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
<!-- Success state (hidden initially) -->
|
|
84
|
+
<div data-success-state style="display: none;">
|
|
85
|
+
<div class="ca-success-message">
|
|
86
|
+
<svg class="ca-success-icon" viewBox="0 0 24 24" width="48" height="48">
|
|
87
|
+
<circle cx="12" cy="12" r="10" fill="#10B981" />
|
|
88
|
+
<path d="M8 12l2.5 2.5L16 9" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
89
|
+
</svg>
|
|
90
|
+
<h3 class="ca-success-title">Password Reset Successful</h3>
|
|
91
|
+
<p class="ca-success-text">
|
|
92
|
+
Your password has been successfully reset. You can now sign in with your new password.
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
{backToLoginUrl && (
|
|
96
|
+
<div class="ca-footer">
|
|
97
|
+
<a href={backToLoginUrl} class="ca-link">Sign In</a>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<script>
|
|
104
|
+
import { createClient } from '../../clients/astro-client';
|
|
105
|
+
|
|
106
|
+
document.querySelectorAll('[data-reset-password-form]').forEach((container) => {
|
|
107
|
+
const baseUrl = container.getAttribute('data-base-url') || undefined;
|
|
108
|
+
const token = container.getAttribute('data-token');
|
|
109
|
+
const client = createClient(baseUrl);
|
|
110
|
+
|
|
111
|
+
if (!token) return; // No token, form is not shown
|
|
112
|
+
|
|
113
|
+
const form = container.querySelector('[data-form-element]') as HTMLFormElement;
|
|
114
|
+
const submitBtn = container.querySelector('[data-submit-button]') as HTMLButtonElement;
|
|
115
|
+
const errorDiv = container.querySelector('[data-error-message]') as HTMLDivElement;
|
|
116
|
+
const successState = container.querySelector('[data-success-state]') as HTMLDivElement;
|
|
117
|
+
const formElements = container.querySelectorAll('.ca-form, .ca-subtitle');
|
|
118
|
+
const footer = container.querySelector('[data-footer]') as HTMLDivElement;
|
|
119
|
+
|
|
120
|
+
const showError = (message: string) => {
|
|
121
|
+
errorDiv.textContent = message;
|
|
122
|
+
errorDiv.style.display = 'block';
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const hideError = () => {
|
|
126
|
+
errorDiv.style.display = 'none';
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const setLoading = (loading: boolean) => {
|
|
130
|
+
submitBtn.disabled = loading;
|
|
131
|
+
submitBtn.textContent = loading ? 'Resetting...' : 'Reset Password';
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
form?.addEventListener('submit', async (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
hideError();
|
|
137
|
+
|
|
138
|
+
const formData = new FormData(form);
|
|
139
|
+
const password = formData.get('password') as string;
|
|
140
|
+
const confirmPassword = formData.get('confirmPassword') as string;
|
|
141
|
+
|
|
142
|
+
if (password !== confirmPassword) {
|
|
143
|
+
showError('Passwords do not match');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (password.length < 8) {
|
|
148
|
+
showError('Password must be at least 8 characters');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setLoading(true);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const { error } = await client.resetPassword({
|
|
156
|
+
token,
|
|
157
|
+
newPassword: password
|
|
158
|
+
});
|
|
159
|
+
if (error) throw error;
|
|
160
|
+
|
|
161
|
+
// Show success state
|
|
162
|
+
formElements.forEach(el => (el as HTMLElement).style.display = 'none');
|
|
163
|
+
if (footer) footer.style.display = 'none';
|
|
164
|
+
successState.style.display = 'block';
|
|
165
|
+
} catch (err: any) {
|
|
166
|
+
showError(err.message || 'Failed to reset password');
|
|
167
|
+
} finally {
|
|
168
|
+
setLoading(false);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
</script>
|