@xenterprises/nuxt-x-auth-better 0.1.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,186 @@
1
+ <template>
2
+ <div class="space-y-6">
3
+ <!-- Status States with Transition -->
4
+ <Transition
5
+ enter-active-class="transition-all duration-300 ease-out"
6
+ enter-from-class="opacity-0 scale-95"
7
+ enter-to-class="opacity-100 scale-100"
8
+ leave-active-class="transition-all duration-200 ease-in"
9
+ leave-from-class="opacity-100 scale-100"
10
+ leave-to-class="opacity-0 scale-95"
11
+ mode="out-in"
12
+ >
13
+ <!-- Loading State -->
14
+ <slot v-if="isLoading" name="loading">
15
+ <div key="loading" class="text-center py-8">
16
+ <div class="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-primary/10 ring-1 ring-primary/20">
17
+ <UIcon name="i-lucide-loader-2" class="size-8 text-primary animate-spin" />
18
+ </div>
19
+ <p class="text-muted">
20
+ Processing authentication...
21
+ </p>
22
+ </div>
23
+ </slot>
24
+
25
+ <!-- Error State -->
26
+ <slot v-else-if="error" name="error" :error="error">
27
+ <div key="error" class="text-center py-6">
28
+ <div class="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-error/10 ring-1 ring-error/20">
29
+ <UIcon name="i-lucide-x" class="size-8 text-error" />
30
+ </div>
31
+
32
+ <h3 class="text-lg font-semibold text-highlighted mb-2">
33
+ Authentication error
34
+ </h3>
35
+
36
+ <div class="mb-6 p-4 rounded-xl bg-error/5 ring-1 ring-error/10">
37
+ <p class="text-sm text-muted">
38
+ {{ error }}
39
+ </p>
40
+ </div>
41
+
42
+ <UButton
43
+ label="Return to sign in"
44
+ block
45
+ size="lg"
46
+ @click="goToLogin"
47
+ />
48
+ </div>
49
+ </slot>
50
+
51
+ <!-- Success State -->
52
+ <slot v-else-if="success" name="success">
53
+ <div key="success" class="text-center py-6">
54
+ <div class="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-success/10 ring-1 ring-success/20">
55
+ <UIcon name="i-lucide-check" class="size-8 text-success" />
56
+ </div>
57
+
58
+ <h3 class="text-lg font-semibold text-highlighted mb-2">
59
+ Authentication successful
60
+ </h3>
61
+
62
+ <p class="text-sm text-muted">
63
+ You will be redirected shortly...
64
+ </p>
65
+ </div>
66
+ </slot>
67
+ </Transition>
68
+ </div>
69
+ </template>
70
+
71
+ <script setup>
72
+ const props = defineProps({
73
+ // The type of handler to use
74
+ type: {
75
+ type: String,
76
+ required: true,
77
+ validator: (value) =>
78
+ ["magic-link", "password-reset", "oauth", "email-verification"].includes(value),
79
+ },
80
+ // Where to redirect after successful authentication
81
+ redirectTo: {
82
+ type: String,
83
+ default: undefined,
84
+ },
85
+ // Delay before redirect (in milliseconds)
86
+ redirectDelay: {
87
+ type: Number,
88
+ default: 1500,
89
+ },
90
+ });
91
+
92
+ const config = useAppConfig();
93
+ const router = useRouter();
94
+ const route = useRoute();
95
+ const { handleMagicLinkCallback, resetPassword } = useXAuth();
96
+
97
+ const isLoading = ref(true);
98
+ const error = ref(null);
99
+ const success = ref(false);
100
+
101
+ const goToLogin = () => {
102
+ router.push(config.xAuth?.redirects?.login || '/auth/login');
103
+ };
104
+
105
+ onMounted(async () => {
106
+ try {
107
+ const code = route.query.code;
108
+ const url = window.location.href;
109
+
110
+ // Handle the authentication based on the type
111
+ switch (props.type) {
112
+ case "magic-link":
113
+ if (code) {
114
+ const result = await handleMagicLinkCallback(code);
115
+ if (result?.error) {
116
+ throw new Error(result.error);
117
+ }
118
+ } else {
119
+ throw new Error("Magic link code not found in URL");
120
+ }
121
+ break;
122
+
123
+ case "password-reset":
124
+ // Password reset just displays a form, handled separately
125
+ // This handler is for showing the reset form
126
+ success.value = true;
127
+ isLoading.value = false;
128
+ return; // Don't redirect for password reset
129
+
130
+ case "oauth":
131
+ // OAuth callbacks are typically handled automatically
132
+ // Just check if we're authenticated after the callback
133
+ const result = await handleMagicLinkCallback(url);
134
+ if (result?.error) {
135
+ throw new Error(result.error);
136
+ }
137
+ break;
138
+
139
+ case "email-verification":
140
+ if (code) {
141
+ // Email verification would be handled here
142
+ // Implementation depends on the auth provider
143
+ } else {
144
+ throw new Error("Verification code not found in URL");
145
+ }
146
+ break;
147
+ }
148
+
149
+ // Set success state
150
+ success.value = true;
151
+
152
+ // Determine where to redirect
153
+ let redirectPath = props.redirectTo || config.xAuth?.redirects?.afterLogin || "/";
154
+
155
+ // Check if there's a redirect_to in the query or state parameter
156
+ if (route.query.redirect_to) {
157
+ redirectPath = route.query.redirect_to;
158
+ } else if (route.query.state) {
159
+ try {
160
+ const stateObj = JSON.parse(route.query.state);
161
+ if (stateObj.redirect_to) {
162
+ redirectPath = stateObj.redirect_to;
163
+ }
164
+ } catch {
165
+ // Ignore invalid state parameter
166
+ }
167
+ }
168
+
169
+ // Redirect after delay
170
+ setTimeout(() => {
171
+ router.push(redirectPath);
172
+ }, props.redirectDelay);
173
+ } catch (err) {
174
+ error.value = err.message || `An error occurred during ${props.type} authentication.`;
175
+ } finally {
176
+ isLoading.value = false;
177
+ }
178
+ });
179
+
180
+ // Expose states for parent components
181
+ defineExpose({
182
+ isLoading,
183
+ error,
184
+ success,
185
+ });
186
+ </script>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div class="w-full space-y-6">
3
+ <UAuthForm
4
+ :title="cardLogoUrl ? undefined : title"
5
+ :fields="fields"
6
+ :providers="oauthProviders"
7
+ :separator="showSeparator ? 'or' : undefined"
8
+ :schema="schema"
9
+ :loading="isLoading"
10
+ :submit="{ label: 'Sign in' }"
11
+ :on-submit="handleLogin"
12
+ >
13
+ <template v-if="cardLogoUrl" #header>
14
+ <div class="flex flex-col items-center">
15
+ <img :src="cardLogoUrl" alt="Logo" class="h-12 w-auto mb-4" />
16
+ <h1 class="text-xl text-pretty font-semibold text-highlighted">{{ title }}</h1>
17
+ </div>
18
+ </template>
19
+
20
+ <template #description>
21
+ <span v-if="showSignupLink" class="text-muted">
22
+ Don't have an account?
23
+ <ULink
24
+ :to="signupPath"
25
+ class="text-primary font-medium hover:text-primary/80 transition-colors"
26
+ >
27
+ Sign up
28
+ </ULink>
29
+ </span>
30
+ </template>
31
+
32
+ <template #footer>
33
+ <div class="space-y-3">
34
+ <div v-if="showForgotPassword" class="flex justify-end">
35
+ <ULink
36
+ :to="forgotPasswordPath"
37
+ class="text-primary/80 hover:text-primary text-sm font-medium transition-colors"
38
+ >
39
+ Forgot password?
40
+ </ULink>
41
+ </div>
42
+ <nav
43
+ v-if="showMagicLink"
44
+ class="flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5 text-sm"
45
+ >
46
+ <ULink
47
+ to="/auth/magic-link"
48
+ class="text-muted hover:text-highlighted transition-colors"
49
+ >
50
+ Sign in with Magic Link
51
+ </ULink>
52
+ </nav>
53
+ </div>
54
+ </template>
55
+ </UAuthForm>
56
+ </div>
57
+ </template>
58
+
59
+ <script setup>
60
+ import { z } from "zod"
61
+
62
+ const config = useAppConfig()
63
+ const { login, loginWithProvider, isLoading } = useXAuth()
64
+
65
+ const title = "Welcome back"
66
+
67
+ const showSignupLink = computed(() => config.xAuth?.features?.signup !== false)
68
+ const showForgotPassword = computed(() => config.xAuth?.features?.forgotPassword !== false)
69
+ const showMagicLink = computed(() => config.xAuth?.features?.magicLink || false)
70
+
71
+ const signupPath = computed(() => config.xAuth?.redirects?.signup || '/auth/signup')
72
+ const forgotPasswordPath = computed(() => config.xAuth?.redirects?.forgotPassword || '/auth/forgot-password')
73
+
74
+ const showSeparator = computed(() => config.xAuth?.ui?.form?.showSeparator !== false)
75
+
76
+ const cardLogoUrl = computed(() => config.xAuth?.ui?.card?.logoUrl || '')
77
+
78
+ const oauthProviders = computed(() => {
79
+ if (!config.xAuth?.features?.oauth) return []
80
+
81
+ const providers = config.xAuth?.oauthProviders || []
82
+ return providers.map((p) => ({
83
+ label: p.label,
84
+ icon: p.icon,
85
+ color: "neutral",
86
+ variant: "subtle",
87
+ size: "lg",
88
+ onClick: () => handleOAuth(p.id),
89
+ }))
90
+ })
91
+
92
+ const fields = [
93
+ {
94
+ name: "email",
95
+ type: "email",
96
+ label: "Email",
97
+ placeholder: "you@example.com",
98
+ required: true,
99
+ },
100
+ {
101
+ name: "password",
102
+ type: "password",
103
+ label: "Password",
104
+ placeholder: "••••••••",
105
+ required: true,
106
+ },
107
+ ]
108
+
109
+ const schema = z.object({
110
+ email: z.string().email("Please enter a valid email address"),
111
+ password: z.string().min(1, "Password is required"),
112
+ })
113
+
114
+ async function handleLogin(payload) {
115
+ await login(payload.data.email, payload.data.password)
116
+ }
117
+
118
+ async function handleOAuth(providerId) {
119
+ await loginWithProvider(providerId)
120
+ }
121
+ </script>
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <div>
3
+ <Transition
4
+ enter-active-class="transition-all duration-300 ease-out"
5
+ enter-from-class="opacity-0 translate-y-2"
6
+ enter-to-class="opacity-100 translate-y-0"
7
+ leave-active-class="transition-all duration-200 ease-in"
8
+ leave-from-class="opacity-100 translate-y-0"
9
+ leave-to-class="opacity-0 -translate-y-2"
10
+ mode="out-in"
11
+ >
12
+ <!-- Email Form -->
13
+ <UAuthForm
14
+ v-if="!emailSent"
15
+ key="form"
16
+ title="Sign in with Magic Link"
17
+ description="No password needed"
18
+ :fields="fields"
19
+ :schema="schema"
20
+ :loading="isLoading"
21
+ :submit="{ label: 'Send magic link' }"
22
+ @submit="handleSendMagicLink"
23
+ >
24
+ <template #footer>
25
+ <ULink
26
+ :to="config.xAuth?.redirects?.login || '/auth/login'"
27
+ class="inline-flex items-center gap-1.5 text-sm text-muted hover:text-highlighted transition-colors"
28
+ >
29
+ <UIcon name="i-lucide-arrow-left" class="size-4" />
30
+ Back to sign in
31
+ </ULink>
32
+ </template>
33
+ </UAuthForm>
34
+
35
+ <!-- Success State -->
36
+ <article v-else key="success" class="text-center space-y-6">
37
+ <figure class="mx-auto flex size-16 items-center justify-center rounded-full bg-primary/10 ring-1 ring-primary/20">
38
+ <UIcon name="i-lucide-mail" class="size-8 text-primary" />
39
+ </figure>
40
+
41
+ <header class="space-y-2">
42
+ <h3 class="text-xl font-semibold tracking-tight text-highlighted">
43
+ Check your email
44
+ </h3>
45
+ <p class="text-sm text-muted">
46
+ Click the link in your email to sign in
47
+ </p>
48
+ </header>
49
+
50
+ <aside class="p-5 rounded-xl bg-muted/50 ring-1 ring-default">
51
+ <p class="text-sm text-muted">
52
+ We've sent a magic link to
53
+ </p>
54
+ <p class="mt-1 font-semibold text-highlighted">
55
+ {{ sentEmail }}
56
+ </p>
57
+ </aside>
58
+
59
+ <p class="text-xs text-muted">
60
+ The link will expire in 15 minutes.
61
+ </p>
62
+
63
+ <UButton
64
+ variant="outline"
65
+ color="neutral"
66
+ label="Use a different email"
67
+ block
68
+ size="lg"
69
+ @click="resetEmailSent"
70
+ />
71
+ </article>
72
+ </Transition>
73
+ </div>
74
+ </template>
75
+
76
+ <script setup>
77
+ import { z } from "zod"
78
+
79
+ const config = useAppConfig()
80
+ const { sendMagicLink, isLoading, emailSent, resetState } = useXAuth()
81
+
82
+ const sentEmail = ref("")
83
+
84
+
85
+ // Form fields
86
+ const fields = [
87
+ {
88
+ name: "email",
89
+ type: "email",
90
+ label: "Email",
91
+ placeholder: "you@example.com",
92
+ required: true,
93
+ },
94
+ ]
95
+
96
+ // Validation schema
97
+ const schema = z.object({
98
+ email: z.string().email("Please enter a valid email address"),
99
+ })
100
+
101
+ async function handleSendMagicLink(payload) {
102
+ sentEmail.value = payload.data.email
103
+ const origin = window.location.origin
104
+ const redirectUri = `${origin}/auth/handler/magic-link-callback`
105
+ await sendMagicLink(payload.data.email, { redirectUri })
106
+ }
107
+
108
+ function resetEmailSent() {
109
+ resetState()
110
+ }
111
+ </script>
@@ -0,0 +1,130 @@
1
+ <template>
2
+ <UButton
3
+ block
4
+ variant="outline"
5
+ color="neutral"
6
+ size="xl"
7
+ :loading="isLoading"
8
+ :disabled="isLoading"
9
+ :aria-label="`Sign in with ${providerName}`"
10
+ class="group relative overflow-hidden transition-all duration-200 hover:bg-neutral-50 dark:hover:bg-neutral-800/50"
11
+ @click="handleSignIn"
12
+ >
13
+ <template #leading>
14
+ <!-- Google Icon (colorful) -->
15
+ <svg v-if="props.provider === 'google'" class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
16
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
17
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
18
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
19
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
20
+ </svg>
21
+
22
+ <!-- GitHub Icon -->
23
+ <svg v-else-if="props.provider === 'github'" class="h-5 w-5 text-neutral-900 dark:text-white" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
24
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 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.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.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 0022 12.017C22 6.484 17.522 2 12 2z"/>
25
+ </svg>
26
+
27
+ <!-- Microsoft Icon -->
28
+ <svg v-else-if="props.provider === 'microsoft'" class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
29
+ <path fill="#F25022" d="M1 1h10v10H1z"/>
30
+ <path fill="#00A4EF" d="M1 13h10v10H1z"/>
31
+ <path fill="#7FBA00" d="M13 1h10v10H13z"/>
32
+ <path fill="#FFB900" d="M13 13h10v10H13z"/>
33
+ </svg>
34
+
35
+ <!-- Facebook Icon -->
36
+ <svg v-else-if="props.provider === 'facebook'" class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
37
+ <path fill="#1877F2" d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
38
+ </svg>
39
+
40
+ <!-- Twitter/X Icon -->
41
+ <svg v-else-if="props.provider === 'twitter'" class="h-5 w-5 text-neutral-900 dark:text-white" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
42
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
43
+ </svg>
44
+
45
+ <!-- Apple Icon -->
46
+ <svg v-else-if="props.provider === 'apple'" class="h-5 w-5 text-neutral-900 dark:text-white" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
47
+ <path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/>
48
+ </svg>
49
+
50
+ <!-- LinkedIn Icon -->
51
+ <svg v-else-if="props.provider === 'linkedin'" class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
52
+ <path fill="#0A66C2" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
53
+ </svg>
54
+
55
+ <!-- Discord Icon -->
56
+ <svg v-else-if="props.provider === 'discord'" class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
57
+ <path fill="#5865F2" d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z"/>
58
+ </svg>
59
+
60
+ <!-- Spotify Icon -->
61
+ <svg v-else-if="props.provider === 'spotify'" class="h-5 w-5" viewBox="0 0 24 24" aria-hidden="true">
62
+ <path fill="#1DB954" d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/>
63
+ </svg>
64
+
65
+ <!-- Default fallback icon -->
66
+ <UIcon v-else name="i-lucide-user-circle" class="h-5 w-5" :aria-hidden="true" />
67
+ </template>
68
+
69
+ <span class="font-medium">
70
+ {{ isLoading ? 'Connecting...' : `Continue with ${providerName}` }}
71
+ </span>
72
+ </UButton>
73
+ </template>
74
+
75
+ <script setup>
76
+ const props = defineProps({
77
+ provider: {
78
+ type: String,
79
+ required: true,
80
+ validator: (value) =>
81
+ [
82
+ "google",
83
+ "github",
84
+ "facebook",
85
+ "twitter",
86
+ "microsoft",
87
+ "apple",
88
+ "linkedin",
89
+ "discord",
90
+ "spotify",
91
+ ].includes(value),
92
+ },
93
+ variant: {
94
+ type: String,
95
+ default: "outline",
96
+ },
97
+ color: {
98
+ type: String,
99
+ default: "neutral",
100
+ },
101
+ });
102
+
103
+ const { loginWithProvider } = useXAuth();
104
+ const isLoading = ref(false);
105
+
106
+ const providerName = computed(() => {
107
+ const providers = {
108
+ google: "Google",
109
+ github: "GitHub",
110
+ microsoft: "Microsoft",
111
+ facebook: "Facebook",
112
+ twitter: "X",
113
+ apple: "Apple",
114
+ linkedin: "LinkedIn",
115
+ discord: "Discord",
116
+ spotify: "Spotify",
117
+ };
118
+ return providers[props.provider] || props.provider;
119
+ });
120
+
121
+ const handleSignIn = async () => {
122
+ isLoading.value = true;
123
+ try {
124
+ await loginWithProvider(props.provider);
125
+ // The redirect will be handled by the auth provider
126
+ } catch {
127
+ isLoading.value = false;
128
+ }
129
+ };
130
+ </script>
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <div v-if="shouldShow" class="space-y-3">
3
+ <XAuthOAuthButton
4
+ v-for="provider in enabledProviders"
5
+ :key="provider.id"
6
+ :provider="provider.id"
7
+ />
8
+ </div>
9
+ </template>
10
+
11
+ <script setup>
12
+ /**
13
+ * OAuth Button Group
14
+ *
15
+ * Displays OAuth provider buttons for authentication.
16
+ * This component is for standalone use outside of UAuthForm.
17
+ * When using UAuthForm, pass providers directly to the :providers prop.
18
+ *
19
+ * Config format in app.config.ts:
20
+ * oauthProviders: [
21
+ * { id: 'google', label: 'Google', icon: 'i-simple-icons-google' },
22
+ * { id: 'facebook', label: 'Facebook', icon: 'i-simple-icons-facebook' },
23
+ * ]
24
+ */
25
+
26
+ const props = defineProps({
27
+ providers: {
28
+ type: Array,
29
+ default: undefined,
30
+ },
31
+ });
32
+
33
+ const config = useAppConfig();
34
+
35
+ // Check if OAuth is enabled
36
+ const isEnabled = computed(() => config.xAuth?.features?.oauth || false);
37
+
38
+ // Get enabled providers from props or config
39
+ const enabledProviders = computed(() => {
40
+ if (props.providers && props.providers.length > 0) {
41
+ return props.providers;
42
+ }
43
+
44
+ // Get from config (new format with objects)
45
+ const configProviders = config.xAuth?.oauthProviders || [];
46
+ if (configProviders.length > 0) {
47
+ return configProviders;
48
+ }
49
+
50
+ // Default fallback
51
+ return [{ id: 'google', label: 'Google', icon: 'i-simple-icons-google' }];
52
+ });
53
+
54
+ // Only show if OAuth is enabled and there are providers
55
+ const shouldShow = computed(() => {
56
+ return isEnabled.value && enabledProviders.value.length > 0;
57
+ });
58
+ </script>
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <UAuthForm
3
+ :title="title"
4
+ :fields="fields"
5
+ :schema="schema"
6
+ :loading="isLoading"
7
+ :submit="{ label: 'Create account' }"
8
+ @submit="handleSignup"
9
+ >
10
+ <template #description>
11
+ <span class="text-muted">
12
+ Already have an account?
13
+ <ULink
14
+ :to="loginPath"
15
+ class="text-primary font-medium hover:text-primary/80 transition-colors"
16
+ >
17
+ Sign in
18
+ </ULink>
19
+ </span>
20
+ </template>
21
+
22
+ <template #footer>
23
+ <p class="text-xs text-muted text-center leading-relaxed">
24
+ By creating an account, you agree to our
25
+ <ULink to="/terms" class="text-primary/80 hover:text-primary font-medium transition-colors">Terms of Service</ULink>
26
+ and
27
+ <ULink to="/privacy" class="text-primary/80 hover:text-primary font-medium transition-colors">Privacy Policy</ULink>.
28
+ </p>
29
+ </template>
30
+ </UAuthForm>
31
+ </template>
32
+
33
+ <script setup>
34
+ import { z } from "zod"
35
+
36
+ const config = useAppConfig()
37
+ const { signup, isLoading } = useXAuth()
38
+
39
+ const title = "Create an account"
40
+
41
+ const loginPath = computed(() => config.xAuth?.redirects?.login || '/auth/login')
42
+
43
+ const fields = [
44
+ {
45
+ name: "email",
46
+ type: "email",
47
+ label: "Email",
48
+ placeholder: "you@example.com",
49
+ required: true,
50
+ },
51
+ {
52
+ name: "password",
53
+ type: "password",
54
+ label: "Password",
55
+ placeholder: "At least 8 characters",
56
+ required: true,
57
+ },
58
+ ]
59
+
60
+ const schema = z.object({
61
+ email: z.string().email("Please enter a valid email address"),
62
+ password: z.string().min(8, "Password must be at least 8 characters"),
63
+ })
64
+
65
+ async function handleSignup(payload) {
66
+ await signup(payload.data.email, payload.data.password)
67
+ }
68
+ </script>