Chapter 11 of 15

A Laravel backend

The notes Freddy built so far are his own, on his Mac. His team's notes live in the Laravel app from Freddy the Notetaker. From here on, the desktop app is that app's desktop half: same users, same API, same validation.

The idea

In the guide's words, an Elyra app can be the desktop half of a product whose web half is Laravel. It signs the user in with a Sanctum token, calls the same API as the web, and shows Laravel's validation messages in its forms as if they were its own. Freddy's private notebook from chapters 5 to 7 stays in the local database; his team's notes come from the server. The feature is backend:

elyra = { git = "https://github.com/kwhorne/elyra-framework", tag = "v0.16.0",
          features = ["database", "backend"] }
App::new().backend(Backend::new("https://notes.example.com"))

The Laravel side

On the web app, Freddy needs Sanctum and three routes in routes/api.php: one that issues a token, one that revokes it and one that returns the user. The guide gives them in full:

php artisan install:api      # Sanctum; then add HasApiTokens to App\Models\User
Route::post('/sanctum/token', function (Request $request) {
    $request->validate(['email' => 'required|email', 'password' => 'required', 'device_name' => 'required']);
    $user = User::where('email', $request->email)->first();
    if (! $user || ! Hash::check($request->password, $user->password)) {
        throw ValidationException::withMessages(['email' => ['The provided credentials are incorrect.']]);
    }
    return $user->createToken($request->device_name)->plainTextToken;
});

Route::middleware('auth:sanctum')->group(function () {
    Route::delete('/sanctum/token', fn (Request $r) => $r->user()->currentAccessToken()->delete());
    Route::get('/user', fn (Request $r) => $r->user());
    Route::apiResource('notes', NoteController::class);
});

Different paths are fine: token_route, revoke_route and user_route on Backend. Since 0.16.0 the repository also has a Composer package, elyracode/elyra-backend, with these routes as Route::elyraAuth() and the middleware later chapters need; the 0.16.0 changelog says it is not published yet, so this course writes the routes by hand.

Signing in

import { auth, onSignedOut, ValidationError } from "@elyra/runtime";

try {
  await auth.signIn(email, password);
} catch (e) {
  if (e instanceof ValidationError) errors = e.errors;   // { email: ["…"] }
}
$auth;                                                    // { signedIn, user, reason }
onSignedOut((reason) => reason === "expired" && toast("Please sign in again"));

Three things make this safe, and they are chapter 1's untrusted page again:

  • The token lives in the OS keychain only, never in the webview or a file, and is restored when the app starts. With no keychain, sign-in fails and the token the server just issued is revoked: there is no plain-text fallback.
  • The device is named after the machine and the app, so Freddy recognizes it among his tokens on the web.
  • A 401 from any request signs the user out, and the page hears expired.

Calling the API

let backend = ctx.get::<Backend>();
let page: Page<TeamNote> = backend.get("/api/notes").query(&query).json().await?;
let note: TeamNote       = backend.get(format!("/api/notes/{id}")).resource().await?;
let created: TeamNote    = backend.post("/api/notes").body(&input).resource().await?;

The token goes as Bearer, and every request asks for JSON, which is what makes Laravel answer a failed validation with a 422 instead of a redirect. .resource() unwraps an API Resource's data, and Page<T> reads both of Laravel's paginators. Under it is Http, Laravel's HTTP facade in Rust: a 30-second timeout by default, retries for idempotent methods, HTTPS only except to loopback, and a redirect to another host does not carry the token.

Laravel's answers, as kinds

LaravelThe page sees
422 { errors }a ValidationError, per field, as from a local command
401, 419kind unauthenticated; the token is forgotten
403 { message }kind denied, with Laravel's message
404kind not-found
409, 412kind conflict (chapter 13)
429 + Retry-Afterkind too-many-requests, saying when
5xxkind server, without the error page
no answerkind offline

? in a command turns each into the right error, so Freddy's UI can say "you're offline" instead of showing an error, and a 422 from his Laravel StoreNoteRequest lands under the field in the desktop form, just as chapter 6's local validation did.

Live queries over the API

Chapter 7 carries over. Inside a live command a GET depends on its resource, the path without the query and trailing ids, so /api/notes?page=2 and /api/notes/12 are both the notes resource, and a successful write to it re-runs them. That covers what this app writes. What a colleague changes on the web is chapter 12.

Or generate all of it

rata make:resource TeamNote --backend /api/notes --generate title:string body:text 'pinned:bool?'

That is chapter 6's resource, with the same screens, types, abilities and events, except that its commands call the API. There is no migration or seeder, because the table is the server's, and validation is the server's too. rata prints what the Laravel side needs, and the tests run against an HttpFake. For now the project needs the database feature as well, because the resource registry also lists migrations.

The guides are A Laravel backend and HTTP client.

What you learned

  • Backend calls a Laravel API with the user's Sanctum token; Http under it is Laravel's HTTP facade in Rust.
  • Three routes on the Laravel side; elyracode/elyra-backend has them too, but is not published yet.
  • The token lives in the keychain only, and a 401 anywhere signs the user out.
  • Laravel's answers arrive as kinds, and a 422 lands in the form per field.
  • make:resource --backend generates a resource whose commands call the API.
Next: in Chapter 12 Changes made on the web show up in Freddy's open windows.