Built for people who think better out loud.

frontend: add OAuth Storybook demo

isaaccorbrey.com e9efc005 c81eb949

verified
+141 -1
+2 -1
frontend/src/components/Input.svelte
··· 1 1 <script lang="ts"> 2 2 import type { HTMLInputAttributes } from "svelte/elements"; 3 - let { class: className = "", ...props }: HTMLInputAttributes = $props(); 3 + let { class: className = "", value = $bindable(""), ...props }: HTMLInputAttributes = $props(); 4 4 </script> 5 5 6 6 <input 7 7 {...props} 8 + bind:value 8 9 class=" 9 10 px-3 py-px font-base bg-slate-950/7 10 11 border-s-slate-950 rounded-md
+23
frontend/src/components/OAuthDemo.stories.svelte
··· 1 + <script module> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import OAuthDemo from "./OAuthDemo.svelte"; 4 + 5 + const { Story } = defineMeta({ 6 + title: "Experiments/OAuth Demo", 7 + component: OAuthDemo, 8 + argTypes: { 9 + backendBase: { control: "text" }, 10 + subject: { control: "text" }, 11 + openInNewTab: { control: "boolean" }, 12 + }, 13 + }); 14 + </script> 15 + 16 + <Story 17 + name="OAuth Demo" 18 + args={{ 19 + backendBase: "http://localhost:3001", 20 + subject: "", 21 + openInNewTab: true, 22 + }} 23 + />
+116
frontend/src/components/OAuthDemo.svelte
··· 1 + <script lang="ts"> 2 + import Button from "./Button.svelte"; 3 + import Input from "./Input.svelte"; 4 + 5 + let { 6 + backendBase = $bindable("http://localhost:3001"), 7 + subject = $bindable(""), 8 + openInNewTab = $bindable(true), 9 + } = $props(); 10 + 11 + let error = $state(""); 12 + 13 + function normalizeBase(value: string) { 14 + return value.trim().replace(/\/+$/, ""); 15 + } 16 + 17 + function buildStartUrl(base: string, subjectValue: string) { 18 + const normalized = normalizeBase(base); 19 + const trimmedSubject = subjectValue.trim(); 20 + if (!normalized || !trimmedSubject) { 21 + return ""; 22 + } 23 + return `${normalized}/api/auth/atproto/start?subject=${encodeURIComponent(trimmedSubject)}`; 24 + } 25 + 26 + const startUrl = $derived(buildStartUrl(backendBase, subject)); 27 + 28 + function beginOAuth() { 29 + if (!subject.trim()) { 30 + error = "Enter a handle or DID before starting OAuth."; 31 + return; 32 + } 33 + error = ""; 34 + if (!startUrl) { 35 + error = "Start URL could not be built. Check the backend base URL."; 36 + return; 37 + } 38 + if (openInNewTab) { 39 + window.open(startUrl, "_blank", "noopener,noreferrer"); 40 + } else { 41 + window.location.assign(startUrl); 42 + } 43 + } 44 + </script> 45 + 46 + <div 47 + class=" 48 + max-w-2xl rounded-xl border border-slate-900/15 49 + bg-[radial-gradient(circle_at_top,#f8fafc,transparent)] 50 + p-6 shadow-[0_10px_30px_rgba(15,23,42,0.12)] 51 + " 52 + > 53 + <div class="flex flex-col gap-2"> 54 + <p class="text-sm uppercase tracking-[0.18em] text-slate-600"> 55 + ATProto OAuth Demo 56 + </p> 57 + <h2 class="font-display text-2xl text-slate-900"> 58 + Log in to your PDS 59 + </h2> 60 + <p class="text-sm text-slate-600"> 61 + This demo hits your backend OAuth endpoints and redirects you to your 62 + authorization server. Make sure the backend is running with the OAuth 63 + env vars configured for your PDS. 64 + </p> 65 + </div> 66 + 67 + <div class="mt-6 grid gap-4"> 68 + <label class="grid gap-2 text-sm text-slate-700"> 69 + Backend base URL 70 + <Input bind:value={backendBase} placeholder="http://localhost:3001" /> 71 + </label> 72 + 73 + <label class="grid gap-2 text-sm text-slate-700"> 74 + Handle or DID 75 + <Input bind:value={subject} placeholder="alice.example.com or did:plc:..." /> 76 + </label> 77 + 78 + <label class="flex items-center gap-3 text-sm text-slate-700"> 79 + <input 80 + type="checkbox" 81 + class="h-4 w-4 rounded border-slate-400 text-blue-600" 82 + bind:checked={openInNewTab} 83 + /> 84 + Open OAuth in a new tab 85 + </label> 86 + 87 + <div class="flex flex-wrap gap-3"> 88 + <Button type="button" onclick={beginOAuth}> 89 + Start OAuth 90 + </Button> 91 + {#if startUrl} 92 + <a 93 + class="text-sm text-slate-700 underline decoration-slate-400 underline-offset-4" 94 + href={startUrl} 95 + target="_blank" 96 + rel="noopener noreferrer" 97 + > 98 + Open start URL 99 + </a> 100 + {/if} 101 + </div> 102 + 103 + {#if error} 104 + <p class="text-sm text-red-600">{error}</p> 105 + {/if} 106 + 107 + {#if startUrl} 108 + <div class="rounded-lg bg-white/70 p-3 text-xs text-slate-600"> 109 + <div class="uppercase tracking-[0.2em] text-[10px] text-slate-500"> 110 + Start URL 111 + </div> 112 + <div class="mt-1 break-all font-mono">{startUrl}</div> 113 + </div> 114 + {/if} 115 + </div> 116 + </div>