@mframework/layer-auth 0.0.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/app/components/blocks/logoutButton.vue +14 -0
- package/app/composables/useAuth.ts +159 -0
- package/app/composables/useUser.ts +65 -0
- package/app/layouts/auth.vue +8 -0
- package/app/middleware/redirect.global.ts +7 -0
- package/app/middleware/seller.ts +7 -0
- package/app/middleware/session.ts +12 -0
- package/app/module.ts +19 -0
- package/app/pages/callback.vue +22 -0
- package/app/pages/confirm.vue +19 -0
- package/app/pages/forgot-password.vue +156 -0
- package/app/pages/i18n.json +111 -0
- package/app/pages/login.vue +138 -0
- package/app/pages/register.vue +177 -0
- package/app/pages/reset-password.vue +124 -0
- package/app/stores/user.ts +43 -0
- package/app/utils/plugins.ts +24 -0
- package/app/utils/providers/better-auth.ts +125 -0
- package/nuxt.config.ts +18 -0
- package/package.json +30 -0
- package/server/api/auth/[...all].ts +7 -0
- package/server/tsconfig.json +3 -0
- package/server/utils/auth.ts +3 -0
- package/server/utils/query.ts +28 -0
- package/server/utils/require-auth.ts +23 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="max-w-md">
|
|
3
|
+
<v-toolbar color="transparent">
|
|
4
|
+
<v-toolbar-title class="text-xs md:text-sm">Sign In</v-toolbar-title>
|
|
5
|
+
<v-toolbar-subtitle>
|
|
6
|
+
Enter your email below to login to your account
|
|
7
|
+
</v-toolbar-subtitle>
|
|
8
|
+
</v-toolbar>
|
|
9
|
+
|
|
10
|
+
<v-card>
|
|
11
|
+
<div class="grid gap-4">
|
|
12
|
+
<div class="grid gap-2">
|
|
13
|
+
<Label for="email">Email</Label>
|
|
14
|
+
<Input id="email" type="email" placeholder="m@example.com" required v-model="email" />
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="grid gap-2">
|
|
18
|
+
<div class="flex items-center">
|
|
19
|
+
<Label for="password">Password</Label>
|
|
20
|
+
<NuxtLink to="#" class="ml-auto inline-block text-sm underline">
|
|
21
|
+
Forgot your password?
|
|
22
|
+
</NuxtLink>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<Input id="password" type="password" placeholder="password" autoComplete="password" v-model="password" />
|
|
26
|
+
</div>
|
|
27
|
+
<div class="flex items-center gap-2">
|
|
28
|
+
<Checkbox id="remember" v-model="rememberMe" />
|
|
29
|
+
<Label for="remember">Remember me</Label>
|
|
30
|
+
</div>
|
|
31
|
+
<v-btn type="submit" class="w-full" :disabled="loading" @click="handleSignIn">
|
|
32
|
+
<Loader2 size="16" class="animate-spin" v-if="loading" />
|
|
33
|
+
<p v-else>Login</p>
|
|
34
|
+
</v-btn>
|
|
35
|
+
<v-btn variant="secondary" :disabled="loading" class="gap-2" @click="handlePasskey">
|
|
36
|
+
<Key size="16" />
|
|
37
|
+
Sign-in with Passkey
|
|
38
|
+
</v-btn>
|
|
39
|
+
<div :class="cn(
|
|
40
|
+
'w-full gap-2 flex items-center',
|
|
41
|
+
'justify-between flex-col'
|
|
42
|
+
)">
|
|
43
|
+
<v-btn variant="outlined" :class="cn(
|
|
44
|
+
'w-full gap-2'
|
|
45
|
+
)" :disabled="loading" @click="handleSocialSignIn('github')">
|
|
46
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
47
|
+
<path fill="currentColor"
|
|
48
|
+
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2">
|
|
49
|
+
</path>
|
|
50
|
+
</svg>
|
|
51
|
+
Sign in with Github
|
|
52
|
+
</v-btn>
|
|
53
|
+
<v-btn variant="outlined" :class="cn(
|
|
54
|
+
'w-full gap-2'
|
|
55
|
+
)" :disabled="loading" @click="handleSocialSignIn('microsoft')">
|
|
56
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
57
|
+
<path fill="#00A4EF" d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"></path>
|
|
58
|
+
</svg>
|
|
59
|
+
Sign in with Microsoft
|
|
60
|
+
</v-btn>
|
|
61
|
+
<v-btn variant="outlined" :class="cn(
|
|
62
|
+
'w-full gap-2'
|
|
63
|
+
)" :disabled="loading" @click="handleSocialSignIn('twitter')">
|
|
64
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 448 512">
|
|
65
|
+
<path fill="currentColor"
|
|
66
|
+
d="M64 32C28.7 32 0 60.7 0 96v320c0 35.3 28.7 64 64 64h320c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64zm297.1 84L257.3 234.6L379.4 396h-95.6L209 298.1L123.3 396H75.8l111-126.9L69.7 116h98l67.7 89.5l78.2-89.5zm-37.8 251.6L153.4 142.9h-28.3l171.8 224.7h26.3z">
|
|
67
|
+
</path>
|
|
68
|
+
</svg>
|
|
69
|
+
Sign in with Twitter
|
|
70
|
+
</v-btn>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</v-card>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<script setup>
|
|
78
|
+
import {
|
|
79
|
+
ref
|
|
80
|
+
} from "vue";
|
|
81
|
+
import { useAuth } from "../composables/useAuth";
|
|
82
|
+
|
|
83
|
+
const { signIn } = useAuth();
|
|
84
|
+
|
|
85
|
+
const email = ref("");
|
|
86
|
+
const password = ref("");
|
|
87
|
+
const loading = ref(false);
|
|
88
|
+
const rememberMe = ref(false);
|
|
89
|
+
const toast = useToast();
|
|
90
|
+
|
|
91
|
+
// simple classnames helper used in the template
|
|
92
|
+
const cn = (...classes) =>
|
|
93
|
+
classes.filter(Boolean).join(" ");
|
|
94
|
+
|
|
95
|
+
const handleSignIn = async () => {
|
|
96
|
+
await signIn.email({
|
|
97
|
+
email: email.value,
|
|
98
|
+
password: password.value,
|
|
99
|
+
rememberMe: rememberMe.value,
|
|
100
|
+
fetchOptions: {
|
|
101
|
+
onRequest: () => {
|
|
102
|
+
loading.value = true;
|
|
103
|
+
},
|
|
104
|
+
onResponse: () => {
|
|
105
|
+
loading.value = false;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handlePasskey = async () => {
|
|
112
|
+
await signIn.passkey({
|
|
113
|
+
fetchOptions: {
|
|
114
|
+
onRequest: () => {
|
|
115
|
+
loading.value = true;
|
|
116
|
+
},
|
|
117
|
+
onResponse: () => {
|
|
118
|
+
loading.value = false;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleSocialSignIn = async (provider) => {
|
|
125
|
+
await signIn.social({
|
|
126
|
+
provider,
|
|
127
|
+
callbackURL: "/",
|
|
128
|
+
fetchOptions: {
|
|
129
|
+
onRequest: () => {
|
|
130
|
+
loading.value = true;
|
|
131
|
+
},
|
|
132
|
+
onResponse: () => {
|
|
133
|
+
loading.value = false;
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
</script>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="z-50 rounded-md rounded-t-none max-w-md">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<v-toolbar-title class="text-lg md:text-xl">Sign Up</v-toolbar-title>
|
|
5
|
+
<v-toolbar-subtitle>
|
|
6
|
+
Enter your information to create an account
|
|
7
|
+
</v-toolbar-subtitle>
|
|
8
|
+
</v-toolbar>
|
|
9
|
+
<v-card>
|
|
10
|
+
<div class="grid gap-4">
|
|
11
|
+
<div class="grid grid-cols-2 gap-4">
|
|
12
|
+
<div class="grid gap-2">
|
|
13
|
+
<v-text-field
|
|
14
|
+
id="first-name"
|
|
15
|
+
placeholder="Max"
|
|
16
|
+
label="First Name"
|
|
17
|
+
required
|
|
18
|
+
v-model="firstName"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="grid gap-2">
|
|
22
|
+
<v-text-field
|
|
23
|
+
id="last-name"
|
|
24
|
+
placeholder="Robinson"
|
|
25
|
+
label="Last Name"
|
|
26
|
+
required
|
|
27
|
+
v-model="lastName"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="grid gap-2">
|
|
31
|
+
<v-text-field
|
|
32
|
+
id="email"
|
|
33
|
+
type="email"
|
|
34
|
+
placeholder="m@example.com"
|
|
35
|
+
label="Email"
|
|
36
|
+
required
|
|
37
|
+
v-model="email"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="grid gap-2">
|
|
41
|
+
<v-text-field
|
|
42
|
+
id="password"
|
|
43
|
+
type="password"
|
|
44
|
+
placeholder="Password"
|
|
45
|
+
autocomplete="new-password"
|
|
46
|
+
label="Password"
|
|
47
|
+
v-model="password"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="grid gap-2">
|
|
51
|
+
<v-text-field
|
|
52
|
+
id="password_confirmation"
|
|
53
|
+
type="password"
|
|
54
|
+
autocomplete="new-password"
|
|
55
|
+
placeholder="Confirm Password"
|
|
56
|
+
label="Confirm Password"
|
|
57
|
+
v-model="passwordConfirmation"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="grid gap-2">
|
|
61
|
+
<v-text-field
|
|
62
|
+
label="Profile Image (optional)"
|
|
63
|
+
/>
|
|
64
|
+
<div class="flex items-end gap-4">
|
|
65
|
+
<div v-if="imagePreview" class="relative w-16 h-16 rounded-sm overflow-hidden">
|
|
66
|
+
<NuxtImg
|
|
67
|
+
:src="imagePreview"
|
|
68
|
+
alt="Profile preview"
|
|
69
|
+
class="object-cover"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="flex items-center gap-2 w-full">
|
|
73
|
+
<v-text-field
|
|
74
|
+
id="image"
|
|
75
|
+
type="file"
|
|
76
|
+
accept="image/*"
|
|
77
|
+
@change="handleImageChange"
|
|
78
|
+
class="w-full"
|
|
79
|
+
/>
|
|
80
|
+
<X
|
|
81
|
+
class="cursor-pointer"
|
|
82
|
+
@click="image = null; imagePreview = null"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<v-btn
|
|
88
|
+
type="submit"
|
|
89
|
+
class="w-full"
|
|
90
|
+
:disabled="loading"
|
|
91
|
+
@click="handleSignUp"
|
|
92
|
+
>
|
|
93
|
+
<Loader2 size="16" class="animate-spin" v-if="loading" />
|
|
94
|
+
<span v-else>Create your account</span>
|
|
95
|
+
</v-btn>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</v-card>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
<script setup>
|
|
104
|
+
import { ref } from 'vue'
|
|
105
|
+
import { definePageMeta, useHead, useRouter } from '#imports'
|
|
106
|
+
import { useAuth } from '../composables/useAuth'
|
|
107
|
+
|
|
108
|
+
const { signUp } = useAuth();
|
|
109
|
+
|
|
110
|
+
definePageMeta({
|
|
111
|
+
layout: false,
|
|
112
|
+
auth: {
|
|
113
|
+
only: 'guest'
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const router = useRouter();
|
|
118
|
+
const toast = useToast();
|
|
119
|
+
|
|
120
|
+
const firstName = ref("");
|
|
121
|
+
const lastName = ref("");
|
|
122
|
+
const email = ref("");
|
|
123
|
+
const password = ref("");
|
|
124
|
+
const passwordConfirmation = ref("");
|
|
125
|
+
const image = ref<File | null>(null);
|
|
126
|
+
const imagePreview = ref<string | null>(null);
|
|
127
|
+
const loading = ref(false);
|
|
128
|
+
|
|
129
|
+
async function convertImageToBase64(file) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const reader = new FileReader();
|
|
132
|
+
reader.onloadend = () => resolve(reader.result);
|
|
133
|
+
reader.onerror = reject;
|
|
134
|
+
reader.readAsDataURL(file);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const handleSignUp = async () => {
|
|
139
|
+
await signUp.email({
|
|
140
|
+
email: email.value,
|
|
141
|
+
password: password.value,
|
|
142
|
+
name: `${firstName.value} ${lastName.value}`,
|
|
143
|
+
image: image.value ? await convertImageToBase64(image.value) : "",
|
|
144
|
+
callbackURL: "/dashboard",
|
|
145
|
+
fetchOptions: {
|
|
146
|
+
onResponse: () => {
|
|
147
|
+
loading.value = false;
|
|
148
|
+
},
|
|
149
|
+
onRequest: () => {
|
|
150
|
+
loading.value = true;
|
|
151
|
+
},
|
|
152
|
+
onError: (ctx) => {
|
|
153
|
+
toast.error(ctx.error.message);
|
|
154
|
+
},
|
|
155
|
+
onSuccess: () => {
|
|
156
|
+
router.push("/dashboard");
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleImageChange = (e) => {
|
|
163
|
+
const file = (e.target)?.files?.[0];
|
|
164
|
+
if (file) {
|
|
165
|
+
image.value = file;
|
|
166
|
+
const reader = new FileReader();
|
|
167
|
+
reader.onloadend = () => {
|
|
168
|
+
imagePreview.value = reader.result;
|
|
169
|
+
};
|
|
170
|
+
reader.readAsDataURL(file);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
useHead({
|
|
175
|
+
title: 'Register'
|
|
176
|
+
});
|
|
177
|
+
</script>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="authPage">
|
|
3
|
+
<section data-bs-version="5.1" class="authForm">
|
|
4
|
+
<NuxtImg loading="lazy" src="~/assets/images/logo512alpha-128x128.png" alt="Meeovi Logo" class="authLogo" />
|
|
5
|
+
<h1 class="mbr-section-title mbr-fonts-style display-1">Reset Password</h1>
|
|
6
|
+
|
|
7
|
+
<div class="reset-password-form">
|
|
8
|
+
<form class="mbr-section-btn" :schema="schema" :state="state" @submit="onSubmit">
|
|
9
|
+
<v-text-field v-model="state.password" type="password" label="New Password" required></v-text-field>
|
|
10
|
+
<v-text-field v-model="state.confirmPassword" type="password" label="Confirm Password" required></v-text-field>
|
|
11
|
+
<div class="mb-3">
|
|
12
|
+
<div ref="turnstileRef"></div>
|
|
13
|
+
</div>
|
|
14
|
+
<v-btn class="mt-2 btn btn-primary display-4" type="submit" block :loading="loading" :disabled="loading || !turnstileToken">
|
|
15
|
+
Reset Password
|
|
16
|
+
</v-btn>
|
|
17
|
+
</form>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<v-alert v-if="message" :type="messageType" class="mt-4" variant="tonal">
|
|
21
|
+
{{ message }}
|
|
22
|
+
</v-alert>
|
|
23
|
+
|
|
24
|
+
<div class="mt-4 text-center">
|
|
25
|
+
<NuxtLink to="/login">Back to Login</NuxtLink>
|
|
26
|
+
</div>
|
|
27
|
+
</section>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script setup>
|
|
32
|
+
import { useHead, useRoute, useRuntimeConfig, navigateTo } from '#app'
|
|
33
|
+
import { definePageMeta, useLocalePath, useI18n } from '#imports'
|
|
34
|
+
import { z } from 'zod'
|
|
35
|
+
import { reactive, ref } from 'vue'
|
|
36
|
+
import { useAuth } from '../composables/useAuth'
|
|
37
|
+
|
|
38
|
+
definePageMeta({
|
|
39
|
+
auth: {
|
|
40
|
+
only: 'guest'
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const { t } = useI18n()
|
|
45
|
+
useHead({
|
|
46
|
+
title: t('resetPassword.title')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const auth = useAuth()
|
|
50
|
+
const toast = useToast()
|
|
51
|
+
const route = useRoute()
|
|
52
|
+
const localePath = useLocalePath()
|
|
53
|
+
const runtimeConfig = useRuntimeConfig()
|
|
54
|
+
|
|
55
|
+
const state = reactive({
|
|
56
|
+
password: undefined,
|
|
57
|
+
confirmPassword: undefined
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const schema = z.object({
|
|
61
|
+
password: z.string().min(8, ('resetPassword.errors.minLength', { min: 8 })),
|
|
62
|
+
confirmPassword: z.string().min(8, ('resetPassword.errors.minLength', { min: 8 })).refine(val => val === state.password, {
|
|
63
|
+
message: ('resetPassword.errors.passwordMismatch')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const loading = ref(false)
|
|
68
|
+
|
|
69
|
+
// reactive message used by the template's v-alert
|
|
70
|
+
const message = ref(null)
|
|
71
|
+
const messageType = ref('info')
|
|
72
|
+
// ref for the Turnstile container and the token set by the widget
|
|
73
|
+
const turnstileRef = ref(null)
|
|
74
|
+
const turnstileToken = ref(null)
|
|
75
|
+
|
|
76
|
+
async function onSubmit(event) {
|
|
77
|
+
event.preventDefault()
|
|
78
|
+
if (loading.value)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
// reset any previous message
|
|
82
|
+
message.value = null
|
|
83
|
+
messageType.value = 'info'
|
|
84
|
+
|
|
85
|
+
loading.value = true
|
|
86
|
+
const { error } = await auth.resetPassword({
|
|
87
|
+
newPassword: state.password,
|
|
88
|
+
token: route.query.token
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (error) {
|
|
92
|
+
toast.show({
|
|
93
|
+
title: error.message,
|
|
94
|
+
color: 'error'
|
|
95
|
+
})
|
|
96
|
+
message.value = error.message
|
|
97
|
+
messageType.value = 'error'
|
|
98
|
+
} else {
|
|
99
|
+
const successMsg = t('resetPassword.success')
|
|
100
|
+
toast.show({
|
|
101
|
+
title: successMsg,
|
|
102
|
+
color: 'success'
|
|
103
|
+
})
|
|
104
|
+
message.value = successMsg
|
|
105
|
+
messageType.value = 'success'
|
|
106
|
+
navigateTo(localePath((runtimeConfig.public).auth.redirectGuestTo))
|
|
107
|
+
}
|
|
108
|
+
loading.value = false
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<style scoped>
|
|
113
|
+
.authForm {
|
|
114
|
+
max-width: 400px;
|
|
115
|
+
margin: 2rem auto;
|
|
116
|
+
padding: 2rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.message {
|
|
120
|
+
margin-top: 1rem;
|
|
121
|
+
padding: 1rem;
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
|
|
3
|
+
interface UserState {
|
|
4
|
+
user: any | null
|
|
5
|
+
profile: any | null
|
|
6
|
+
loading: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useUserStore = defineStore<'user', UserState>('user', {
|
|
10
|
+
state: (): UserState => ({
|
|
11
|
+
user: null,
|
|
12
|
+
profile: null,
|
|
13
|
+
loading: true
|
|
14
|
+
}),
|
|
15
|
+
|
|
16
|
+
actions: {
|
|
17
|
+
setAuth(user: any, profile: any) {
|
|
18
|
+
this.user = user
|
|
19
|
+
this.profile = profile
|
|
20
|
+
this.loading = false
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
setUser(user: any) {
|
|
24
|
+
this.user = user
|
|
25
|
+
},
|
|
26
|
+
clearUser() {
|
|
27
|
+
this.user = null
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
clear() {
|
|
31
|
+
this.user = null
|
|
32
|
+
this.profile = null
|
|
33
|
+
this.loading = false
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
getters: {
|
|
38
|
+
isAuthenticated: (s: UserState) => !!s.user,
|
|
39
|
+
isSeller: (s: UserState) => s.profile?.role?.key === 'seller' && s.profile?.seller_approved === true,
|
|
40
|
+
isAdmin: (s: UserState) => s.profile?.role?.key === 'admin',
|
|
41
|
+
isLoggedIn: (state: UserState) => !!state.user
|
|
42
|
+
}
|
|
43
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { stripeClient } from '@better-auth/stripe/client'
|
|
2
|
+
import { polarClient, adminClient, inferAdditionalFields } from '@mframework/adapter-betterauth/'
|
|
3
|
+
|
|
4
|
+
export type AuthPluginOptions = {
|
|
5
|
+
/** whether to enable subscription support for stripe plugin */
|
|
6
|
+
subscription?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getAuthPlugins(opts: AuthPluginOptions = {}): any[] {
|
|
10
|
+
const { subscription = true } = opts
|
|
11
|
+
|
|
12
|
+
return [
|
|
13
|
+
inferAdditionalFields({
|
|
14
|
+
user: {
|
|
15
|
+
polarCustomerId: {
|
|
16
|
+
type: 'string'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}),
|
|
20
|
+
adminClient(),
|
|
21
|
+
polarClient(),
|
|
22
|
+
stripeClient({ subscription })
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Re-export the adapter-provided BetterAuth provider implementation so the
|
|
2
|
+
// layer continues to work while the implementation lives in the adapter
|
|
3
|
+
// package.
|
|
4
|
+
import * as BetterAuth from '@mframework/adapter-betterauth'
|
|
5
|
+
import { registerAuthProvider, getAuthConfig, getFrameworkContext } from '@mframework/adapter-betterauth/'
|
|
6
|
+
import type { AuthProvider } from '@mframework/adapter-betterauth/'
|
|
7
|
+
|
|
8
|
+
// Create a single shared Better Auth client instance
|
|
9
|
+
let client: any = null
|
|
10
|
+
|
|
11
|
+
function resolveClientFactory() {
|
|
12
|
+
return (BetterAuth as any).Client ?? (BetterAuth as any).default ?? BetterAuth
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getClient() {
|
|
16
|
+
if (client) return client
|
|
17
|
+
|
|
18
|
+
const config = getAuthConfig()
|
|
19
|
+
const ctx = getFrameworkContext()
|
|
20
|
+
|
|
21
|
+
const Client = resolveClientFactory()
|
|
22
|
+
client = Client({
|
|
23
|
+
baseURL: config.baseUrl,
|
|
24
|
+
fetch: async (url: string | Request | URL, options: RequestInit = {}) => {
|
|
25
|
+
// Framework‑agnostic request wrapper
|
|
26
|
+
const baseHeaders = ctx.getRequestHeaders?.() || {}
|
|
27
|
+
|
|
28
|
+
let optionHeaders: Record<string, string> = {}
|
|
29
|
+
|
|
30
|
+
if (options.headers instanceof Headers) {
|
|
31
|
+
options.headers.forEach((value, key) => {
|
|
32
|
+
optionHeaders[key] = value
|
|
33
|
+
})
|
|
34
|
+
} else if (Array.isArray(options.headers)) {
|
|
35
|
+
for (const [k, v] of options.headers) {
|
|
36
|
+
optionHeaders[k] = v
|
|
37
|
+
}
|
|
38
|
+
} else if (typeof options.headers === 'object' && options.headers !== null) {
|
|
39
|
+
optionHeaders = options.headers as Record<string, string>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const headers = {
|
|
43
|
+
...baseHeaders,
|
|
44
|
+
...optionHeaders
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return fetch(url, { ...options, headers })
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return client
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const BetterAuthProvider: AuthProvider = {
|
|
55
|
+
async login(credentials) {
|
|
56
|
+
const client = getClient()
|
|
57
|
+
const result = await client.signIn(credentials)
|
|
58
|
+
|
|
59
|
+
if (result.error) {
|
|
60
|
+
throw new Error(result.error.message || 'Login failed')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result.data
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async logout() {
|
|
67
|
+
const client = getClient()
|
|
68
|
+
await client.signOut()
|
|
69
|
+
|
|
70
|
+
const ctx = getFrameworkContext()
|
|
71
|
+
ctx.reloadApp?.()
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async session() {
|
|
75
|
+
const client = getClient()
|
|
76
|
+
const result = await client.session()
|
|
77
|
+
|
|
78
|
+
if (result.error) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result.data
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async register(data) {
|
|
86
|
+
const client = getClient()
|
|
87
|
+
const result = await client.signUp(data)
|
|
88
|
+
|
|
89
|
+
if (result.error) {
|
|
90
|
+
throw new Error(result.error.message || 'Registration failed')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result.data
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async refresh() {
|
|
97
|
+
const client = getClient()
|
|
98
|
+
const result = await client.refresh()
|
|
99
|
+
|
|
100
|
+
if (result.error) {
|
|
101
|
+
throw new Error(result.error.message || 'Session refresh failed')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result.data
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Prefer the adapter-provided implementation when available. Use a
|
|
109
|
+
// synchronous `require` guarded in a try/catch so the layer can work in
|
|
110
|
+
// development even when the adapter package isn't published yet.
|
|
111
|
+
declare const require: any
|
|
112
|
+
try {
|
|
113
|
+
// Try the package root first, then the source path used during development
|
|
114
|
+
const adapterPkg = require('@mframework/adapter-betterauth') || require('@mframework/adapter-betterauth/src/provider')
|
|
115
|
+
const AdapterProvider = adapterPkg?.BetterAuthProvider ?? adapterPkg?.default
|
|
116
|
+
if (AdapterProvider) {
|
|
117
|
+
registerAuthProvider('better-auth', AdapterProvider)
|
|
118
|
+
} else {
|
|
119
|
+
registerAuthProvider('better-auth', BetterAuthProvider)
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
registerAuthProvider('better-auth', BetterAuthProvider)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default BetterAuthProvider
|
package/nuxt.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mframework/layer-auth",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The authentication layer for M Framework applications.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./nuxt.config.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
|
+
"build": "tsc -p tsconfig.json"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"better-auth",
|
|
13
|
+
"authentication",
|
|
14
|
+
"auth",
|
|
15
|
+
"typescript",
|
|
16
|
+
"vue",
|
|
17
|
+
"m-framework"
|
|
18
|
+
],
|
|
19
|
+
"author": "M Framework",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@mframework/adapter-betterauth": "^0.0.7",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.0.9",
|
|
27
|
+
"@types/uuid": "^10.0.0",
|
|
28
|
+
"nuxt": "^4.3.0"
|
|
29
|
+
}
|
|
30
|
+
}
|