Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Voir cette série
Mon CMS - L'administration
Mercredi 11 septembre 2024 19:29

Maintenant que nous avons créé la partie visuelle du site, autrement dénommée frontend, et même si nous y ajouterons sans doute encore quelques éléments, nous allons à présent nous consacrer à la partie cachée du CMS, l'administration, aussi dénommée backend. Nous allons conserver la même architecture avec MaryUi et Volt pour rester cohérent. On pourrait utiliser n'importe quel autre environnement et même passer par des solutions toutes prêtes comme Filament que j'aime bien. Cependant, il est bien plus pertinent d'un point de vue didactique de développer complètement cette partie.

Pour rappel, la table des matières est ici.

Développement du backend

Avantages de développer son propre backend

En développant notre propre backend, nous allons :

  • Acquérir une compréhension approfondie de l'architecture du CMS
  • Avoir un contrôle total sur les fonctionnalités et l'interface utilisateur
  • Pouvoir personnaliser chaque aspect selon nos besoins spécifiques

Structure du backend

Notre backend comprendra les sections suivantes :

  • Tableau de bord : Pour une vue d'ensemble des statistiques et activités récentes
  • Gestion des contenus : Pour créer, éditer et supprimer les pages, articles, et autres types de contenu
  • Gestion des utilisateurs : Pour administrer les comptes et les permissions
  • Configuration du site : Pour ajuster les paramètres généraux du CMS
  • Gestion des médias : Pour gérer les images

Intégration avec MaryUi et Volt

En utilisant MaryUi et Volt pour le backend, nous bénéficierons de :

  • Une cohérence visuelle entre le frontend et le backend
  • La réutilisation de composants déjà créés pour le frontend
  • Une courbe d'apprentissage réduite pour le développement

Sécurité et performances

Dans le développement de notre backend, nous accorderons une attention particulière à :

  • L'authentification robuste et la gestion des sessions
  • La protection contre les attaques courantes (CSRF, XSS, injection SQL)
  • L'optimisation des requêtes pour des performances optimales

Extensibilité

Bien que nous développions notre propre solution, nous veillerons à créer une architecture modulaire permettant :

  • L'ajout facile de nouvelles fonctionnalités
  • L'intégration de plugins tiers si nécessaire
  • La mise à jour et la maintenance simplifiées du système

En développant notre propre backend, nous gagnerons en flexibilité et en connaissance, tout en créant une solution parfaitement adaptée à nos besoins spécifiques. Cette approche, bien que plus exigeante initialement, offrira des avantages significatifs à long terme en termes de contrôle, de personnalisation et d'évolutivité de notre CMS.

Des middlewares

Pour les besoins de l'administration, on aura souvent besoin de savoir si l'utilisateur connecté est administrateur ou rédacteur pour filtrer les accès correspondants.

Un middleware dans Laravel est une couche intermédiaire qui traite les requêtes HTTP avant qu'elles n'atteignent les contrôleurs ou après qu'elles aient été traitées par les contrôleurs. Il permet d'exécuter des tâches spécifiques, comme l'authentification, la validation, la journalisation, ou la modification des requêtes et des réponses. Les middlewares sont utilisés pour centraliser et organiser le code qui doit être exécuté à chaque requête, ce qui facilite la maintenance et la réutilisation du code.

On commence par ajouter deux méthodes dans le modèle User :

public function isAdmin(): bool
{
    return 'admin' === $this->role;
}

public function isRedac(): bool
{
    return 'redac' === $this->role;
}

Administrateur

On crée un middleware pour sélectionner les administrateurs :

php artisan make:middleware IsAdmin

public function handle(Request $request, Closure $next): Response
{
    if (!auth()->user()->isAdmin()) {
        abort(403);
    }
    
    return $next($request);
}

Administrateur ou Rédacteur

On crée un middleware pour sélectionner les administrateurs ou les rédacteurs :

php artisan make:middleware IsAdminOrRedac

public function handle(Request $request, Closure $next): Response
{
    if (!auth()->user()->isAdmin() && !auth()->user()->isRedac()) {
        abort(403);
    }
    
    return $next($request);
}

Le layout

Au niveau du layout, nous n'allons pas avoir les mêmes besoins que pour l'authentification et l'affichage des articles. On va donc créer un layout spécifique pour cette partie. En particulier, on va se contenter d'une barre latérale de navigation.

La barre de navigation

On crée le composant pour la barre de navigation :

php artisan make:volt admin/sidebar --class

Avec ce code :

<?php

use Illuminate\Support\Facades\{Auth, Session};
use Livewire\Volt\Component;

new class() extends Component {
	public function logout(): void
	{
		Auth::guard('web')->logout();

		Session::invalidate();
		Session::regenerateToken();

		$this->redirect('/');
	}
}; ?>

<div>
    <x-menu activate-by-route>
        <x-menu-separator />
        <x-list-item :item="Auth::user()" value="name" sub-value="email" no-separator no-hover class="-mx-2 !-my-2 rounded">
            <x-slot:actions>
                <x-button icon="o-power" wire:click="logout" class="btn-circle btn-ghost btn-xs"
                    tooltip-left="{{ __('Logout') }}" no-wire-navigate />
            </x-slot:actions>
        </x-list-item>
        <x-menu-separator />
        <x-menu-item title="{{ __('Dashboard') }}" icon="s-building-office-2" link="{{ route('admin') }}" />        
        <x-menu-item icon="m-arrow-right-end-on-rectangle" title="{{ __('Go on site') }}" link="/" />
        <x-menu-item>
            <x-theme-toggle />
        </x-menu-item>
    </x-menu>
</div>

Vous avez sans doute remarqué qu'on a une route que nous n'avons pas encore créée, mais ça ne va pas tarder.

Le layout

Maintenant, nous pouvons ajouter notre layout :

Avec ce code :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ isset($title) ? $title . ' | ' . config('app.name') : config('app.name') }}</title>

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

<body class="min-h-screen font-sans antialiased bg-base-200/50 dark:bg-base-200">

    {{-- MAIN --}}
    <x-main full-width>

        {{-- SIDEBAR --}}
        <x-slot:sidebar drawer="main-drawer" collapsible class="bg-base-100">
            <livewire:admin.sidebar />
        </x-slot:sidebar>

        <x-slot:content>
            <!-- Drawer toggle for "main-drawer" -->
            <label for="main-drawer" class="mr-3 lg:hidden">
                <x-icon name="o-bars-3" class="cursor-pointer" />
            </label>
            {{ $slot }}
        </x-slot:content>

    </x-main>

    {{--  TOAST area --}}
    <x-toast />

</body>

</html>

Un composant pour le tableau de bord

Il ne nous manque plus que le composant pour notre tableau de bord :

php artisan make:volt admin/index --class

Ajoutez ce code :

<?php

use App\Models\{Comment, Page, Post, User};
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\{Layout, Title};
use Livewire\Volt\Component;
use Mary\Traits\Toast;

new #[Title('Dashboard')] #[Layout('components.layouts.admin')] class extends Component {
	use Toast;

	public array $headersPosts;
	public bool $openGlance = true;

	public function mount(): void
	{
		$this->headersPosts = [['key' => 'date', 'label' => __('Date')], ['key' => 'title', 'label' => __('Title')]];
	}

	public function deleteComment(Comment $comment): void
	{
		$comment->delete();

		$this->warning('Comment deleted', __('Good bye!'), position: 'toast-bottom');
	}

	public function with(): array
	{
		$user    = Auth::user();
		$isRedac = $user->isRedac();
		$userId  = $user->id;

		return [
			'pages'          => Page::select('id', 'title', 'slug')->get(),
			'posts'          => Post::select('id', 'title', 'slug', 'user_id', 'created_at', 'updated_at')->when($isRedac, fn (Builder $q) => $q->where('user_id', $userId))->latest()->get(),
			'commentsNumber' => Comment::when($isRedac, fn (Builder $q) => $q->whereRelation('post', 'user_id', $userId))->count(),
			'comments'       => Comment::with('user', 'post:id,title,slug')->when($isRedac, fn (Builder $q) => $q->whereRelation('post', 'user_id', $userId))->latest()->take(5)->get(),
			'users'          => User::count(),
		];
	}
}; ?>

<div>
    <x-collapse wire:model="openGlance" class="shadow-md">
        <x-slot:heading>
            @lang('In a glance')
        </x-slot:heading>
        <x-slot:content class="flex flex-wrap gap-4">

            <a href="#" class="flex-grow">
                <x-stat title="{{ __('Posts') }}" description="" value="{{ $posts->count() }}" icon="s-document-text"
                    class="shadow-hover" />
            </a>

            @if (Auth::user()->isAdmin())
                <a href="#" class="flex-grow">
                    <x-stat title="{{ __('Pages') }}" value="{{ $pages->count() }}" icon="s-document"
                        class="shadow-hover" />
                </a>
                <a href="#" class="flex-grow">
                    <x-stat title="{{ __('Users') }}" value="{{ $users }}" icon="s-user"
                        class="shadow-hover" />
                </a>
            @endif
            <a href="#" class="flex-grow">
                <x-stat title="{{ __('Comments') }}" value="{{ $commentsNumber }}" icon="c-chat-bubble-left"
                    class="shadow-hover" />
            </a>
        </x-slot:content>
    </x-collapse>

    <br>

    @foreach ($comments as $comment)
        @if (!$comment->user->valid)
            <x-alert title="{!! __('Comment to valid from ') . $comment->user->name !!}" description="{!! $comment->body !!}" icon="c-chat-bubble-left"
                class="shadow-md alert-warning">
                <x-slot:actions>
                    <x-button link="#" label="{!! __('Show the comments') !!}" />
                </x-slot:actions>
            </x-alert>
            <br>
        @endif
    @endforeach

    <x-collapse class="shadow-md">
        <x-slot:heading>
            @lang('Recent posts')
        </x-slot:heading>
        <x-slot:content>
            <x-table :headers="$headersPosts" :rows="$posts->take(5)" striped>
                @scope('cell_date', $post)
                    @lang('Created') {{ $post->created_at->diffForHumans() }}
                    @if ($post->updated_at != $post->created_at)
                        <br>
                        @lang('Updated') {{ $post->updated_at->diffForHumans() }}
                    @endif
                @endscope
                @scope('actions', $post)
                    <x-popover>
                        <x-slot:trigger>
                            <x-button icon="s-document-text" link="{{ route('posts.show', $post->slug) }}" spinner class="btn-ghost btn-sm" />                            
                        </x-slot:trigger>
                        <x-slot:content class="pop-small">
                            @lang('Show post')
                        </x-slot:content>
                    </x-popover>
                @endscope
            </x-table>
        </x-slot:content>
    </x-collapse>

    <br>

    <x-collapse class="shadow-md">
        <x-slot:heading>
            @lang('Recent Comments')
        </x-slot:heading>
        <x-slot:content>
            @foreach ($comments as $comment)
                <x-list-item :item="$comment" no-separator no-hover>
                    <x-slot:avatar>
                        <x-avatar :image="Gravatar::get($comment->user->email)">
                            <x-slot:title>
                                {{ $comment->user->name }}
                            </x-slot:title>
                        </x-avatar>
                    </x-slot:avatar>
                    <x-slot:value>
                        @lang ('in post:') {{ $comment->post->title }}
                    </x-slot:value>
                    <x-slot:actions>
                        <x-popover>
                            <x-slot:trigger>
                                <x-button icon="c-eye" link="#" spinner class="btn-ghost btn-sm" />                         
                            </x-slot:trigger>
                            <x-slot:content class="pop-small">
                                @lang('Edit or answer')
                            </x-slot:content>
                        </x-popover>
                        <x-popover>
                            <x-slot:trigger>
                                <x-button icon="s-document-text" link="{{ route('posts.show', $comment->post->slug) }}" spinner class="btn-ghost btn-sm" />                       
                            </x-slot:trigger>
                            <x-slot:content class="pop-small">
                                @lang('Show post')
                            </x-slot:content>
                        </x-popover>
                        <x-popover>
                            <x-slot:trigger>
                                <x-button icon="o-trash" wire:click="deleteComment({{ $comment->id }})"
                                    wire:confirm="{{ __('Are you sure to delete this comment?') }}" 
                                    spinner class="text-red-500 btn-ghost btn-sm" />                   
                            </x-slot:trigger>
                            <x-slot:content class="pop-small">
                                @lang('Delete')
                            </x-slot:content>
                        </x-popover>
                    </x-slot:actions>
                </x-list-item>
                <p class="ml-16">{!! Str::words(nl2br($comment->body), 20, ' ...') !!}</p>
                <br>
            @endforeach
        </x-slot:content>
    </x-collapse>
</div>

Le composant Table de MaryUI est une interface utilisateur qui permet d'afficher des données sous forme de tableau, nous allons en faire largement usage dans toute la partie administration.

Il offre une manière structurée et interactive de présenter des informations, souvent utilisée pour des listes de données, des rapports, ou des interfaces de gestion. Voici quelques caractéristiques et fonctionnalités typiques du composant Table dans MaryUI :

  • Affichage des Données : Permet de présenter des données sous forme de lignes et de colonnes.
  • Pagination : Gère l'affichage des données par pages, ce qui est utile pour les grands ensembles de données.
  • Tri : Permet aux utilisateurs de trier les colonnes par ordre croissant ou décroissant.
  • Filtrage : Offre des options pour filtrer les données en fonction de critères spécifiques.
  • Recherche : Intègre une fonctionnalité de recherche pour trouver rapidement des éléments spécifiques dans le tableau.
  • Sélection : Permet aux utilisateurs de sélectionner des lignes ou des colonnes pour des actions spécifiques.
  • Personnalisation : Offre des options pour personnaliser l'apparence et le comportement du tableau, comme les styles, les icônes, et les actions personnalisées.

Le composant Table de MaryUI est conçu pour être flexible et réutilisable, permettant aux développeurs de créer des interfaces utilisateur efficaces et interactives avec un minimum de configuration.

On ajoute la route :

use App\Http\Middleware\IsAdminOrRedac;

...

Route::middleware('auth')->group(function () {
	...
	Route::middleware(IsAdminOrRedac::class)->prefix('admin')->group(function () {
		Volt::route('/dashboard', 'admin.index')->name('admin');
	});
});

Avec l'URL /admin/dashboard, on arrive maintenant sur le tableau de bord :

On a besoin de quelques traductions :

"In a glance": "En un coup d'oeil",
"Recent posts": "Articles récents",
"Users": "Utilisateurs",
"Dashboard": "Tableau de bord",
"Recent Comments": "Commentaires récents",
"Show post": "Afficher l'article",
"in post:": "dans l'article :",
"Go on site": "Aller sur le site",
"Edit or answer": "Modifier ou repondre",
"Posts": "Articles",

C'est mieux :

On affiche sur ce tableau de bord quelques statistiques concernant les articles, les pages, les utilisateurs et les commentaires. Pour le moment, les liens ne mènent nulle part, nous complèterons cela lorsque nous coderons les gestions correspondantes.

On renseigne aussi sur les derniers articles et commentaires, un bon résumé de l'activité du CMS. Pour le faire, on utilise le composant Table de MaryUi :

Je détaillerai la création de ce type de tableau dans un prochain article.

La navigation

Il faut prévoir un accès au tableau de bord à partir de la barre de navigation. On va d'abord ajouter une fonction dans le modèle User pour simplifier notre code :

public function isAdminOrRedac(): bool
{
    return 'admin' === $this->role || 'redac' === $this->role;
}

Et dans la barre de navigation (navigation.navbar) :

<x-slot:actions>
    <span class="hidden lg:block">
        @if ($user = auth()->user())
            <x-dropdown>
                ...
                @if ($user->isAdminOrRedac())
                    <x-menu-item title="{{ __('Administration') }}" link="{{ route('admin') }}" />
                @endif
            </x-dropdown>
        @else

Maintenant, on peut accéder facilement à l'administration, du moins les rédacteurs et les administrateurs. Nous allons ajouter toutefois une dernière touche : lorsqu'un administrateur se connecte, c'est sans doute pour accéder à l'administration, alors on va compléter le code du composant auth.login pour établir la redirection vers le tableau de bord :

public function login()
{
    $credentials = $this->validate();

    if (auth()->attempt($credentials)) {
        request()->session()->regenerate();

        if (auth()->user()->isAdmin()) {
            return redirect()->intended('/admin/dashboard');
        }

        return redirect()->intended('/');
    }

    $this->addError('email', __('The provided credentials do not match our records.'));
}

Conclusion

Notre administration est désormais en place, dans les prochains articles, on complètera avec la gestion du contenu du CMS ainsi que des paramètres de fonctionnement.

Pour vous simplifier la vie, vous pouvez charger le projet dans son état à l’issue de ce chapitre.



Par bestmomo

Aucun commentaire

Article précédent : Mon CMS - Les favoris
Article suivant : Mon CMS - Tableau des articles