@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.
@@ -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
@@ -0,0 +1,18 @@
1
+ import {
2
+ defineNuxtConfig
3
+ } from 'nuxt/config'
4
+
5
+ export default defineNuxtConfig({
6
+ $meta: {
7
+ name: 'auth',
8
+ },
9
+
10
+ runtimeConfig: {
11
+ auth: {
12
+ redirect: {
13
+ login: '/login',
14
+ home: '/'
15
+ }
16
+ }
17
+ }
18
+ })
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
+ }
@@ -0,0 +1,7 @@
1
+ import { defineEventHandler, toWebRequest } from 'h3';
2
+ import { auth } from '../../utils/auth';
3
+
4
+
5
+ export default defineEventHandler((event) => {
6
+ return auth.handler(toWebRequest(event));
7
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../.nuxt/tsconfig.server.json"
3
+ }
@@ -0,0 +1,3 @@
1
+ // Re-export the configured BetterAuth runtime from the adapter package.
2
+ export { auth } from '@mframework/adapter-betterauth'
3
+ export type { Auth } from '@mframework/adapter-betterauth'