Retour

Obsidian_

/

documentation_

Tout ce que tu dois savoir pour démarrer avec Obsidian.

Installation

1. Clone le repo
git clone https://github.com/kainovaii/obsidian-skeleton.git
cd obsidian-skeleton
2. Lance l'application
./build.bat
⚠️ Prérequis
  • • Java 17 ou supérieur
  • • Maven 3.6+
  • • MySQL/PostgreSQL/SQLite

Routing avec annotations

Fini les routes déclarées manuellement. Utilise les annotations directement sur tes méthodes.

Annotations disponibles
@GET(value = "/articles", name = "articles.index")
@POST(value = "/articles", name = "articles.store")
@PUT(value = "/articles/:id", name = "articles.update")
@DELETE(value = "/articles/:id", name = "articles.delete")
@PATCH(value = "/articles/:id", name = "articles.patch")
Paramètres de route
@GET(value = "/articles/:id", name = "articles.show")
private Object show(Request req, Response res) {
    String id = req.params(":id");
    // ...
}
Query parameters
@GET(value = "/search", name = "search")
private Object search(Request req, Response res) {
    String query = req.queryParams("q");
    String page = req.queryParams("page");
    // ...
}

Middleware System

Intercepte les requêtes avant et après l'exécution des controllers avec des annotations simples.

Utilisation basique
@Before(LoggingMiddleware.class)
@GET(value = "/dashboard", name = "dashboard")
private Object dashboard(Request req, Response res) {
    return render("dashboard.html", Map.of());
}
Chaîner plusieurs middlewares
@Before({CorsMiddleware.class, ApiKeyMiddleware.class, RateLimitMiddleware.class})
@GET(value = "/api/data", name = "api.data")
private Object data(Request req, Response res) {
    return "{ \"data\": [] }";
}
Before et After
@Before(TimingMiddleware.class)
@After(TimingMiddleware.class)
@GET(value = "/profile", name = "profile")
private Object profile(Request req, Response res) {
    // TimingMiddleware mesure le temps d'exécution
    return render("profile.html", Map.of());
}
📦 Middlewares Built-in
LoggingMiddleware
Log automatique de chaque requête avec IP et User-Agent.
Utile pour: Debug, monitoring, audit trail
CorsMiddleware
Ajoute les headers CORS pour autoriser les requêtes cross-origin.
Utile pour: APIs publiques, frontends séparés, apps mobiles
RateLimitMiddleware
Limite à 100 requêtes/minute par IP. Retourne 429 si dépassé.
Utile pour: Protection anti-spam, anti-DDoS, endpoints publics
TimingMiddleware
Mesure le temps d'exécution et ajoute le header X-Response-Time.
Utile pour: Optimisation performance, détection de routes lentes
ApiKeyMiddleware
Vérifie la présence du header X-API-Key. Retourne 401 si invalide.
Utile pour: Sécuriser les APIs, authentification simple
Exemple: Sécuriser une API
@Before({CorsMiddleware.class, ApiKeyMiddleware.class, RateLimitMiddleware.class})
@GET(value = "/api/users", name = "api.users")
private Object users(Request req, Response res) {
    // 1. CORS headers ajoutés
    // 2. API Key validée (sinon 401)
    // 3. Rate limit vérifié (sinon 429)
    // 4. Controller exécuté

    return "{ \"users\": [] }";
}
Créer un middleware custom
public class MaintenanceMiddleware implements Middleware {

    @Override
    public void handle(Request req, Response res) throws Exception {
        boolean isMaintenance = System.getenv("MAINTENANCE_MODE") != null;

        if (isMaintenance && !req.pathInfo().equals("/maintenance")) {
            res.redirect("/maintenance");
            throw new Exception("Maintenance mode");
        }
    }
}

// Utilisation
@Before(MaintenanceMiddleware.class)
@GET("/dashboard")
private Object dashboard(Request req, Response res) {
    return render("dashboard.html", Map.of());
}
Headers ajoutés automatiquement
# TimingMiddleware
X-Response-Time: 42ms

# RateLimitMiddleware
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87

# CorsMiddleware
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-TOKEN
Logs générés (LoggingMiddleware)
INFO: GET /articles - IP: 192.168.1.1 - User-Agent: Mozilla/5.0...
INFO: POST /api/users - IP: 10.0.0.5 - User-Agent: PostmanRuntime/7.26.8
INFO: DELETE /articles/42 - IP: 192.168.1.1 - User-Agent: Mozilla/5.0...
💡 Ordre d'exécution
  • • @Before middlewares s'exécutent dans l'ordre du tableau
  • • Controller s'exécute ensuite
  • • @After middlewares s'exécutent après le controller
  • • Si un middleware lance une exception, l'exécution s'arrête
  • • Les middlewares sont instanciés une seule fois (singleton)

Controllers

Tous tes controllers doivent hériter de BaseController et porter l'annotation @Controller.

Controller basique
@Controller
public class ArticleController extends BaseController {

    @GET(value = "/articles", name = "articles.index")
    private Object index(Request req, Response res) {
        return render("articles/index.html", Map.of(
            "title", "Mes articles"
        ));
    }

    @POST(value = "/articles", name = "articles.store")
    private Object store(Request req, Response res) {
        String title = req.queryParams("title");

        Article article = new Article();
        article.set("title", title);
        article.saveIt();

        res.redirect("/articles");
        return null;
    }
}
Injection de dépendances
@GET(value = "/articles", name = "articles.index")
private Object index(ArticleRepository articleRepo) {
    List<Article> articles = DB.withConnection(() ->
        articleRepo.findAll().stream().toList()
    );

    return render("articles/index.html", Map.of(
        "articles", articles
    ));
}
💡 Astuce

L'injection fonctionne avec n'importe quelle classe annotée @Repository. Tu peux injecter autant de dépendances que nécessaire.

Models ActiveJDBC

Les models utilisent ActiveJDBC, un ORM léger qui suit le pattern ActiveRecord.

Créer un model
public class Article extends Model {

    // Getters
    public String getTitle() {
        return getString("title");
    }

    public String getContent() {
        return getString("content");
    }

    public Integer getStatus() {
        return getInteger("status");
    }

    // Setters
    public void setTitle(String title) {
        set("title", title);
    }

    public void setContent(String content) {
        set("content", content);
    }

    public void setStatus(Integer status) {
        set("status", status);
    }
}
Utilisation
// Créer
Article article = new Article();
article.setTitle("Mon titre");
article.setContent("Mon contenu");
article.saveIt();

// Récupérer
Article article = Article.findById(1);
LazyList<Article> articles = Article.findAll();
LazyList<Article> published = Article.where("status = ?", 1);

// Modifier
article.setTitle("Nouveau titre");
article.saveIt();

// Supprimer
article.delete();
Relations
public class Article extends Model {

    // Un article appartient à un user
    public User getAuthor() {
        return parent(User.class);
    }

    // Un article a plusieurs commentaires
    public LazyList<Comment> getComments() {
        return get(Comment.class, "article_id = ?", getId());
    }
}

Migrations

Un système de migrations fluide inspiré de Laravel pour gérer ta structure de base de données.

Créer une table
public class CreateArticlesTable extends Migration {

    @Override
    public void up() {
        createTable("articles", table -> {
            table.id();
            table.string("title").notNull();
            table.text("content").notNull();
            table.integer("status").defaultValue(1);
            table.integer("user_id");
            table.timestamps();
        });
    }

    @Override
    public void down() {
        dropTable("articles");
    }
}
Types de colonnes disponibles
table.id()                              // PRIMARY KEY AUTO_INCREMENT
table.string("name")                    // VARCHAR(255)
table.string("code", 10)                // VARCHAR(10)
table.text("content")                   // TEXT
table.integer("count")                  // INT
table.bigInteger("big_count")           // BIGINT
table.decimal("price", 10, 2)           // DECIMAL(10,2)
table.boolean("active")                 // BOOLEAN
table.date("birth_date")                // DATE
table.datetime("event_at")              // DATETIME
table.timestamp("logged_at")            // TIMESTAMP
table.timestamps()                      // created_at + updated_at
Modificateurs
table.string("email").notNull()
table.integer("count").defaultValue(0)
table.string("code").unique()
table.integer("user_id").foreignKey("users", "id")

Repositories

Sépare ta logique de données avec le pattern Repository.

Créer un repository
@Repository
public class ArticleRepository {

    public LazyList<Article> findAll() {
        return Article.findAll();
    }

    public LazyList<Article> findPublished() {
        return Article.where("status = ?", 1);
    }

    public LazyList<Article> findByAuthor(Integer userId) {
        return Article.where("user_id = ?", userId);
    }

    public Article findById(Integer id) {
        return Article.findById(id);
    }

    public void create(String title, String content) {
        Article article = new Article();
        article.setTitle(title);
        article.setContent(content);
        article.setStatus(1);
        article.saveIt();
    }
}
💡 Bonne pratique

Utilise les repositories pour toute logique complexe de récupération de données. Ça garde tes controllers propres et facilite les tests.

Templates Pebble

Utilise Pebble pour tes vues. Syntaxe simple et puissante.

Template de base
{% extends "layout.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
    <h1>{{ title }}</h1>

    {% for article in articles %}
    <article>
        <h2>{{ article.title }}</h2>
        <p>{{ article.content }}</p>
    </article>
    {% endfor %}
{% endblock %}
Render depuis un controller
return render("articles/index.html", Map.of(
    "title", "Mes articles",
    "articles", articles
));
Filtres utiles
{{ article.title | upper }}{{ article.content | truncate(100) }}{{ article.createdAt | date("Y-m-d") }}{{ article.price | number_format }}

Configuration Database

Utilisation dans le code
// Toutes les opérations DB doivent être wrapped
List<Article> articles = DB.withConnection(() ->
    Article.findAll().stream().toList()
);

// Ou pour plusieurs opérations
DB.withConnection(() -> {
    Article article = new Article();
    article.setTitle("Test");
    article.saveIt();

    return article;
});

Security & Authentication

Le système d'authentification est basé sur UserDetailsService, une abstraction qui te laisse libre d'organiser ton modèle User comme tu veux.

1. Créer ton UserDetailsService
@UserDetailsServiceImpl
public class AppUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public AppUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadByUsername(String username) {
        return DB.withConnection(() -> {
            if (!UserRepository.userExist(username)) return null;
            User user = userRepository.findByUsername(username);
            return adapt(user);
        });
    }

    @Override
    public UserDetails loadById(Object id) {
        return DB.withConnection(() -> {
            User user = User.findById(id);
            return adapt(user);
        });
    }

    private UserDetails adapt(User user) {
        if (user == null) return null;

        return new UserDetails() {
            public Object getId() { return user.getId(); }
            public String getUsername() { return user.getUsername(); }
            public String getPassword() { return user.getPassword(); }
            public String getRole() { return user.getRole(); }
            public boolean isEnabled() { return true; }
        };
    }
}
2. Utiliser dans tes controllers
@Controller
public class LoginController extends BaseController {

    @POST("/users/login")
    private Object loginBack(Request req, Response res) {
        String username = req.queryParams("username");
        String password = req.queryParams("password");
        Session session = req.session(true);

        if (login(username, password, session)) {
            res.redirect("/dashboard");
            return true;
        }

        return redirectWithFlash(req, res, "error",
            "Incorrect login", "/users/login");
    }

    @HasRole("DEFAULT")
    @GET("/users/logout")
    private Object logout(Request req, Response res) {
        logout(req.session(true));
        return redirectWithFlash(req, res, "success",
            "Logged out", "/users/login");
    }
}
Méthodes disponibles dans BaseController
// Authentification
login(username, password, session)  // Authentifie et crée la session
logout(session)                     // Déconnecte l'utilisateur

// Vérifications
isLogged(req)                       // Vérifie si connecté
hasRole(req, "ADMIN")               // Vérifie un rôle
requireLogin(req, res)              // Force la connexion (redirige sinon)

// Récupération
getLoggedUser(req)                  // Récupère le UserDetails
Protection par rôle
@HasRole("ADMIN")
@GET("/admin/dashboard")
private Object adminDashboard(Request req, Response res) {
    // Accessible uniquement aux ADMIN
    return render("admin/dashboard.html", Map.of());
}

@HasRole("DEFAULT")
@GET("/profile")
private Object profile(Request req, Response res) {
    // Accessible à tous les utilisateurs connectés
    UserDetails user = getLoggedUser(req);
    return render("profile.html", Map.of("user", user));
}
Ajouter des champs personnalisés
// 1. Créer ton interface custom
public interface AppUserDetails extends UserDetails {
    String getEmail();
    String getIp();
}

// 2. Adapter vers cette interface
private AppUserDetails adapt(User user) {
    if (user == null) return null;
    return new AppUserDetails() {
        public Object getId() { return user.getId(); }
        public String getUsername() { return user.getUsername(); }
        public String getPassword() { return user.getPassword(); }
        public String getRole() { return user.getRole(); }

        // Tes champs custom
        public String getEmail() { return user.getEmail(); }
        public String getIp() { return user.getIp(); }
    };
}

// 3. Utiliser dans les controllers
@GET("/profile")
private Object profile(Request req, Response res) {
    AppUserDetails user = getLoggedUser(req);
    String email = user.getEmail();  // ✅ Disponible !

    return render("profile.html", Map.of("user", user));
}
💡 Astuce

L'annotation @UserDetailsServiceImpl permet au framework de détecter automatiquement ton implémentation. Pas besoin de config manuelle !

Le rôle "DEFAULT" signifie "n'importe quel utilisateur connecté".

CSRF Protection

Protection automatique contre les attaques Cross-Site Request Forgery.

Activer la protection sur une route
@CsrfProtect
@POST(value = "/articles", name = "articles.store")
private Object store(Request req, Response res) {
    // Token validé automatiquement
    // Si invalide → 403 Forbidden
}
Dans les formulaires HTML
<form method="POST" action="/articles">
    {{ csrf_field() | raw }}
    <input type="text" name="title">
    <button type="submit">Create</button>
</form>
Pour les requêtes AJAX
const csrfToken = '{{ csrf_token() }}';

fetch('/articles', {
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': csrfToken,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ title: 'My article' })
});
Régénérer le token (après login)
@POST("/login")
private Object login(Request req, Response res) {
    if (isLogged(req)) {
        regenerateCsrfToken(req);
        res.redirect("/dashboard");
    }
    return null;
}
💡 Comportement
  • • Les requêtes GET/HEAD/OPTIONS ne sont jamais protégées
  • • POST/PUT/DELETE/PATCH nécessitent l'annotation @CsrfProtect
  • • Le token expire après 1 heure d'inactivité
  • • Le token peut être envoyé via header X-CSRF-TOKEN ou champ _csrf

Flash Messages

Affiche des notifications temporaires après des actions utilisateur. Aucune dépendance externe requise.

Utilisation dans un controller
@Post("/login")
public Object login(Request req, Response res)
{
    String username = req.queryParams("username");
    String password = req.queryParams("password");

    if (login(username, password, req.session())) {
        return redirectWithFlash(req, res, "success", "Welcome back!", "/dashboard");
    }

    return redirectWithFlash(req, res, "error", "Invalid credentials", "/login");
}
Types de notifications disponibles
// Success (vert/orange)
redirectWithFlash(req, res, "success", "Operation completed!", "/");

// Error (rouge)
redirectWithFlash(req, res, "error", "Something went wrong", "/");

// Info (bleu)
redirectWithFlash(req, res, "info", "Check your email", "/");

// Warning (jaune/orange)
redirectWithFlash(req, res, "warning", "Changes require restart", "/");
Affichage dans le template
<!DOCTYPE html>
<html>
<body>
    <nav>...</nav>

    {% block content %}{% endblock %}
    {{ flash() }}
</body>
</html>
Configuration personnalisée (optionnel)
// Dans Main.java
public static void configureFlash()
{
    // Changer la durée (en ms)
    FlashConfig.setDuration(5000);

    // Changer la position
    FlashConfig.setPosition("top-right");
    // Options: top-right, top-left, bottom-right,
    //          bottom-left, top-center, bottom-center

    // CSS personnalisé
    FlashConfig.setCustomCSS("""
        .flash-notification {
            border-radius: 1rem;
            font-size: 0.875rem;
        }
        .flash-success {
            background: linear-gradient(to right, #10b981, #059669);
        }
        """);
}