@contentgrowth/content-auth 0.4.9 → 0.5.0

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.
Files changed (28) hide show
  1. package/README.md +68 -2
  2. package/dist/{chunk-CTASTCWI.js → chunk-3HNFZJ7S.js} +6 -6
  3. package/dist/chunk-CBNSTIX6.js +17 -0
  4. package/dist/chunk-F2G7XJIZ.js +17 -0
  5. package/dist/frontend/astro.d.ts +6 -0
  6. package/dist/frontend/astro.js +9 -0
  7. package/dist/frontend/client.d.ts +54 -18
  8. package/dist/frontend/clients/astro-client.d.ts +3134 -0
  9. package/dist/frontend/clients/astro-client.js +9 -0
  10. package/dist/frontend/clients/vue-client.d.ts +3520 -0
  11. package/dist/frontend/clients/vue-client.js +9 -0
  12. package/dist/frontend/components/astro/astro/AuthForm.astro +389 -0
  13. package/dist/frontend/components/astro/astro/ForgotPasswordForm.astro +127 -0
  14. package/dist/frontend/components/astro/astro/Organization.astro +254 -0
  15. package/dist/frontend/components/astro/astro/PasswordChanger.astro +128 -0
  16. package/dist/frontend/components/astro/astro/ProfileEditor.astro +133 -0
  17. package/dist/frontend/components/astro/astro/ResetPasswordForm.astro +172 -0
  18. package/dist/frontend/components/vue/vue/AuthForm.vue +337 -0
  19. package/dist/frontend/components/vue/vue/ForgotPasswordForm.vue +108 -0
  20. package/dist/frontend/components/vue/vue/Organization.vue +215 -0
  21. package/dist/frontend/components/vue/vue/PasswordChanger.vue +115 -0
  22. package/dist/frontend/components/vue/vue/ProfileEditor.vue +112 -0
  23. package/dist/frontend/components/vue/vue/ResetPasswordForm.vue +150 -0
  24. package/dist/frontend/index.js +1 -1
  25. package/dist/frontend/vue.d.ts +12 -0
  26. package/dist/frontend/vue.js +23 -0
  27. package/dist/index.js +1 -1
  28. package/package.json +32 -6
@@ -0,0 +1,337 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, watch } from 'vue';
3
+ import { createClient } from '../../clients/vue-client';
4
+
5
+ interface Props {
6
+ view?: 'signin' | 'signup';
7
+ baseUrl?: string;
8
+ socialProviders?: string[];
9
+ socialLayout?: 'row' | 'column';
10
+ title?: string;
11
+ width?: 'default' | 'compact' | 'wide';
12
+ layout?: 'default' | 'split';
13
+ socialPosition?: 'top' | 'bottom';
14
+ defaultEmail?: string;
15
+ lockEmail?: boolean;
16
+ forgotPasswordUrl?: string;
17
+ turnstileSiteKey?: string;
18
+ redirectUrl?: string;
19
+ }
20
+
21
+ const props = withDefaults(defineProps<Props>(), {
22
+ view: 'signin',
23
+ socialProviders: () => [],
24
+ socialLayout: 'row',
25
+ width: 'default',
26
+ layout: 'default',
27
+ socialPosition: 'top',
28
+ defaultEmail: '',
29
+ lockEmail: false,
30
+ });
31
+
32
+ const emit = defineEmits<{
33
+ (e: 'success', data?: any): void;
34
+ (e: 'error', error: string): void;
35
+ (e: 'switchMode'): void;
36
+ }>();
37
+
38
+ const client = computed(() => createClient(props.baseUrl));
39
+
40
+ const isLogin = ref(props.view !== 'signup');
41
+ const email = ref(props.defaultEmail);
42
+ const password = ref('');
43
+ const name = ref('');
44
+ const loading = ref(false);
45
+ const error = ref<string | null>(null);
46
+ const turnstileToken = ref<string | null>(null);
47
+ const mounted = ref(false);
48
+
49
+ const widthClass = computed(() => {
50
+ if (props.width === 'compact') return 'ca-width-compact';
51
+ if (props.width === 'wide') return 'ca-width-wide';
52
+ return 'ca-width-default';
53
+ });
54
+
55
+ const containerClass = computed(() =>
56
+ `ca-container ${props.layout === 'split' ? 'ca-layout-split' : ''} ${widthClass.value}`
57
+ );
58
+
59
+ const socialClass = computed(() =>
60
+ props.socialLayout === 'column' ? 'ca-social-column' : 'ca-social-grid'
61
+ );
62
+
63
+ const displayTitle = computed(() =>
64
+ props.title || (isLogin.value ? 'Welcome Back' : 'Create Account')
65
+ );
66
+
67
+ const turnstileEnabled = computed(() => !!props.turnstileSiteKey);
68
+ const turnstileRequired = computed(() => turnstileEnabled.value && !isLogin.value);
69
+ const canSubmit = computed(() => !turnstileRequired.value || !!turnstileToken.value);
70
+
71
+ watch(() => props.view, (newView) => {
72
+ isLogin.value = newView !== 'signup';
73
+ });
74
+
75
+ onMounted(() => {
76
+ mounted.value = true;
77
+ });
78
+
79
+ const handleSubmit = async () => {
80
+ if (turnstileRequired.value && !turnstileToken.value) {
81
+ error.value = 'Please complete the security challenge';
82
+ return;
83
+ }
84
+
85
+ loading.value = true;
86
+ error.value = null;
87
+
88
+ try {
89
+ let response: any;
90
+ if (isLogin.value) {
91
+ response = await client.value.signIn.email({
92
+ email: email.value,
93
+ password: password.value,
94
+ });
95
+ if (response.error) throw response.error;
96
+ } else {
97
+ const signupData: any = {
98
+ email: email.value,
99
+ password: password.value,
100
+ name: name.value,
101
+ };
102
+ if (turnstileToken.value) {
103
+ signupData.turnstileToken = turnstileToken.value;
104
+ }
105
+ response = await client.value.signUp.email(signupData);
106
+ if (response.error) throw response.error;
107
+ }
108
+
109
+ emit('success', response.data);
110
+
111
+ if (props.redirectUrl) {
112
+ window.location.href = props.redirectUrl;
113
+ }
114
+ } catch (err: any) {
115
+ const errorMessage = err.message || '';
116
+ let friendlyMessage = 'An error occurred. Please try again.';
117
+
118
+ if (errorMessage.includes('TURNSTILE') || errorMessage.includes('security challenge')) {
119
+ friendlyMessage = 'Security verification failed. Please complete the challenge and try again.';
120
+ } else if (errorMessage.includes('EMAIL_EXISTS') || errorMessage.includes('already exists')) {
121
+ friendlyMessage = 'An account with this email already exists. Try signing in instead.';
122
+ } else if (errorMessage.includes('Invalid email') || errorMessage.includes('invalid email')) {
123
+ friendlyMessage = 'Please enter a valid email address.';
124
+ } else if (errorMessage.includes('Invalid password') || errorMessage.includes('incorrect')) {
125
+ friendlyMessage = 'Invalid email or password. Please check your credentials.';
126
+ } else if (errorMessage.includes('User not found')) {
127
+ friendlyMessage = 'No account found with this email. Try signing up instead.';
128
+ } else if (errorMessage.includes('too short') || errorMessage.includes('password')) {
129
+ friendlyMessage = 'Password must be at least 8 characters long.';
130
+ } else if (err.message) {
131
+ friendlyMessage = err.message;
132
+ }
133
+
134
+ error.value = friendlyMessage;
135
+ emit('error', friendlyMessage);
136
+ turnstileToken.value = null;
137
+ } finally {
138
+ loading.value = false;
139
+ }
140
+ };
141
+
142
+ const handleSocialLogin = async (provider: string) => {
143
+ loading.value = true;
144
+ try {
145
+ await client.value.signIn.social({
146
+ provider: provider as any,
147
+ callbackURL: props.redirectUrl || window.location.href
148
+ });
149
+ } catch (err: any) {
150
+ error.value = err.message || `Failed to sign in with ${provider}`;
151
+ emit('error', error.value);
152
+ loading.value = false;
153
+ }
154
+ };
155
+
156
+ const switchMode = () => {
157
+ isLogin.value = !isLogin.value;
158
+ error.value = null;
159
+ turnstileToken.value = null;
160
+ emit('switchMode');
161
+ };
162
+
163
+ const handleTurnstileSuccess = (token: string) => {
164
+ turnstileToken.value = token;
165
+ };
166
+
167
+ const handleTurnstileError = () => {
168
+ turnstileToken.value = null;
169
+ error.value = 'Security verification failed. Please try again.';
170
+ };
171
+ </script>
172
+
173
+ <template>
174
+ <div :class="containerClass">
175
+ <h2 class="ca-title">{{ displayTitle }}</h2>
176
+
177
+ <!-- Default Layout -->
178
+ <template v-if="layout !== 'split'">
179
+ <!-- Social buttons at top -->
180
+ <template v-if="socialPosition === 'top' && socialProviders.length > 0 && !lockEmail">
181
+ <div :class="socialClass">
182
+ <button
183
+ v-for="provider in socialProviders"
184
+ :key="provider"
185
+ type="button"
186
+ :class="`ca-button ca-button-social ca-button-${provider}`"
187
+ :disabled="loading"
188
+ @click="handleSocialLogin(provider)"
189
+ >
190
+ <svg v-if="provider === 'google'" 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>
191
+ <svg v-else-if="provider === 'github'" 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>
192
+ <span class="ca-btn-text">{{ provider === 'github' ? 'GitHub' : 'Google' }}</span>
193
+ </button>
194
+ </div>
195
+ <div class="ca-divider">
196
+ <span class="ca-divider-text">Or continue with</span>
197
+ </div>
198
+ </template>
199
+
200
+ <!-- Form -->
201
+ <form class="ca-form" @submit.prevent="handleSubmit">
202
+ <div v-if="!isLogin" class="ca-input-group">
203
+ <label class="ca-label" for="name">Name</label>
204
+ <input
205
+ id="name"
206
+ v-model="name"
207
+ type="text"
208
+ class="ca-input"
209
+ required
210
+ />
211
+ </div>
212
+
213
+ <div class="ca-input-group">
214
+ <label class="ca-label" for="email">Email</label>
215
+ <input
216
+ id="email"
217
+ v-model="email"
218
+ type="email"
219
+ :class="['ca-input', { 'ca-input-locked': lockEmail }]"
220
+ :readonly="lockEmail"
221
+ required
222
+ />
223
+ </div>
224
+
225
+ <div class="ca-input-group">
226
+ <div class="ca-label-row">
227
+ <label class="ca-label" for="password">Password</label>
228
+ <a v-if="isLogin && forgotPasswordUrl" :href="forgotPasswordUrl" class="ca-forgot-link">
229
+ Forgot password?
230
+ </a>
231
+ </div>
232
+ <input
233
+ id="password"
234
+ v-model="password"
235
+ type="password"
236
+ class="ca-input"
237
+ required
238
+ />
239
+ </div>
240
+
241
+ <!-- Turnstile placeholder - implement with vue-turnstile if needed -->
242
+ <div v-if="turnstileSiteKey && !isLogin" class="ca-turnstile">
243
+ <p class="ca-turnstile-hint">Please complete the security check above</p>
244
+ </div>
245
+
246
+ <div v-if="error" class="ca-error">{{ error }}</div>
247
+
248
+ <button
249
+ type="submit"
250
+ class="ca-button"
251
+ :disabled="loading || !canSubmit"
252
+ >
253
+ {{ loading ? 'Loading...' : (isLogin ? 'Sign In' : 'Sign Up') }}
254
+ </button>
255
+ </form>
256
+
257
+ <!-- Social buttons at bottom -->
258
+ <template v-if="socialPosition === 'bottom' && socialProviders.length > 0 && !lockEmail">
259
+ <div class="ca-divider">
260
+ <span class="ca-divider-text">Or continue with</span>
261
+ </div>
262
+ <div :class="socialClass">
263
+ <button
264
+ v-for="provider in socialProviders"
265
+ :key="provider"
266
+ type="button"
267
+ :class="`ca-button ca-button-social ca-button-${provider}`"
268
+ :disabled="loading"
269
+ @click="handleSocialLogin(provider)"
270
+ >
271
+ <svg v-if="provider === 'google'" 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>
272
+ <svg v-else-if="provider === 'github'" 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>
273
+ <span class="ca-btn-text">{{ provider === 'github' ? 'GitHub' : 'Google' }}</span>
274
+ </button>
275
+ </div>
276
+ </template>
277
+
278
+ <!-- Footer -->
279
+ <div class="ca-footer">
280
+ {{ isLogin ? "Don't have an account? " : "Already have an account? " }}
281
+ <button class="ca-link" type="button" @click="switchMode">
282
+ {{ isLogin ? 'Sign up' : 'Sign in' }}
283
+ </button>
284
+ </div>
285
+ </template>
286
+
287
+ <!-- Split Layout -->
288
+ <template v-else>
289
+ <div class="ca-split-body">
290
+ <div class="ca-split-main">
291
+ <form class="ca-form" @submit.prevent="handleSubmit">
292
+ <div v-if="!isLogin" class="ca-input-group">
293
+ <label class="ca-label" for="name">Name</label>
294
+ <input id="name" v-model="name" type="text" class="ca-input" required />
295
+ </div>
296
+ <div class="ca-input-group">
297
+ <label class="ca-label" for="email">Email</label>
298
+ <input id="email" v-model="email" type="email" :class="['ca-input', { 'ca-input-locked': lockEmail }]" :readonly="lockEmail" required />
299
+ </div>
300
+ <div class="ca-input-group">
301
+ <div class="ca-label-row">
302
+ <label class="ca-label" for="password">Password</label>
303
+ <a v-if="isLogin && forgotPasswordUrl" :href="forgotPasswordUrl" class="ca-forgot-link">Forgot password?</a>
304
+ </div>
305
+ <input id="password" v-model="password" type="password" class="ca-input" required />
306
+ </div>
307
+ <div v-if="error" class="ca-error">{{ error }}</div>
308
+ <button type="submit" class="ca-button" :disabled="loading || !canSubmit">
309
+ {{ loading ? 'Loading...' : (isLogin ? 'Sign In' : 'Sign Up') }}
310
+ </button>
311
+ </form>
312
+ <div class="ca-footer">
313
+ {{ isLogin ? "Don't have an account? " : "Already have an account? " }}
314
+ <button class="ca-link" type="button" @click="switchMode">
315
+ {{ isLogin ? 'Sign up' : 'Sign in' }}
316
+ </button>
317
+ </div>
318
+ </div>
319
+ <template v-if="socialProviders.length > 0 && !lockEmail">
320
+ <div class="ca-split-divider">
321
+ <span class="ca-split-divider-text">Or</span>
322
+ </div>
323
+ <div class="ca-split-social">
324
+ <h3 class="ca-social-header">{{ isLogin ? 'Sign In With' : 'Sign Up With' }}</h3>
325
+ <div :class="socialClass">
326
+ <button v-for="provider in socialProviders" :key="provider" type="button" :class="`ca-button ca-button-social ca-button-${provider}`" :disabled="loading" @click="handleSocialLogin(provider)">
327
+ <svg v-if="provider === 'google'" class="ca-icon" viewBox="0 0 24 24" width="20" height="20"><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>
328
+ <svg v-else-if="provider === 'github'" class="ca-icon" viewBox="0 0 24 24" width="20" height="20"><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>
329
+ <span class="ca-btn-text">{{ provider === 'github' ? 'GitHub' : 'Google' }}</span>
330
+ </button>
331
+ </div>
332
+ </div>
333
+ </template>
334
+ </div>
335
+ </template>
336
+ </div>
337
+ </template>
@@ -0,0 +1,108 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+ import { createClient } from '../../clients/vue-client';
4
+
5
+ interface Props {
6
+ baseUrl?: string;
7
+ backToLoginUrl?: string;
8
+ title?: string;
9
+ width?: 'default' | 'compact' | 'wide';
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), {
13
+ title: 'Reset Password',
14
+ width: 'default',
15
+ });
16
+
17
+ const emit = defineEmits<{
18
+ (e: 'success'): void;
19
+ (e: 'error', error: string): void;
20
+ }>();
21
+
22
+ const client = computed(() => createClient(props.baseUrl));
23
+
24
+ const email = ref('');
25
+ const loading = ref(false);
26
+ const error = ref<string | null>(null);
27
+ const success = ref(false);
28
+
29
+ const widthClass = computed(() => {
30
+ if (props.width === 'compact') return 'ca-width-compact';
31
+ if (props.width === 'wide') return 'ca-width-wide';
32
+ return 'ca-width-default';
33
+ });
34
+
35
+ const handleSubmit = async () => {
36
+ loading.value = true;
37
+ error.value = null;
38
+
39
+ try {
40
+ const { error: err } = await client.value.requestPasswordReset({
41
+ email: email.value,
42
+ redirectTo: window.location.origin + '/auth/reset-password'
43
+ });
44
+ if (err) throw err;
45
+ success.value = true;
46
+ emit('success');
47
+ } catch (err: any) {
48
+ error.value = err.message || 'Failed to send reset email';
49
+ emit('error', error.value);
50
+ } finally {
51
+ loading.value = false;
52
+ }
53
+ };
54
+ </script>
55
+
56
+ <template>
57
+ <div :class="`ca-container ${widthClass}`">
58
+ <h2 class="ca-title">{{ title }}</h2>
59
+
60
+ <!-- Success State -->
61
+ <template v-if="success">
62
+ <div class="ca-success-message">
63
+ <svg class="ca-success-icon" viewBox="0 0 24 24" width="48" height="48">
64
+ <circle cx="12" cy="12" r="10" fill="#10B981" />
65
+ <path d="M8 12l2.5 2.5L16 9" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
66
+ </svg>
67
+ <h3 class="ca-success-title">Check your email</h3>
68
+ <p class="ca-success-text">
69
+ We've sent a password reset link to <strong>{{ email }}</strong>.
70
+ Please check your inbox and click the link to reset your password.
71
+ </p>
72
+ </div>
73
+ <div v-if="backToLoginUrl" class="ca-footer">
74
+ <a :href="backToLoginUrl" class="ca-link">Back to Sign In</a>
75
+ </div>
76
+ </template>
77
+
78
+ <!-- Form State -->
79
+ <template v-else>
80
+ <p class="ca-subtitle">
81
+ Enter your email address and we'll send you a link to reset your password.
82
+ </p>
83
+ <form class="ca-form" @submit.prevent="handleSubmit">
84
+ <div class="ca-input-group">
85
+ <label class="ca-label" for="email">Email</label>
86
+ <input
87
+ id="email"
88
+ v-model="email"
89
+ type="email"
90
+ class="ca-input"
91
+ placeholder="you@example.com"
92
+ required
93
+ />
94
+ </div>
95
+
96
+ <div v-if="error" class="ca-error">{{ error }}</div>
97
+
98
+ <button type="submit" class="ca-button" :disabled="loading">
99
+ {{ loading ? 'Sending...' : 'Send Reset Link' }}
100
+ </button>
101
+ </form>
102
+
103
+ <div v-if="backToLoginUrl" class="ca-footer">
104
+ <a :href="backToLoginUrl" class="ca-link">Back to Sign In</a>
105
+ </div>
106
+ </template>
107
+ </div>
108
+ </template>
@@ -0,0 +1,215 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, onMounted, watch } from 'vue';
3
+ import { createClient } from '../../clients/vue-client';
4
+
5
+ // CreateOrganizationForm Props
6
+ interface CreateOrgProps {
7
+ component: 'create';
8
+ baseUrl?: string;
9
+ className?: string;
10
+ }
11
+
12
+ // OrganizationSwitcher Props
13
+ interface SwitcherProps {
14
+ component: 'switcher';
15
+ baseUrl?: string;
16
+ currentOrgId?: string;
17
+ className?: string;
18
+ }
19
+
20
+ // InviteMemberForm Props
21
+ interface InviteProps {
22
+ component: 'invite';
23
+ baseUrl?: string;
24
+ className?: string;
25
+ }
26
+
27
+ type Props = CreateOrgProps | SwitcherProps | InviteProps;
28
+
29
+ const props = defineProps<Props>();
30
+
31
+ const emit = defineEmits<{
32
+ (e: 'success', data?: any): void;
33
+ (e: 'error', error: string): void;
34
+ }>();
35
+
36
+ const client = computed(() => createClient(props.baseUrl));
37
+
38
+ // Create Organization Form State
39
+ const orgName = ref('');
40
+ const orgSlug = ref('');
41
+ const createLoading = ref(false);
42
+ const createError = ref<string | null>(null);
43
+ const createSuccess = ref(false);
44
+
45
+ // Organization Switcher State
46
+ const orgs = ref<any[]>([]);
47
+ const switcherLoading = ref(true);
48
+ const selectedOrgId = ref('');
49
+
50
+ // Invite Member Form State
51
+ const inviteEmail = ref('');
52
+ const inviteRole = ref('member');
53
+ const inviteLoading = ref(false);
54
+ const inviteError = ref<string | null>(null);
55
+ const inviteSuccess = ref(false);
56
+
57
+ // Auto-slugify for create form
58
+ watch(orgName, (val) => {
59
+ if (props.component === 'create' && !orgSlug.value) {
60
+ orgSlug.value = val.toLowerCase().replace(/\s+/g, '-');
61
+ }
62
+ });
63
+
64
+ // Load orgs for switcher
65
+ onMounted(async () => {
66
+ if (props.component === 'switcher') {
67
+ try {
68
+ const { data } = await client.value.organization.list({});
69
+ if (data) orgs.value = data;
70
+ if ((props as SwitcherProps).currentOrgId) {
71
+ selectedOrgId.value = (props as SwitcherProps).currentOrgId!;
72
+ }
73
+ } catch (err) {
74
+ console.error('Failed to load organizations', err);
75
+ } finally {
76
+ switcherLoading.value = false;
77
+ }
78
+ }
79
+ });
80
+
81
+ const handleCreateOrg = async () => {
82
+ createLoading.value = true;
83
+ createError.value = null;
84
+ createSuccess.value = false;
85
+
86
+ try {
87
+ const result = await client.value.organization.create({
88
+ name: orgName.value,
89
+ slug: orgSlug.value,
90
+ });
91
+ if (result.error) throw result.error;
92
+ orgName.value = '';
93
+ orgSlug.value = '';
94
+ createSuccess.value = true;
95
+ emit('success', result.data);
96
+ } catch (err: any) {
97
+ createError.value = err.message || 'Failed to create organization';
98
+ emit('error', createError.value);
99
+ } finally {
100
+ createLoading.value = false;
101
+ }
102
+ };
103
+
104
+ const handleSwitchOrg = async () => {
105
+ if (!selectedOrgId.value) return;
106
+ await client.value.organization.setActive({ organizationId: selectedOrgId.value });
107
+ emit('success', selectedOrgId.value);
108
+ };
109
+
110
+ const handleInvite = async () => {
111
+ inviteLoading.value = true;
112
+ inviteError.value = null;
113
+ inviteSuccess.value = false;
114
+
115
+ try {
116
+ const result = await client.value.organization.inviteMember({
117
+ email: inviteEmail.value,
118
+ role: inviteRole.value as any,
119
+ });
120
+ if (result.error) throw result.error;
121
+ inviteEmail.value = '';
122
+ inviteSuccess.value = true;
123
+ emit('success', result.data);
124
+ } catch (err: any) {
125
+ inviteError.value = err.message || 'Invitation failed';
126
+ emit('error', inviteError.value);
127
+ } finally {
128
+ inviteLoading.value = false;
129
+ }
130
+ };
131
+ </script>
132
+
133
+ <template>
134
+ <!-- Create Organization Form -->
135
+ <form v-if="component === 'create'" :class="`ca-form ${(props as CreateOrgProps).className || ''}`" @submit.prevent="handleCreateOrg">
136
+ <h3 class="ca-subtitle">Create Organization</h3>
137
+ <div class="ca-input-group">
138
+ <label class="ca-label">Organization Name</label>
139
+ <input
140
+ v-model="orgName"
141
+ type="text"
142
+ class="ca-input"
143
+ required
144
+ placeholder="Acme Corp"
145
+ />
146
+ </div>
147
+ <div class="ca-input-group">
148
+ <label class="ca-label">Slug</label>
149
+ <input
150
+ v-model="orgSlug"
151
+ type="text"
152
+ class="ca-input"
153
+ required
154
+ placeholder="acme-corp"
155
+ />
156
+ </div>
157
+ <div v-if="createError" class="ca-error">{{ createError }}</div>
158
+ <div v-if="createSuccess" class="ca-success-message" style="padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
159
+ Organization created!
160
+ </div>
161
+ <button type="submit" class="ca-button" :disabled="createLoading">
162
+ {{ createLoading ? 'Creating...' : 'Create Organization' }}
163
+ </button>
164
+ </form>
165
+
166
+ <!-- Organization Switcher -->
167
+ <div v-else-if="component === 'switcher'" :class="`ca-org-switcher ${(props as SwitcherProps).className || ''}`">
168
+ <label class="ca-label">Select Organization</label>
169
+ <select
170
+ v-model="selectedOrgId"
171
+ class="ca-select"
172
+ :disabled="switcherLoading"
173
+ @change="handleSwitchOrg"
174
+ >
175
+ <option v-if="switcherLoading" value="" disabled>Loading...</option>
176
+ <option v-else-if="orgs.length === 0" value="" disabled>No organizations</option>
177
+ <template v-else>
178
+ <option value="" disabled>Select an organization</option>
179
+ <option v-for="org in orgs" :key="org.id" :value="org.id">
180
+ {{ org.name }}
181
+ </option>
182
+ </template>
183
+ </select>
184
+ </div>
185
+
186
+ <!-- Invite Member Form -->
187
+ <form v-else-if="component === 'invite'" :class="`ca-form ${(props as InviteProps).className || ''}`" @submit.prevent="handleInvite">
188
+ <h3 class="ca-subtitle">Invite Member</h3>
189
+ <div class="ca-input-group">
190
+ <label class="ca-label">Email Address</label>
191
+ <input
192
+ v-model="inviteEmail"
193
+ type="email"
194
+ class="ca-input"
195
+ required
196
+ placeholder="colleague@example.com"
197
+ />
198
+ </div>
199
+ <div class="ca-input-group">
200
+ <label class="ca-label">Role</label>
201
+ <select v-model="inviteRole" class="ca-select">
202
+ <option value="member">Member</option>
203
+ <option value="admin">Admin</option>
204
+ <option value="owner">Owner</option>
205
+ </select>
206
+ </div>
207
+ <div v-if="inviteError" class="ca-error">{{ inviteError }}</div>
208
+ <div v-if="inviteSuccess" class="ca-success-message" style="padding: 0.75rem; background: #d1fae5; border-radius: 6px; color: #065f46; text-align: center;">
209
+ Invitation sent!
210
+ </div>
211
+ <button type="submit" class="ca-button" :disabled="inviteLoading">
212
+ {{ inviteLoading ? 'Sending Invite...' : 'Send Invite' }}
213
+ </button>
214
+ </form>
215
+ </template>