rustlavel-cli 0.7.0

The rustlavel command-line tool: new, serve, make:*
@extends("layouts.app")
@section("title", "Security")

@section("content")
  @include("partials.errors")

  <div class="mx-auto max-w-3xl space-y-6">
    {{-- Authenticator app --}}
    <div class="card">
      <div class="card-header flex flex-wrap items-center justify-between gap-3">
        <div>
          <h3 class="font-semibold">Authenticator app</h3>
          <p class="mt-0.5 text-sm text-ink-500 dark:text-ink-400">A six-digit code from an app on your phone.</p>
        </div>
        @if(totp_enabled)<span class="badge-success">On</span>@else<span class="badge-neutral">Off</span>@endif
      </div>
      <div class="card-body">
        @if(totp_enabled)
          <p class="text-sm text-ink-600 dark:text-ink-400">Enabled {{ totp_enabled_at }}.</p>
          <form method="post" action="/settings/security/totp/disable" class="mt-4"
                data-confirm="Turn off the authenticator app? Your account will be protected by a password alone.">
            {!! csrf_field !!}
            <div class="max-w-sm">
              <label for="totp_password" class="field-label">Confirm with your password</label>
              <input id="totp_password" name="password" type="password" required autocomplete="current-password" class="field-input">
            </div>
            <button type="submit" class="btn-danger mt-4">Turn off</button>
          </form>
        @elseif(totp_pending)
          <div class="grid gap-6 sm:grid-cols-[auto_1fr]">
            <div class="mx-auto">
              {{-- The QR is rendered server-side as SVG. Loading a JavaScript
                   library would need a looser CSP, and a chart API would mean
                   posting the secret to a third party. --}}
              <div class="rounded-lg border border-ink-200 bg-white p-3 dark:border-ink-700">{!! totp_qr !!}</div>
            </div>
            <div>
              <ol class="space-y-3 text-sm text-ink-600 dark:text-ink-400">
                <li><span class="font-medium text-ink-900 dark:text-ink-100">1.</span> Scan the code with your authenticator app.</li>
                <li>
                  <span class="font-medium text-ink-900 dark:text-ink-100">2.</span> Cannot scan? Enter this key by hand:
                  <span class="mt-1.5 flex items-center gap-2">
                    <code class="select-all rounded bg-ink-100 px-2 py-1 font-mono text-xs dark:bg-ink-800">{{ totp_secret }}</code>
                    <button type="button" class="btn-ghost btn-sm" data-copy="{{ totp_secret }}">Copy</button>
                  </span>
                </li>
                <li><span class="font-medium text-ink-900 dark:text-ink-100">3.</span> Enter the code it shows.</li>
              </ol>
              <form method="post" action="/settings/security/totp/confirm" class="mt-5" novalidate>
                {!! csrf_field !!}
                <div class="max-w-[12rem]">
                  <label for="code" class="field-label">Code</label>
                  <input id="code" name="code" type="text" required inputmode="numeric" pattern="[0-9]*"
                         maxlength="6" autocomplete="one-time-code" data-otp-input
                         class="field-input text-center font-mono text-xl tracking-[0.3em]
                                @if(error_code) field-input-invalid @endif">
                  @if(error_code)<p class="field-error">{{ error_code }}</p>@endif
                </div>
                <button type="submit" class="btn-primary mt-4">Confirm and turn on</button>
              </form>
            </div>
          </div>
        @else
          <form method="post" action="/settings/security/totp/start">
            {!! csrf_field !!}
            <button type="submit" class="btn-primary">Set up</button>
          </form>
        @endif
      </div>
    </div>

    {{-- Passkeys --}}
    <div class="card">
      <div class="card-header flex flex-wrap items-center justify-between gap-3">
        <div>
          <h3 class="font-semibold">Passkeys</h3>
          <p class="mt-0.5 text-sm text-ink-500 dark:text-ink-400">
            Your fingerprint, face or security key. Cannot be phished, because the browser checks the site for you.
          </p>
        </div>
        @if(passkeys_empty == false)<span class="badge-success">{{ passkey_count }} registered</span>@endif
      </div>
      <div class="card-body">
        @if(passkeys_empty)
          <p class="mb-4 text-sm text-ink-500 dark:text-ink-400">None registered.</p>
        @else
          <ul class="mb-4 divide-y divide-ink-200 dark:divide-ink-800">
            @foreach(passkeys as key)
              <li class="flex flex-wrap items-center justify-between gap-3 py-3">
                <div class="min-w-0">
                  <p class="truncate text-sm font-medium">{{ key.label }}</p>
                  <p class="text-xs text-ink-500 dark:text-ink-400">Added {{ key.created_at }} &middot; last used {{ key.last_used_at }}</p>
                </div>
                {{-- Removing a passkey asks for one first. The script fills
                     `assertion` in after the browser has proved you hold a
                     passkey on this account; with scripting off it stays
                     empty, the server refuses, and nothing is removed. --}}
                <form method="post" action="/settings/security/passkeys/{{ key.id }}/delete"
                      data-passkey-confirm data-allow-resubmit
                      data-confirm-url="/settings/security/passkeys/confirm">
                  {!! csrf_field !!}
                  <input type="hidden" name="assertion" value="">
                  <button type="submit" class="btn-ghost btn-sm text-red-600 dark:text-red-400">Remove</button>
                </form>
              </li>
            @endforeach
          </ul>
        @endif
        <button type="button" class="btn-secondary" data-passkey-register
                data-options-url="/settings/security/passkeys/options"
                data-verify-url="/settings/security/passkeys">
          Add a passkey
        </button>
        <p data-passkey-error class="field-error" hidden></p>
        <p data-passkey-unsupported class="field-hint" hidden>This browser does not support passkeys.</p>
      </div>
    </div>

    {{-- Recovery codes --}}
    <div class="card">
      <div class="card-header flex flex-wrap items-center justify-between gap-3">
        <div>
          <h3 class="font-semibold">Recovery codes</h3>
          <p class="mt-0.5 text-sm text-ink-500 dark:text-ink-400">The way back in when the phone is gone.</p>
        </div>
        @if(recovery_remaining)<span class="badge-neutral">{{ recovery_remaining }} left</span>@endif
      </div>
      <div class="card-body">
        @if(fresh_recovery_codes_empty == false)
          <div class="mb-4 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm
                      dark:border-amber-500/30 dark:bg-amber-500/10">
            <p class="font-medium text-amber-900 dark:text-amber-300">Save these now. They are not shown again.</p>
            <ul class="mt-3 grid grid-cols-2 gap-2 font-mono text-sm">
              @foreach(fresh_recovery_codes as code)
                <li class="rounded bg-white px-2 py-1 text-center dark:bg-ink-900">{{ code }}</li>
              @endforeach
            </ul>
            <div class="mt-3 flex gap-2">
              <button type="button" class="btn-secondary btn-sm" data-copy="{{ fresh_recovery_codes_text }}">Copy all</button>
              <a download="recovery-codes.txt" class="btn-secondary btn-sm"
                 data-download-text="{{ fresh_recovery_codes_text }}" href="#">Download</a>
            </div>
          </div>
        @endif

        <form method="post" action="/settings/security/recovery-codes"
              data-confirm="Generate a new set? Every existing code stops working immediately.">
          {!! csrf_field !!}
          <button type="submit" class="btn-secondary">
            @if(recovery_remaining)Generate a new set@else Generate codes @endif
          </button>
        </form>
      </div>
    </div>

    {{-- Sessions --}}
    <div class="card">
      <div class="card-header"><h3 class="font-semibold">Signed-in devices</h3></div>
      <div class="card-body">
        <p class="mb-4 text-sm text-ink-500 dark:text-ink-400">
          Signing out everywhere else keeps this device and ends the rest.
        </p>
        <form method="post" action="/settings/security/sessions/revoke"
              data-confirm="Sign out of every other device?">
          {!! csrf_field !!}
          <button type="submit" class="btn-secondary">Sign out other devices</button>
        </form>
      </div>
    </div>
  </div>
@endsection