nuxt-auth-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +307 -0
- package/dist/module.cjs +5 -0
- package/dist/module.d.mts +5 -0
- package/dist/module.d.ts +5 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +92 -0
- package/dist/runtime/components/auth/AuthLayout.vue +117 -0
- package/dist/runtime/components/auth/ForgotPasswordForm.vue +110 -0
- package/dist/runtime/components/auth/LoginForm.vue +209 -0
- package/dist/runtime/components/auth/RegisterForm.vue +182 -0
- package/dist/runtime/components/auth/ResetPasswordForm.vue +134 -0
- package/dist/runtime/components/profile/UpdatePasswordForm.vue +115 -0
- package/dist/runtime/components/profile/UpdateProfileForm.vue +157 -0
- package/dist/runtime/composables/useAuth.d.ts +58 -0
- package/dist/runtime/composables/useAuth.js +214 -0
- package/dist/runtime/middleware/auth.d.ts +2 -0
- package/dist/runtime/middleware/auth.js +14 -0
- package/dist/runtime/middleware/guest.d.ts +2 -0
- package/dist/runtime/middleware/guest.js +11 -0
- package/dist/runtime/middleware/role.d.ts +2 -0
- package/dist/runtime/middleware/role.js +18 -0
- package/dist/runtime/plugins/auth.d.ts +2 -0
- package/dist/runtime/plugins/auth.js +20 -0
- package/dist/runtime/stores/auth.d.ts +38 -0
- package/dist/runtime/stores/auth.js +47 -0
- package/dist/runtime/types/index.d.ts +85 -0
- package/dist/runtime/types/index.js +0 -0
- package/dist/types.d.mts +1 -0
- package/dist/types.d.ts +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="profile-update">
|
|
3
|
+
<h2 class="text-2xl font-bold text-[#1a2e1a] mb-6">{{ title }}</h2>
|
|
4
|
+
|
|
5
|
+
<form @submit.prevent="handleSubmit" class="space-y-5">
|
|
6
|
+
<div v-if="successMsg" class="bg-green-50 border border-green-200 text-green-700 rounded-xl px-4 py-3 text-sm">
|
|
7
|
+
{{ successMsg }}
|
|
8
|
+
</div>
|
|
9
|
+
<div v-if="error" class="bg-red-50 border border-red-200 text-red-700 rounded-xl px-4 py-3 text-sm">
|
|
10
|
+
{{ error }}
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Avatar -->
|
|
14
|
+
<div v-if="showAvatar" class="flex items-center gap-5">
|
|
15
|
+
<div class="relative">
|
|
16
|
+
<div class="w-20 h-20 rounded-full overflow-hidden bg-[#1B4332]/10 flex items-center justify-center">
|
|
17
|
+
<img v-if="avatarPreview" :src="avatarPreview" class="w-full h-full object-cover" alt="avatar" />
|
|
18
|
+
<span v-else class="text-2xl font-bold text-[#1B4332]">{{ initials }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
<label class="absolute -bottom-1 -right-1 w-7 h-7 bg-[#1B4332] rounded-full flex items-center justify-center cursor-pointer hover:bg-[#163828] transition">
|
|
21
|
+
<svg class="w-3.5 h-3.5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
22
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
|
23
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
24
|
+
</svg>
|
|
25
|
+
<input type="file" accept="image/*" class="hidden" @change="handleAvatarChange" />
|
|
26
|
+
</label>
|
|
27
|
+
</div>
|
|
28
|
+
<div>
|
|
29
|
+
<p class="font-medium text-[#1a2e1a]">{{ form.name || user?.name }}</p>
|
|
30
|
+
<p class="text-sm text-[#6b7c6b]">{{ user?.email }}</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Name -->
|
|
35
|
+
<div>
|
|
36
|
+
<label class="block text-sm font-medium text-[#1a2e1a] mb-1.5">Nom complet</label>
|
|
37
|
+
<div class="relative">
|
|
38
|
+
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-[#8a9a8a]">
|
|
39
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
40
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
41
|
+
</svg>
|
|
42
|
+
</span>
|
|
43
|
+
<input
|
|
44
|
+
v-model="form.name"
|
|
45
|
+
type="text"
|
|
46
|
+
:placeholder="user?.name || 'Votre nom'"
|
|
47
|
+
class="w-full bg-white border border-[#e0e0d8] rounded-2xl py-3 pl-11 pr-4 text-[#1a2e1a] placeholder-[#aab4aa] focus:outline-none focus:border-[#1B4332] focus:ring-2 focus:ring-[#1B4332]/20 transition"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<p v-if="fieldErrors.name" class="text-red-500 text-xs mt-1">{{ fieldErrors.name }}</p>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Email -->
|
|
54
|
+
<div>
|
|
55
|
+
<label class="block text-sm font-medium text-[#1a2e1a] mb-1.5">Email</label>
|
|
56
|
+
<div class="relative">
|
|
57
|
+
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-[#8a9a8a]">
|
|
58
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
59
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
60
|
+
</svg>
|
|
61
|
+
</span>
|
|
62
|
+
<input
|
|
63
|
+
v-model="form.email"
|
|
64
|
+
type="email"
|
|
65
|
+
:placeholder="user?.email || 'votre@email.com'"
|
|
66
|
+
class="w-full bg-white border border-[#e0e0d8] rounded-2xl py-3 pl-11 pr-4 text-[#1a2e1a] placeholder-[#aab4aa] focus:outline-none focus:border-[#1B4332] focus:ring-2 focus:ring-[#1B4332]/20 transition"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<p v-if="fieldErrors.email" class="text-red-500 text-xs mt-1">{{ fieldErrors.email }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- Extra slots for additional fields -->
|
|
73
|
+
<slot name="extra-fields" :form="form" />
|
|
74
|
+
|
|
75
|
+
<button
|
|
76
|
+
type="submit"
|
|
77
|
+
:disabled="loading"
|
|
78
|
+
class="bg-[#1B4332] hover:bg-[#163828] text-[#D4FF6B] font-semibold py-3 px-8 rounded-2xl transition-colors disabled:opacity-60"
|
|
79
|
+
>
|
|
80
|
+
<span v-if="!loading">Enregistrer les modifications</span>
|
|
81
|
+
<span v-else class="flex items-center gap-2">
|
|
82
|
+
<svg class="animate-spin w-4 h-4" viewBox="0 0 24 24" fill="none">
|
|
83
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
84
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
|
85
|
+
</svg>
|
|
86
|
+
Enregistrement...
|
|
87
|
+
</span>
|
|
88
|
+
</button>
|
|
89
|
+
</form>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script setup lang="ts">
|
|
94
|
+
import { ref, reactive, computed, watch } from 'vue'
|
|
95
|
+
import { useAuth } from '../../composables/useAuth'
|
|
96
|
+
|
|
97
|
+
withDefaults(defineProps<{
|
|
98
|
+
title?: string
|
|
99
|
+
showAvatar?: boolean
|
|
100
|
+
}>(), {
|
|
101
|
+
title: 'Informations du profil',
|
|
102
|
+
showAvatar: true
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const emit = defineEmits<{ success: [user: any] }>()
|
|
106
|
+
|
|
107
|
+
const { user, updateProfile, loading } = useAuth()
|
|
108
|
+
|
|
109
|
+
const form = reactive({
|
|
110
|
+
name: user?.name || '',
|
|
111
|
+
email: user?.email || '',
|
|
112
|
+
avatar: null as File | null
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const avatarPreview = ref<string | null>(user?.avatar as string || null)
|
|
116
|
+
const error = ref<string | null>(null)
|
|
117
|
+
const successMsg = ref<string | null>(null)
|
|
118
|
+
const fieldErrors = reactive<Record<string, string>>({})
|
|
119
|
+
|
|
120
|
+
const initials = computed(() => {
|
|
121
|
+
const name = form.name || user?.name || ''
|
|
122
|
+
return name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
function handleAvatarChange(e: Event) {
|
|
126
|
+
const file = (e.target as HTMLInputElement).files?.[0]
|
|
127
|
+
if (!file) return
|
|
128
|
+
form.avatar = file
|
|
129
|
+
const reader = new FileReader()
|
|
130
|
+
reader.onload = (ev) => { avatarPreview.value = ev.target?.result as string }
|
|
131
|
+
reader.readAsDataURL(file)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function handleSubmit() {
|
|
135
|
+
error.value = null
|
|
136
|
+
successMsg.value = null
|
|
137
|
+
Object.keys(fieldErrors).forEach(k => delete fieldErrors[k])
|
|
138
|
+
|
|
139
|
+
const data: any = {}
|
|
140
|
+
if (form.name) data.name = form.name
|
|
141
|
+
if (form.email) data.email = form.email
|
|
142
|
+
if (form.avatar) data.avatar = form.avatar
|
|
143
|
+
|
|
144
|
+
const result = await updateProfile(data)
|
|
145
|
+
|
|
146
|
+
if (result.success) {
|
|
147
|
+
successMsg.value = 'Profil mis à jour avec succès !'
|
|
148
|
+
emit('success', user)
|
|
149
|
+
} else if (result.error) {
|
|
150
|
+
if (result.error.errors) {
|
|
151
|
+
Object.entries(result.error.errors).forEach(([k, v]) => { fieldErrors[k] = v[0] })
|
|
152
|
+
} else {
|
|
153
|
+
error.value = result.error.message
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LoginCredentials, RegisterData, UpdateProfileData, UpdatePasswordData, ForgotPasswordData, ResetPasswordData, ApiError } from '../types/index.js';
|
|
2
|
+
export declare function useAuth(): {
|
|
3
|
+
user: any;
|
|
4
|
+
token: any;
|
|
5
|
+
loading: any;
|
|
6
|
+
isAuthenticated: any;
|
|
7
|
+
isGuest: any;
|
|
8
|
+
hasRole: any;
|
|
9
|
+
hasPermission: any;
|
|
10
|
+
login: (credentials: LoginCredentials) => Promise<{
|
|
11
|
+
success: boolean;
|
|
12
|
+
error?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
success: boolean;
|
|
15
|
+
error: ApiError;
|
|
16
|
+
}>;
|
|
17
|
+
register: (data: RegisterData) => Promise<{
|
|
18
|
+
success: boolean;
|
|
19
|
+
error?: undefined;
|
|
20
|
+
} | {
|
|
21
|
+
success: boolean;
|
|
22
|
+
error: ApiError;
|
|
23
|
+
}>;
|
|
24
|
+
logout: () => Promise<void>;
|
|
25
|
+
fetchUser: () => Promise<any>;
|
|
26
|
+
updateProfile: (data: UpdateProfileData) => Promise<{
|
|
27
|
+
success: boolean;
|
|
28
|
+
error?: undefined;
|
|
29
|
+
} | {
|
|
30
|
+
success: boolean;
|
|
31
|
+
error: ApiError;
|
|
32
|
+
}>;
|
|
33
|
+
updatePassword: (data: UpdatePasswordData) => Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
error?: undefined;
|
|
36
|
+
} | {
|
|
37
|
+
success: boolean;
|
|
38
|
+
error: ApiError;
|
|
39
|
+
}>;
|
|
40
|
+
forgotPassword: (data: ForgotPasswordData) => Promise<{
|
|
41
|
+
success: boolean;
|
|
42
|
+
message: string;
|
|
43
|
+
error?: undefined;
|
|
44
|
+
} | {
|
|
45
|
+
success: boolean;
|
|
46
|
+
error: ApiError;
|
|
47
|
+
message?: undefined;
|
|
48
|
+
}>;
|
|
49
|
+
resetPassword: (data: ResetPasswordData) => Promise<{
|
|
50
|
+
success: boolean;
|
|
51
|
+
message: string;
|
|
52
|
+
error?: undefined;
|
|
53
|
+
} | {
|
|
54
|
+
success: boolean;
|
|
55
|
+
error: ApiError;
|
|
56
|
+
message?: undefined;
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { useRuntimeConfig, navigateTo, useCookie } from "#app";
|
|
2
|
+
import { useAuthStore } from "../stores/auth.js";
|
|
3
|
+
export function useAuth() {
|
|
4
|
+
const store = useAuthStore();
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const opts = config.public.nuxtAuthKit;
|
|
7
|
+
const apiBase = opts?.apiBase || "";
|
|
8
|
+
const endpoints = {
|
|
9
|
+
login: "/api/auth/login",
|
|
10
|
+
register: "/api/auth/register",
|
|
11
|
+
logout: "/api/auth/logout",
|
|
12
|
+
me: "/api/auth/me",
|
|
13
|
+
updateProfile: "/api/auth/profile",
|
|
14
|
+
updatePassword: "/api/auth/password",
|
|
15
|
+
forgotPassword: "/api/auth/forgot-password",
|
|
16
|
+
resetPassword: "/api/auth/reset-password",
|
|
17
|
+
...opts?.endpoints || {}
|
|
18
|
+
};
|
|
19
|
+
const redirects = {
|
|
20
|
+
login: "/auth/login",
|
|
21
|
+
home: "/",
|
|
22
|
+
afterLogout: "/auth/login",
|
|
23
|
+
...opts?.redirects || {}
|
|
24
|
+
};
|
|
25
|
+
const tokenCookieName = opts?.tokenCookieName || "auth_token";
|
|
26
|
+
const tokenCookie = useCookie(tokenCookieName, {
|
|
27
|
+
default: () => null,
|
|
28
|
+
secure: true,
|
|
29
|
+
sameSite: "lax",
|
|
30
|
+
maxAge: 60 * 60 * 24 * 7
|
|
31
|
+
// 7 days
|
|
32
|
+
});
|
|
33
|
+
async function apiFetch(path, options = {}) {
|
|
34
|
+
const headers = {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
"Accept": "application/json",
|
|
37
|
+
...options.headers || {}
|
|
38
|
+
};
|
|
39
|
+
if (store.token) {
|
|
40
|
+
headers["Authorization"] = `Bearer ${store.token}`;
|
|
41
|
+
}
|
|
42
|
+
const response = await fetch(`${apiBase}${path}`, {
|
|
43
|
+
...options,
|
|
44
|
+
headers
|
|
45
|
+
});
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const error = {
|
|
49
|
+
message: data.message || "Une erreur est survenue",
|
|
50
|
+
errors: data.errors
|
|
51
|
+
};
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
function persistToken(token) {
|
|
57
|
+
store.setToken(token);
|
|
58
|
+
tokenCookie.value = token;
|
|
59
|
+
}
|
|
60
|
+
function clearToken() {
|
|
61
|
+
store.clearAuth();
|
|
62
|
+
tokenCookie.value = null;
|
|
63
|
+
}
|
|
64
|
+
async function login(credentials) {
|
|
65
|
+
store.setLoading(true);
|
|
66
|
+
try {
|
|
67
|
+
const data = await apiFetch(endpoints.login, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
body: JSON.stringify(credentials)
|
|
70
|
+
});
|
|
71
|
+
persistToken(data.token);
|
|
72
|
+
store.setUser(data.user);
|
|
73
|
+
await navigateTo(redirects.home);
|
|
74
|
+
return { success: true };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return { success: false, error };
|
|
77
|
+
} finally {
|
|
78
|
+
store.setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function register(data) {
|
|
82
|
+
store.setLoading(true);
|
|
83
|
+
try {
|
|
84
|
+
const response = await apiFetch(endpoints.register, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: JSON.stringify(data)
|
|
87
|
+
});
|
|
88
|
+
persistToken(response.token);
|
|
89
|
+
store.setUser(response.user);
|
|
90
|
+
await navigateTo(redirects.home);
|
|
91
|
+
return { success: true };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return { success: false, error };
|
|
94
|
+
} finally {
|
|
95
|
+
store.setLoading(false);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function logout() {
|
|
99
|
+
store.setLoading(true);
|
|
100
|
+
try {
|
|
101
|
+
await apiFetch(endpoints.logout, { method: "POST" });
|
|
102
|
+
} catch (_) {
|
|
103
|
+
} finally {
|
|
104
|
+
clearToken();
|
|
105
|
+
store.setLoading(false);
|
|
106
|
+
await navigateTo(redirects.afterLogout);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function fetchUser() {
|
|
110
|
+
if (!store.token && !tokenCookie.value) return null;
|
|
111
|
+
if (!store.token && tokenCookie.value) {
|
|
112
|
+
store.setToken(tokenCookie.value);
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const data = await apiFetch(endpoints.me);
|
|
116
|
+
store.setUser(data.user || data);
|
|
117
|
+
return store.user;
|
|
118
|
+
} catch (_) {
|
|
119
|
+
clearToken();
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function updateProfile(data) {
|
|
124
|
+
store.setLoading(true);
|
|
125
|
+
try {
|
|
126
|
+
let body;
|
|
127
|
+
let headers = {};
|
|
128
|
+
if (data.avatar instanceof File) {
|
|
129
|
+
const formData = new FormData();
|
|
130
|
+
Object.entries(data).forEach(([k, v]) => {
|
|
131
|
+
if (v !== void 0 && v !== null) formData.append(k, v);
|
|
132
|
+
});
|
|
133
|
+
body = formData;
|
|
134
|
+
} else {
|
|
135
|
+
body = JSON.stringify(data);
|
|
136
|
+
headers["Content-Type"] = "application/json";
|
|
137
|
+
}
|
|
138
|
+
const response = await apiFetch(endpoints.updateProfile, {
|
|
139
|
+
method: "PUT",
|
|
140
|
+
body,
|
|
141
|
+
headers
|
|
142
|
+
});
|
|
143
|
+
store.setUser(response.user || response);
|
|
144
|
+
return { success: true };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return { success: false, error };
|
|
147
|
+
} finally {
|
|
148
|
+
store.setLoading(false);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function updatePassword(data) {
|
|
152
|
+
store.setLoading(true);
|
|
153
|
+
try {
|
|
154
|
+
await apiFetch(endpoints.updatePassword, {
|
|
155
|
+
method: "PUT",
|
|
156
|
+
body: JSON.stringify(data)
|
|
157
|
+
});
|
|
158
|
+
return { success: true };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return { success: false, error };
|
|
161
|
+
} finally {
|
|
162
|
+
store.setLoading(false);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function forgotPassword(data) {
|
|
166
|
+
store.setLoading(true);
|
|
167
|
+
try {
|
|
168
|
+
const response = await apiFetch(endpoints.forgotPassword, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
body: JSON.stringify(data)
|
|
171
|
+
});
|
|
172
|
+
return { success: true, message: response.message };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return { success: false, error };
|
|
175
|
+
} finally {
|
|
176
|
+
store.setLoading(false);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function resetPassword(data) {
|
|
180
|
+
store.setLoading(true);
|
|
181
|
+
try {
|
|
182
|
+
const response = await apiFetch(endpoints.resetPassword, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
body: JSON.stringify(data)
|
|
185
|
+
});
|
|
186
|
+
return { success: true, message: response.message };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return { success: false, error };
|
|
189
|
+
} finally {
|
|
190
|
+
store.setLoading(false);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const { hasRole, hasPermission } = store;
|
|
194
|
+
return {
|
|
195
|
+
// State
|
|
196
|
+
user: store.user,
|
|
197
|
+
token: store.token,
|
|
198
|
+
loading: store.loading,
|
|
199
|
+
isAuthenticated: store.isAuthenticated,
|
|
200
|
+
isGuest: store.isGuest,
|
|
201
|
+
// RBAC
|
|
202
|
+
hasRole,
|
|
203
|
+
hasPermission,
|
|
204
|
+
// Actions
|
|
205
|
+
login,
|
|
206
|
+
register,
|
|
207
|
+
logout,
|
|
208
|
+
fetchUser,
|
|
209
|
+
updateProfile,
|
|
210
|
+
updatePassword,
|
|
211
|
+
forgotPassword,
|
|
212
|
+
resetPassword
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
|
|
2
|
+
import { useAuthStore } from "../stores/auth.js";
|
|
3
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
4
|
+
const store = useAuthStore();
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const opts = config.public.nuxtAuthKit;
|
|
7
|
+
const loginPath = opts?.redirects?.login || "/auth/login";
|
|
8
|
+
if (!store.isAuthenticated) {
|
|
9
|
+
return navigateTo({
|
|
10
|
+
path: loginPath,
|
|
11
|
+
query: { redirect: to.fullPath }
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
|
|
2
|
+
import { useAuthStore } from "../stores/auth.js";
|
|
3
|
+
export default defineNuxtRouteMiddleware(() => {
|
|
4
|
+
const store = useAuthStore();
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const opts = config.public.nuxtAuthKit;
|
|
7
|
+
const homePath = opts?.redirects?.home || "/";
|
|
8
|
+
if (store.isAuthenticated) {
|
|
9
|
+
return navigateTo(homePath);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, navigateTo, useRuntimeConfig } from "#app";
|
|
2
|
+
import { useAuthStore } from "../stores/auth.js";
|
|
3
|
+
export default defineNuxtRouteMiddleware((to, _from) => {
|
|
4
|
+
const store = useAuthStore();
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const opts = config.public.nuxtAuthKit;
|
|
7
|
+
const loginPath = opts?.redirects?.login || "/auth/login";
|
|
8
|
+
if (!store.isAuthenticated) {
|
|
9
|
+
return navigateTo(loginPath);
|
|
10
|
+
}
|
|
11
|
+
const requiredRoles = to.meta?.roles;
|
|
12
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
13
|
+
const superAdmin = opts?.rbac?.superAdminRole || "super-admin";
|
|
14
|
+
if (!store.hasRole(superAdmin) && !store.hasRole(requiredRoles)) {
|
|
15
|
+
return navigateTo("/");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useCookie, useRuntimeConfig } from "#app";
|
|
2
|
+
import { useAuthStore } from "../stores/auth.js";
|
|
3
|
+
import { useAuth } from "../composables/useAuth.js";
|
|
4
|
+
export default defineNuxtPlugin(async (nuxtApp) => {
|
|
5
|
+
const store = useAuthStore();
|
|
6
|
+
const config = useRuntimeConfig();
|
|
7
|
+
const opts = config.public.nuxtAuthKit;
|
|
8
|
+
const tokenCookieName = opts?.tokenCookieName || "auth_token";
|
|
9
|
+
const tokenCookie = useCookie(tokenCookieName);
|
|
10
|
+
if (tokenCookie.value && !store.token) {
|
|
11
|
+
store.setToken(tokenCookie.value);
|
|
12
|
+
try {
|
|
13
|
+
const { fetchUser } = useAuth();
|
|
14
|
+
await fetchUser();
|
|
15
|
+
} catch (_) {
|
|
16
|
+
store.clearAuth();
|
|
17
|
+
tokenCookie.value = null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AuthUser } from '../types/index.js';
|
|
2
|
+
export declare const useAuthStore: import("pinia").StoreDefinition<"nuxt-auth-kit", Pick<{
|
|
3
|
+
user: import("vue").Ref<any, any>;
|
|
4
|
+
token: import("vue").Ref<string | null, string | null>;
|
|
5
|
+
loading: import("vue").Ref<boolean, boolean>;
|
|
6
|
+
isAuthenticated: import("vue").ComputedRef<boolean>;
|
|
7
|
+
isGuest: import("vue").ComputedRef<boolean>;
|
|
8
|
+
hasRole: (role: string | string[]) => boolean;
|
|
9
|
+
hasPermission: (permission: string | string[]) => boolean;
|
|
10
|
+
setUser: (userData: AuthUser) => void;
|
|
11
|
+
setToken: (tokenValue: string) => void;
|
|
12
|
+
clearAuth: () => void;
|
|
13
|
+
setLoading: (state: boolean) => void;
|
|
14
|
+
}, "user" | "token" | "loading">, Pick<{
|
|
15
|
+
user: import("vue").Ref<any, any>;
|
|
16
|
+
token: import("vue").Ref<string | null, string | null>;
|
|
17
|
+
loading: import("vue").Ref<boolean, boolean>;
|
|
18
|
+
isAuthenticated: import("vue").ComputedRef<boolean>;
|
|
19
|
+
isGuest: import("vue").ComputedRef<boolean>;
|
|
20
|
+
hasRole: (role: string | string[]) => boolean;
|
|
21
|
+
hasPermission: (permission: string | string[]) => boolean;
|
|
22
|
+
setUser: (userData: AuthUser) => void;
|
|
23
|
+
setToken: (tokenValue: string) => void;
|
|
24
|
+
clearAuth: () => void;
|
|
25
|
+
setLoading: (state: boolean) => void;
|
|
26
|
+
}, "isAuthenticated" | "isGuest">, Pick<{
|
|
27
|
+
user: import("vue").Ref<any, any>;
|
|
28
|
+
token: import("vue").Ref<string | null, string | null>;
|
|
29
|
+
loading: import("vue").Ref<boolean, boolean>;
|
|
30
|
+
isAuthenticated: import("vue").ComputedRef<boolean>;
|
|
31
|
+
isGuest: import("vue").ComputedRef<boolean>;
|
|
32
|
+
hasRole: (role: string | string[]) => boolean;
|
|
33
|
+
hasPermission: (permission: string | string[]) => boolean;
|
|
34
|
+
setUser: (userData: AuthUser) => void;
|
|
35
|
+
setToken: (tokenValue: string) => void;
|
|
36
|
+
clearAuth: () => void;
|
|
37
|
+
setLoading: (state: boolean) => void;
|
|
38
|
+
}, "hasRole" | "hasPermission" | "setUser" | "setToken" | "clearAuth" | "setLoading">>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineStore } from "pinia";
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
export const useAuthStore = defineStore("nuxt-auth-kit", () => {
|
|
4
|
+
const user = ref(null);
|
|
5
|
+
const token = ref(null);
|
|
6
|
+
const loading = ref(false);
|
|
7
|
+
const isAuthenticated = computed(() => !!token.value && !!user.value);
|
|
8
|
+
const isGuest = computed(() => !isAuthenticated.value);
|
|
9
|
+
function hasRole(role) {
|
|
10
|
+
if (!user.value?.roles) return false;
|
|
11
|
+
const roles = Array.isArray(role) ? role : [role];
|
|
12
|
+
return roles.some((r) => user.value.roles.includes(r));
|
|
13
|
+
}
|
|
14
|
+
function hasPermission(permission) {
|
|
15
|
+
if (!user.value?.permissions) return false;
|
|
16
|
+
const perms = Array.isArray(permission) ? permission : [permission];
|
|
17
|
+
return perms.some((p) => user.value.permissions.includes(p));
|
|
18
|
+
}
|
|
19
|
+
function setUser(userData) {
|
|
20
|
+
user.value = userData;
|
|
21
|
+
}
|
|
22
|
+
function setToken(tokenValue) {
|
|
23
|
+
token.value = tokenValue;
|
|
24
|
+
}
|
|
25
|
+
function clearAuth() {
|
|
26
|
+
user.value = null;
|
|
27
|
+
token.value = null;
|
|
28
|
+
}
|
|
29
|
+
function setLoading(state) {
|
|
30
|
+
loading.value = state;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
user,
|
|
34
|
+
token,
|
|
35
|
+
loading,
|
|
36
|
+
isAuthenticated,
|
|
37
|
+
isGuest,
|
|
38
|
+
hasRole,
|
|
39
|
+
hasPermission,
|
|
40
|
+
setUser,
|
|
41
|
+
setToken,
|
|
42
|
+
clearAuth,
|
|
43
|
+
setLoading
|
|
44
|
+
};
|
|
45
|
+
}, {
|
|
46
|
+
persist: false
|
|
47
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export interface AuthUser {
|
|
2
|
+
id: number | string;
|
|
3
|
+
name: string;
|
|
4
|
+
email: string;
|
|
5
|
+
avatar?: string;
|
|
6
|
+
roles?: string[];
|
|
7
|
+
permissions?: string[];
|
|
8
|
+
email_verified_at?: string | null;
|
|
9
|
+
created_at?: string;
|
|
10
|
+
updated_at?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface LoginCredentials {
|
|
14
|
+
email: string;
|
|
15
|
+
password: string;
|
|
16
|
+
remember?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface RegisterData {
|
|
19
|
+
name: string;
|
|
20
|
+
email: string;
|
|
21
|
+
password: string;
|
|
22
|
+
password_confirmation: string;
|
|
23
|
+
role?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface UpdateProfileData {
|
|
27
|
+
name?: string;
|
|
28
|
+
email?: string;
|
|
29
|
+
avatar?: File | string | null;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface UpdatePasswordData {
|
|
33
|
+
current_password: string;
|
|
34
|
+
password: string;
|
|
35
|
+
password_confirmation: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ForgotPasswordData {
|
|
38
|
+
email: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ResetPasswordData {
|
|
41
|
+
token: string;
|
|
42
|
+
email: string;
|
|
43
|
+
password: string;
|
|
44
|
+
password_confirmation: string;
|
|
45
|
+
}
|
|
46
|
+
export interface AuthResponse {
|
|
47
|
+
user: AuthUser;
|
|
48
|
+
token: string;
|
|
49
|
+
token_type?: string;
|
|
50
|
+
expires_in?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ApiError {
|
|
53
|
+
message: string;
|
|
54
|
+
errors?: Record<string, string[]>;
|
|
55
|
+
}
|
|
56
|
+
export interface ModuleOptions {
|
|
57
|
+
/** Laravel API base URL */
|
|
58
|
+
apiBase: string;
|
|
59
|
+
/** API endpoints (customizable) */
|
|
60
|
+
endpoints?: {
|
|
61
|
+
login?: string;
|
|
62
|
+
register?: string;
|
|
63
|
+
logout?: string;
|
|
64
|
+
me?: string;
|
|
65
|
+
updateProfile?: string;
|
|
66
|
+
updatePassword?: string;
|
|
67
|
+
forgotPassword?: string;
|
|
68
|
+
resetPassword?: string;
|
|
69
|
+
};
|
|
70
|
+
/** Token storage strategy */
|
|
71
|
+
tokenStorage?: 'cookie' | 'localStorage';
|
|
72
|
+
/** Cookie name for token */
|
|
73
|
+
tokenCookieName?: string;
|
|
74
|
+
/** Redirect routes */
|
|
75
|
+
redirects?: {
|
|
76
|
+
login?: string;
|
|
77
|
+
home?: string;
|
|
78
|
+
afterLogout?: string;
|
|
79
|
+
};
|
|
80
|
+
/** Role-based access control */
|
|
81
|
+
rbac?: {
|
|
82
|
+
superAdminRole?: string;
|
|
83
|
+
defaultUserRole?: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
File without changes
|