@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.
- package/LICENSE +60 -0
- package/README.md +199 -0
- package/app/app.config.ts +66 -0
- package/app/assets/css/main.css +118 -0
- package/app/components/XAuth/ForgotPassword.vue +91 -0
- package/app/components/XAuth/Handler.vue +186 -0
- package/app/components/XAuth/Login.vue +121 -0
- package/app/components/XAuth/MagicLink.vue +111 -0
- package/app/components/XAuth/OAuthButton.vue +130 -0
- package/app/components/XAuth/OAuthButtonGroup.vue +58 -0
- package/app/components/XAuth/Signup.vue +68 -0
- package/app/composables/types.ts +69 -0
- package/app/composables/useXAuth.ts +317 -0
- package/app/layouts/auth.vue +223 -0
- package/app/middleware/auth.global.ts +45 -0
- package/app/pages/auth/forgot-password.vue +9 -0
- package/app/pages/auth/handler/[...slug].vue +33 -0
- package/app/pages/auth/login.vue +9 -0
- package/app/pages/auth/logout.vue +18 -0
- package/app/pages/auth/magic-link.vue +9 -0
- package/app/pages/auth/signup.vue +9 -0
- package/app/plugins/auth-token.ts +9 -0
- package/app/utils/cookieStorage.ts +64 -0
- package/app/utils/fieldMapper.ts +102 -0
- package/nuxt.config.ts +25 -0
- package/package.json +62 -0
|
@@ -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>
|