Skip to content

Handling secret state #184

@nileshtrivedi

Description

@nileshtrivedi

The README mentions the caveat about LiveSvelte sending the whole state as JSON.

But the secrets worth keeping are typically in the assigns, and not in templates. I think the get_props method should simply filter any assigns whose name begins with, say, secret_ and therefore not send them to the client at all.

I made a simple word-guessing game:

defmodule ExampleWeb.LiveSigil do
  use ExampleWeb, :live_view

  def render(assigns) do
    ~V"""
    <script>
      import { onMount } from 'svelte';

      // props sent from the server
      let { shown_word, leaderboard, live } = $props();

      // Client-Side State using Runes
      let name = $state('');
      let user_guess = $state(''); // e.g., "ACB"
      let status_message = $state(''); // Feedback from the server
      let error_message = $state(''); // Client-side validation errors

      onMount(() => {
        live.handleEvent("feedback", (payload) => {
          // This will be called when the server pushes feedback
          status_message = payload.message;
        });
      });

      function handleSubmit(){
              // --- Client-Side Validation ---
              // Assignments to $state variables automatically trigger updates.
              error_message = '';
              status_message = '';

              if (name.trim() === '') {
                error_message = 'Please enter your name.';
                return;
              }

              if (user_guess.length != shown_word.length) {
                error_message = 'Word length is not correct.';
                return;
              }

              if (user_guess.split('').sort().join('') != shown_word.split('').sort().join('')) {
                error_message = 'Letters are not correct.';
                return;
              }

              // `pushEvent` sends the data to the Phoenix LiveView.
              live.pushEvent("submit_answer", {name: name, attempt: user_guess});
      }
    </script>

    <h1 class="text-2xl font-bold mb-4">Sorting Guessing Game</h1>

    <p class="prose">The server has put these letters <b>{shown_word}</b> in some random order which you need to guess. Correct guess earns you 10 points.</p>

    <!-- Guess Word -->
    <div class="space-y-4 mb-6">
        <div class="flex items-center space-x-4 bg-gray-50 p-3 rounded-lg">

          <input
            id="name"
            type="text"
            class="w-48 p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
            placeholder="Your name"
            bind:value={name}
          />
          <input
            type="text"
            min="1"
            max="{shown_word.length}"
            class="w-48 p-2 text-center border border-gray-300 rounded-lg shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
            placeholder="Your guess"
            bind:value={user_guess}
          />

          <button
            class="w-full sm:w-auto px-6 py-3 bg-indigo-600 text-white font-bold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
            onclick={handleSubmit}
          >
            Submit Answer
          </button>
        </div>
    </div>


    <!-- Status/Error Messages -->
    {#if status_message}
      <div class="mt-4 p-3 rounded-lg text-center" class:bg-green-100={status_message.includes('Correct')} class:text-green-800={status_message.includes('Correct')} class:bg-red-100={!status_message.includes('Correct')} class:text-red-800={!status_message.includes('Correct')}>
        {status_message}
      </div>
    {/if}

    {#if error_message}
      <div class="mt-4 p-3 rounded-lg bg-yellow-100 text-yellow-800 text-center">
        {error_message}
      </div>
    {/if}

    <!-- Leaderboard Section -->
    <div class="bg-white p-6 sm:p-8 rounded-2xl shadow-lg">
      <h2 class="text-2xl font-bold text-gray-800 mb-4">Leaderboard</h2>
      <div class="space-y-3">
        {#if Object.keys(leaderboard).length > 0}
          <!-- Sort leaderboard by score descending -->
          {#each Object.entries(leaderboard).sort((a, b) => b[1] - a[1]) as [name, score]}
            <div class="flex justify-between items-center bg-gray-50 p-3 rounded-lg">
              <span class="font-medium text-gray-700">{name}</span>
              <span class="font-bold text-indigo-600">{score} pts</span>
            </div>
          {/each}
        {:else}
          <p class="text-gray-500">No scores yet. Be the first!</p>
        {/if}
      </div>
    </div>

    """
  end

  @doc """
  Mounts the LiveView, initializing the server-side state.
  """
  def mount(_params, _session, socket) do
    # On mount, we shuffle the items and set the initial state.
    new_secret_word = Enum.shuffle(String.split("ABC","", trim: true)) |> Enum.join

    socket =
      assign(socket,
        secret_word: new_secret_word,
        shown_word: "ABC",
        leaderboard: %{}
      )

    {:ok, socket}
  end

  @doc """
  Handles the `submit_answer` event pushed from the Svelte client.
  """
  def handle_event("submit_answer", %{"name" => name, "attempt" => attempt}, socket) do
    # All game logic happens securely on the server.
    if socket.assigns.secret_word == attempt do
      # Correct Answer
      new_leaderboard = Map.update(socket.assigns.leaderboard, name, 10, &(&1 + 10))

      # Push a feedback event to the client.
      socket = push_event(socket, "feedback", %{message: "Correct! You earned 10 points."})

      # Reset the game for a new round
      new_secret_word = Enum.shuffle(String.split("ABC","", trim: true)) |> Enum.join

      new_socket =
        assign(socket,
          secret_word: new_secret_word,
          shown_word: "ABC",
          leaderboard: new_leaderboard
        )
      {:noreply, new_socket}
    else
      # Incorrect Answer
      socket = push_event(socket, "feedback", %{message: "Not quite, try again!"})
      {:noreply, socket}
    end
  end

end

As we can see in the browser devtools, all assigns are sent, including secret_word:

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions