npm install nuxt-auth-kit

nuxt-auth-kit

Module Nuxt d'authentification clé-en-main pour API Laravel. Installez-le une fois — connexion, inscription, profil, mots de passe, rôles et permissions sont prêts à l'emploi.

Nuxt 3 & 4 Laravel Sanctum TypeScript Pinia RBAC Auto-import Thème UI PhoneInput

Fonctionnalités

Tout ce dont vous avez besoin, sans rien configurer manuellement.

🔐
Authentification complète
Login, register, logout avec session persistée via cookie sécurisé.
👤
Gestion du profil
Mise à jour nom, email, avatar et changement de mot de passe.
🔑
Reset de mot de passe
Flux complet : mot de passe oublié → email → réinitialisation.
🛡️
RBAC — Rôles & Permissions
hasRole() et hasPermission() disponibles partout.
Middlewares nommés
auth, guest, role dans definePageMeta — aucun import.
🎨
Composants prêts
7 composants Vue split-screen, Tailwind CSS, auto-importés.
🖌️
Thème & styles paramétrables
Prop ui sur chaque composant — couleurs, rounded, boutons, layout.
📞
Saisie numéro de téléphone
Composant PhoneInput avec sélecteur de pays, drapeaux, formatage auto.

Installation

Compatible Nuxt 3.x et Nuxt 4.x. Requiert @nuxt/ui pour les styles.

npm
pnpm
yarn
npm install nuxt-auth-kit
pnpm add nuxt-auth-kit
yarn add nuxt-auth-kit

Configuration

Ajoutez le module dans nuxt.config.ts et pointez sur votre API Laravel.

nuxt.config.ts
ts
export default defineNuxtConfig({
  modules: [
    'nuxt-auth-kit',
    '@nuxt/ui'       // requis pour les styles Tailwind
  ],

  nuxtAuthKit: {
    apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:8000',

    endpoints: {
      login:          '/api/auth/login',
      register:       '/api/auth/register',
      logout:         '/api/auth/logout',
      me:             '/api/auth/me',
      updateProfile:  '/api/profile',
      updatePassword: '/api/profile/password',
      forgotPassword: '/api/auth/forgot-password',
      resetPassword:  '/api/auth/reset-password'
    },

    redirects: {
      login:       '/auth/login',
      home:        '/',
      afterLogout: '/auth/login'
    },

    tokenCookieName: 'auth_token',

    rbac: {
      superAdminRole:  'super-admin',
      defaultUserRole: 'user'
    }
  }
})
.env
env
NUXT_PUBLIC_API_BASE=https://api.monprojet.com
ℹ️

Auto-importuseAuth, useAuthStore, tous les composants et les middlewares sont auto-importés. Aucun import nécessaire dans vos fichiers.


📋Changelog

v1.2.0 feat: theming system, PhoneInput component, except prop on forms

Mise à jour

v1.2.0 feat: add except prop to ProfileUpdateForm and RegisterForm — hide fields dynamically

Mise à jour

v1.0.460 feat: add except prop to ProfileUpdateForm and RegisterForm — hide fields dynamically

Mise à jour

v1.0.450 refactor: update profile endpoints and improve UpdateProfileForm and UpdatePasswordForm components

Mise à jour

v1.0.44 refactor: update AuthLayout with image background and scoped CSS styles

Mise à jour

v1.0.4 fix: import useToast from #imports in LoginForm to resolve runtime error

L'erreur useToast is not defined apparaissait au boot car les composables de @nuxt/ui ne sont pas auto-importés dans le runtime d'un module. Ajout de import { useToast } from '#imports' dans LoginForm.vue.

v1.0.44 fix: pass nuxtApp.$pinia to useAuthStore to prevent getActivePinia error

Problème corrigé

L'erreur getActivePinia() was called but there was no active Pinia apparaissait lors du boot de l'app car le store Pinia était appelé avant que Pinia ne soit monté.

  • plugins/auth.ts — passage en forme objet avec dependsOn: ['pinia'] + nuxtApp.$pinia explicite
  • composables/useAuth.tsuseNuxtApp().$pinia passé à useAuthStore()
v1.0.2 fix: register named middleware via app:resolve hook for Nuxt 4

Les middlewares auth, guest et role retournaient une erreur 500 Unknown route middleware dans Nuxt 4. Remplacé addRouteMiddleware() par le hook app:resolve.

v1.0.1 fix: add Nuxt 4 compatibility in peerDependencies

La dépendance nuxt était limitée à ^3.0.0. Étendu à ^3.0.0 || ^4.0.0.


AuthLayout

Layout split-screen — formulaire à gauche, témoignage + illustration à droite.

Prop Type Défaut Description
app-name string 'App' Nom du logo
quote object { text, author, location, avatar? }

Slots : #logo (remplace le logo), #illustration (remplace le SVG de droite).

Exemple d'utilisation
vue
<template>
  <AuthLayout :quote="quote">
    <!-- formulaire ici -->
  </AuthLayout>
</template>

<script setup lang="ts">
// Aucun import — AuthLayout est auto-importé
const quote = {
  text:     'Une expérience fluide et agréable. La plateforme rend tout si simple.',
  author:   '3S Tech Group',
  location: 'Dakar, Sénégal',
  appName: '3S-Auth',
}
</script>

AuthLoginForm

Formulaire de connexion avec email/mot de passe, social login optionnel et sélecteur de rôle.

Prop Type Défaut Description
title string 'Connexion' Titre du formulaire
subtitle string Sous-titre
show-social boolean false Boutons Google / Apple
roles Array<{value,label}> [] Sélecteur de rôle
Événement Description
@forgot-password Clic "Mot de passe oublié ?"
@register Clic "S'inscrire"
@google-login Clic bouton Google
@apple-login Clic bouton Apple
@success Connexion réussie
pages/auth/login.vue
vue
<template>
  <AuthLayout :quote="quote">
    <AuthLoginForm
      :show-social="true"
      @forgot-password="navigateTo('/auth/forgot-password')"
      @register="navigateTo('/auth/register')"
    />
  </AuthLayout>
</template>

<script setup lang="ts">
// Aucun import — tout est auto-importé
definePageMeta({ middleware: 'guest' })

const quote = {
  text: 'Une expérience fluide et agréable.',
  author: '3S Tech Group',
  location: 'Dakar'
}
</script>

AuthRegisterForm

Inscription avec prénom, nom, email, téléphone (optionnel), mot de passe et confirmation. Les champs peuvent être masqués via la prop except.

Prop Type Défaut Description
title string 'Créer un compte' Titre du formulaire
subtitle string 'Rejoignez-nous dès aujourd'hui.' Sous-titre
except ('password' | 'phone')[] [] Champs à masquer et exclure de la validation
ℹ️

Le champ phone est masqué par défaut. Il s'affiche dès qu'il n'est pas dans except. Le champ password est visible par défaut — passez :except="['password']" pour le masquer (ex : inscription via magic link).

pages/auth/register.vue
vue
<template>
  <AuthLayout>
    <!-- Défaut — mot de passe requis, sans téléphone -->
    <AuthRegisterForm @login="navigateTo('/auth/login')" />

    <!-- Sans mot de passe (ex: magic link) -->
    <AuthRegisterForm
      :except="['password']"
      @login="navigateTo('/auth/login')"
    />

    <!-- Avec téléphone, sans mot de passe -->
    <AuthRegisterForm
      :except="['password']"
      @login="navigateTo('/auth/login')"
    />

    <!-- Sans téléphone ni mot de passe -->
    <AuthRegisterForm
      :except="['password', 'phone']"
      @login="navigateTo('/auth/login')"
    />
  </AuthLayout>
</template>

<script setup lang="ts">
definePageMeta({ middleware: 'guest' })
</script>

AuthForgotPasswordForm

Envoi de l'email de réinitialisation. Affiche un état de succès après envoi.

pages/auth/forgot-password.vue
vue
<template>
  <AuthLayout>
    <AuthForgotPasswordForm
      @back-to-login="navigateTo('/auth/login')"
    />
  </AuthLayout>
</template>

<script setup lang="ts">
definePageMeta({ middleware: 'guest' })
</script>

AuthResetPasswordForm

Lit automatiquement ?token= et ?email= depuis l'URL de réinitialisation.

pages/auth/reset-password.vue
vue
<template>
  <AuthLayout>
    <AuthResetPasswordForm
      @back-to-login="navigateTo('/auth/login')"
    />
  </AuthLayout>
</template>

<script setup lang="ts">
// Lit ?token=xxx&email=xxx depuis useRoute() automatiquement
definePageMeta({ middleware: 'guest' })
</script>
ℹ️

L'email Laravel doit rediriger vers /auth/reset-password?token=TOKEN&email=EMAIL. Configurez dans config/auth.phppasswords.users.

ProfileUpdateForm

Mise à jour du profil connecté : prénom, nom, email, téléphone et avatar. Les champs peuvent être masqués via la prop except.

Prop Type Défaut Description
title string 'Informations du profil' Titre de la section
show-avatar boolean false Affiche la section avatar avec prévisualisation et initiales
except ('first_name' | 'last_name' | 'email' | 'phone')[] [] Champs à masquer et exclure de la validation
ℹ️

Le champ phone est masqué par défaut (absent du formulaire original). Il s'affiche dès qu'il n'est pas dans except. Les champs first_name, last_name et email sont visibles par défaut.

Slot #extra-fields pour ajouter des champs supplémentaires. Accès à l'objet form via la prop de slot.

pages/profile/index.vue
vue
<template>
  <div class="max-w-2xl mx-auto py-10 px-4 space-y-10">
    <!-- Défaut — prénom, nom, email visibles -->
    <ProfileUpdateForm
      title="Mon profil"
      :show-avatar="true"
      @success="onProfileSaved"
    />

    <!-- Avec téléphone, sans email -->
    <ProfileUpdateForm
      :except="['email']"
      @success="onProfileSaved"
    />

    <!-- Nom uniquement -->
    <ProfileUpdateForm
      :except="['email', 'phone']"
      @success="onProfileSaved"
    />
  </div>
</template>

<script setup lang="ts">
definePageMeta({ middleware: 'auth' })
function onProfileSaved() {
  // ex: afficher une notification toast
}
</script>

ProfileUpdatePasswordForm

Changement du mot de passe. Requiert l'ancien mot de passe.

Prop Type Défaut
title string 'Changer le mot de passe'
pages/profile/index.vue
vue
<template>
  <div class="max-w-2xl mx-auto py-10 px-4 space-y-10">
    <ProfileUpdatePasswordForm
      title="Changer le mot de passe"
    />
  </div>
</template>

<script setup lang="ts">
definePageMeta({ middleware: 'auth' })
</script>

useAuth()

Composable principal — auto-importé, aucun import nécessaire dans vos fichiers.

État réactif

Refuser
L'utilisateur connecté, ou null si déconnecté.
Ref<AuthUser | null>
ComputedisAuthenticated
true si un utilisateur est connecté avec un token valide.
ComputedRef<boolean>
ComputedisGuest
Inverse de isAuthenticated.
ComputedRef<boolean>
Refloading
true pendant une requête en cours.
Ref<boolean>

Actions

asynclogin(credentials)
Connexion. Redirige vers redirects.home en cas de succès.
Promise<{ success: boolean, error?: ApiError }>
asyncregister(data)
Inscription et connexion automatique. Redirige vers home.
Promise<{ success: boolean, error?: ApiError }>
asynclogout()
Déconnexion, suppression du cookie et redirection vers redirects.afterLogout.
Promise<void>
asyncfetchUser()
Récupère l'utilisateur depuis /api/auth/me. Appelé automatiquement au boot via le plugin.
Promise<AuthUser | null>
asyncupdateProfile(data)
Met à jour le profil. Supporte File pour l'avatar (envoi en multipart/form-data auto).
Promise<{ success: boolean, error?: ApiError }>
asyncupdatePassword(data)
Requiert current_password, new_password, password_confirmation.
Promise<{ success: boolean, error?: ApiError }>
asyncforgotPassword({ email })
Envoie l'email de réinitialisation.
Promise<{ success: boolean, message?: string, error?: ApiError }>
asyncresetPassword(data)
Requiert { token, email, password, password_confirmation }.
Promise<{ success: boolean, message?: string, error?: ApiError }>
Exemple dans une page
vue
<script setup lang="ts">
// useAuth est auto-importé — aucun import nécessaire
const { user, isAuthenticated, logout, loading } = useAuth()

async function handleLogout() {
  await logout()  // redirige automatiquement
}
</script>

<template>
  <div>
    <span v-if="isAuthenticated">Bonjour, {{ user?.name }}</span>
    <button @click="handleLogout" :disabled="loading">Déconnexion</button>
  </div>
</template>

Rôles & Permissions

hasRole() et hasPermission() accessibles via useAuth() — auto-importé.

ts
const { hasRole, hasPermission } = useAuth()

// Rôle unique
if (hasRole('admin')) { /* ... */ }

// Plusieurs rôles — au moins un suffit
if (hasRole(['admin', 'manager'])) { /* ... */ }

// Permission unique
if (hasPermission('edit-posts')) { /* ... */ }
Dans un template Vue
vue
<template>
  <AdminPanel v-if="hasRole('admin')" />

  <button v-if="hasPermission('create-post')">
    Créer un article
  </button>
</template>

<script setup lang="ts">
const { hasRole, hasPermission } = useAuth()
</script>

Le rôle super-admin (configurable via rbac.superAdminRole) bypasse toutes les vérifications dans le middleware role.


Middlewares

Trois middlewares nommés, utilisables directement dans definePageMeta. Aucun import.

auth
Redirige vers /auth/login si non connecté. Conserve l'URL dans ?redirect=.
guest
Redirige vers / si déjà connecté. À utiliser sur login, register, etc.
role
Vérifie que l'utilisateur a au moins un rôle de meta.roles.
ts
// Page connectés uniquement
definePageMeta({ middleware: 'auth' })

// Page visiteurs uniquement
definePageMeta({ middleware: 'guest' })

// Page avec rôle requis
definePageMeta({
  middleware: 'role',
  roles: ['admin', 'manager']
})

// Connecté ET rôle requis
definePageMeta({
  middleware: ['auth', 'role'],
  roles: ['admin']
})

API Laravel attendue

Tous les endpoints sont personnalisables via nuxtAuthKit.endpoints.

Méthode Route Auth Réponse Description
POST /api/auth/login { user, token } Connexion
POST /api/auth/register { user, token } Inscription
POST /api/auth/logout { message } Déconnexion
GET /api/auth/me { user } Utilisateur connecté
PUT /api/profile { user } Mise à jour profil
PUT /api/profile/password { message } Changement mdp
POST /api/auth/forgot-password { message } Email reset
POST /api/auth/reset-password { message } Reset avec token
ℹ️

Le champ user doit contenir au minimum id, name, email. Les champs roles et permissions (tableaux de strings) sont optionnels pour activer le RBAC.

Exemple avec Laravel Sanctum

routes/api.php
php
use App\Http\Controllers\AuthController;

Route::prefix('auth')->group(function () {
    Route::post('login',           [AuthController::class, 'login']);
    Route::post('register',        [AuthController::class, 'register']);
    Route::post('forgot-password', [AuthController::class, 'forgotPassword']);
    Route::post('reset-password',  [AuthController::class, 'resetPassword']);

    Route::middleware('auth:sanctum')->group(function () {
        Route::post('logout',  [AuthController::class, 'logout']);
        Route::get('me',       [AuthController::class, 'me']);
        Route::put('profile',  [AuthController::class, 'updateProfile']);
        Route::put('password', [AuthController::class, 'updatePassword']);
    });
});
AuthController.php — me()
php
public function me(Request $request): JsonResponse
{
    return response()->json([
        'user' => $request->user()->load('roles', 'permissions'),
    ]);
}

🖌Prop ui

Chaque composant accepte une prop ui?: Partial<FormTheme> pour surcharger partiellement les styles — couleurs, arrondis, variantes de boutons, couleurs du layout.

Formulaires auth & profil

vue
<AuthLoginForm
  :ui="{
    inputRounded:   'rounded-lg',
    btnColor:       'primary',
    btnRounded:     'rounded-lg',
    titleColor:     'text-slate-900',
    accentColor:    'text-blue-600',
  }"
/>

AuthLayout

vue
<AuthLayout
  :quote="quote"
  :ui="{
    layoutPageColor:    '#1e293b',
    layoutTextColor:    '#f8fafc',
    layoutTaglineColor: 'rgba(248,250,252,0.7)',
  }"
/>

Tokens disponibles

Token Défaut Description
inputRounded 'rounded-full' Border-radius des inputs
color 'neutral' Couleur Nuxt UI des inputs
btnRounded 'rounded-full' Border-radius du bouton principal
btnColor 'neutral' Couleur Nuxt UI du bouton principal
btnVariant 'solid' Variante du bouton principal
btnSecondaryColor 'secondary' Couleur des boutons secondaires
btnSecondaryVariant 'subtle' Variante des boutons secondaires
btnSecondaryRounded 'rounded-full' Border-radius des boutons secondaires
titleColor 'text-[#1a2e1a]' Couleur du titre (classe Tailwind)
subtitleColor 'text-[#6b7c6b]' Couleur du sous-titre
accentColor 'text-[#1B4332]' Liens & couleur d'accent
roleRingColor 'ring-[#1B4332]' Ring du sélecteur de rôle actif
layoutPageColor '#eeeee6' Fond du panneau gauche (valeur CSS)
layoutTextColor '#ffffff' Couleur appName panneau droit
layoutTaglineColor 'rgba(255,255,255,0.75)' Couleur tagline panneau droit

📞PhoneInput

Composant de saisie de numéro de téléphone — sélecteur de pays avec drapeaux, indicatifs, formatage AsYouType, validation E.164, détection locale automatique.

Exemple d'utilisation
vue
<PhoneInput
  v-model="form.phone"
  v-model:country-code="form.phoneCountry"
  :preferred-countries="['SN', 'FR', 'CI', 'MA', 'CM']"
  :use-browser-locale="true"
  :ui="{ inputRounded: 'rounded-xl' }"
  @data="onPhoneData"
/>

Payload de l'événement @data

ts
interface PhoneInputData {
  e164:        string | null  // ex: '+221771234567' — à stocker en BDD
  countryCode: string | null  // ex: 'SN'
  formatted:   string         // ex: '77 123 45 67' — format national
  isValid:     boolean
}

Props

Prop Type Défaut Description
modelValue string '' v-model (E.164 ou national)
countryCode string v-model:countryCode
defaultCountry string Pays initial (ISO 3166)
preferredCountries string[] [] Pays épinglés en haut
onlyCountries string[] [] Restreindre à ces pays
ignoredCountries string[] [] Exclure ces pays
useBrowserLocale boolean true Détecter depuis le navigateur
placeholder string Placeholder personnalisé
disabled boolean false Désactiver le champ
error string | boolean Message d'erreur externe
ui Partial<FormTheme> {} Surcharge de style

Types TypeScript

Exportés depuis nuxt-auth-kit. À importer uniquement si vous en avez besoin explicitement.

ts
import type {
  AuthUser,           // structure de l'utilisateur
  LoginCredentials,   // { email, password, remember? }
  RegisterData,       // { name, email, password, password_confirmation }
  UpdateProfileData,  // { name?, email?, avatar? }
  UpdatePasswordData, // { current_password, new_password, new_password_confirmation }
  ForgotPasswordData, // { email }
  ResetPasswordData,  // { token, email, password, password_confirmation }
  AuthResponse,       // { user, token }
  ApiError,           // { message, errors? }
  ModuleOptions       // config nuxtAuthKit
} from 'nuxt-auth-kit'

// Structure minimale de AuthUser
interface AuthUser {
  id:           number | string
  name:         string
  email:        string
  avatar?:      string
  roles?:       string[]
  permissions?: string[]
  [key: string]: unknown
}

Architecture

nuxt-auth-kit/
├── build.config.ts          # config unbuild
├── package.json             # v1.2.0
└── src/
    ├── module.ts                # point d'entrée du module Nuxt
    └── runtime/
        ├── types/index.ts           # types TypeScript
        ├── stores/auth.ts           # Pinia store
        ├── composables/useAuth.ts   # composable principal
        ├── plugins/auth.ts          # restauration session au boot
        ├── middleware/
        │   ├── auth.ts
        │   ├── guest.ts
        │   └── role.ts
        └── components/
            ├── auth/
            │   ├── AuthLayout.vue
            │   ├── LoginForm.vue
            │   ├── RegisterForm.vue
            │   ├── ForgotPasswordForm.vue
            │   └── ResetPasswordForm.vue
            ├── profile/
            │   ├── UpdateProfileForm.vue
            │   └── UpdatePasswordForm.vue
            └── ui/
                └── PhoneInput.vue           # 🆕 v1.2.0
        # composables/useFormTheme.ts         🆕 v1.2.0