State, auth & handlers

The module does not validate JWTs itself. The core authenticates the request then proxies it, injecting the identity into headers; a middleware reads them and stores them in the request extensions.

The state #

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

The authentication middleware #

// 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

Headers injected by the proxy: x-kubuno-user-id, x-kubuno-user-role, x-kubuno-user-email (and X-Internal-Secret instead of the Authorization header).

A 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 })))
}

Axum extractors do the work: State provides the state, Extension fetches the user set by the middleware, and ? propagates errors.

Reading the user & responding — in other languages #

Whatever the language, you read the same headers and filter by 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 });
};