Tutoriel backend — « Memo »

État, auth & handlers

Le module ne valide pas les JWT lui-même. Le core authentifie la requête puis la proxifie en injectant l'identité dans des en-têtes ; un middleware les lit et les pose dans les extensions de la requête.

L'état #

// state.rs
#[derive(Clone)]
pub struct AppState {
    pub db:       sqlx::PgPool,
    pub settings: std::sync::Arc<Settings>,
}

Le middleware d'authentification #

// middleware.rs — lit l'identité injectée par le proxy du core
#[derive(Clone)]
pub struct MemoUser { pub id: Uuid, pub role: String, pub email: String }

pub async fn require_auth(mut req: Request, next: Next) -> Result<Response, MemoError> {
    let h = req.headers();
    let id = h.get("x-kubuno-user-id")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| Uuid::parse_str(s).ok())
        .ok_or(MemoError::Unauthorized)?;
    let role  = h.get("x-kubuno-user-role").and_then(|v| v.to_str().ok()).unwrap_or("user").to_string();
    let email = h.get("x-kubuno-user-email").and_then(|v| v.to_str().ok()).unwrap_or("").to_string();
    req.extensions_mut().insert(MemoUser { id, role, email });
    Ok(next.run(req).await)
}
Note

En-têtes injectés par le proxy : x-kubuno-user-id, x-kubuno-user-role, x-kubuno-user-email (et X-Internal-Secret à la place de l'Authorization).

Un handler #

// handlers/notes.rs
pub async fn list(
    State(state): State<AppState>,
    Extension(user): Extension<MemoUser>,
) -> Result<Json<serde_json::Value>> {
    let notes = sqlx::query_as!(
        Note,
        "SELECT * FROM memo.notes WHERE owner_id = $1 ORDER BY pinned DESC, updated_at DESC",
        user.id
    )
    .fetch_all(&state.db)
    .await?;
    Ok(Json(json!({ "notes": notes })))
}

Les extracteurs Axum font le travail : State donne l'état, Extension récupère l'utilisateur posé par le middleware, ? propage l'erreur.

Lire l'utilisateur & répondre — dans d'autres langages #

Quel que soit le langage, on lit les mêmes en-têtes et on filtre par owner_id :

// Rust — Axum : l'identité est dans les en-têtes injectés par le proxy
async fn list(headers: HeaderMap, State(db): State<PgPool>) -> impl IntoResponse {
    let user_id = headers.get("x-kubuno-user-id")
        .and_then(|v| v.to_str().ok()).unwrap_or("");
    let notes = sqlx::query_as::<_, Note>(
        "SELECT * FROM memo.notes WHERE owner_id = $1::uuid")
        .bind(user_id).fetch_all(&db).await.unwrap_or_default();
    Json(json!({ "notes": notes }))
}
<?php
// PHP : les en-têtes HTTP deviennent HTTP_X_KUBUNO_*
$userId = $_SERVER['HTTP_X_KUBUNO_USER_ID']   ?? '';
$role   = $_SERVER['HTTP_X_KUBUNO_USER_ROLE'] ?? 'user';

$stmt = $pdo->prepare('SELECT * FROM memo.notes WHERE owner_id = :uid');
$stmt->execute([':uid' => $userId]);

header('Content-Type: application/json');
echo json_encode(['notes' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
# Python — Flask
@app.get("/notes")
def list_notes():
    user_id = request.headers.get("X-Kubuno-User-Id", "")
    rows = db.execute(
        "SELECT id, title FROM memo.notes WHERE owner_id = %s", (user_id,)
    ).fetchall()
    return jsonify(notes=[dict(r) for r in rows])
// Go — net/http
func list(w http.ResponseWriter, r *http.Request) {
    userID := r.Header.Get("X-Kubuno-User-Id")
    rows, _ := db.Query(`SELECT id, title FROM memo.notes WHERE owner_id = $1`, userID)
    defer rows.Close()
    notes := scanNotes(rows)
    json.NewEncoder(w).Encode(map[string]any{"notes": notes})
}
# Perl — Mojolicious::Lite
get '/notes' => sub ($c) {
    my $user_id = $c->req->headers->header('X-Kubuno-User-Id') // '';
    my $rows = $dbh->selectall_arrayref(
        'SELECT id, title FROM memo.notes WHERE owner_id = ?',
        { Slice => {} }, $user_id);
    $c->render(json => { notes => $rows });
};