your personal website on atproto - mirror blento.app

Merge pull request #59 from flo-bit/improve-oauth

improve login, small fixes

authored by Florian and committed by GitHub cf7ab30f 800a84eb

+170 -149
-2
docs/Beta.md
··· 24 - when adding images try to add them in a size that best fits aspect ratio 25 26 - onboarding 27 - 28 - - fix invalid handle thing
··· 24 - when adding images try to add them in a size that best fits aspect ratio 25 26 - onboarding
+3 -1
src/lib/atproto/UI/LoginModal.svelte
··· 126 {#if showRecentLogins} 127 <div class="mt-2 mb-2 text-sm font-medium">Recent logins</div> 128 <div class="flex flex-col gap-2"> 129 - {#each Object.values(recentLogins).slice(0, 4) as recentLogin (recentLogin.did)} 130 <div class="group"> 131 <div 132 class="group-hover:bg-base-300 bg-base-200 dark:bg-base-700 dark:hover:bg-base-600 dark:border-base-500/50 border-base-300 relative flex h-10 w-full items-center justify-between gap-2 rounded-full border px-2 font-semibold transition-colors duration-100"
··· 126 {#if showRecentLogins} 127 <div class="mt-2 mb-2 text-sm font-medium">Recent logins</div> 128 <div class="flex flex-col gap-2"> 129 + {#each Object.values(recentLogins) 130 + .filter((l) => l.handle && l.handle !== 'handle.invalid') 131 + .slice(0, 4) as recentLogin (recentLogin.did)} 132 <div class="group"> 133 <div 134 class="group-hover:bg-base-300 bg-base-200 dark:bg-base-700 dark:hover:bg-base-600 dark:border-base-500/50 border-base-300 relative flex h-10 w-full items-center justify-between gap-2 rounded-full border px-2 font-semibold transition-colors duration-100"
+17 -7
src/lib/atproto/auth.svelte.ts
··· 22 import { replaceState } from '$app/navigation'; 23 24 import { metadata } from './metadata'; 25 - import { getDetailedProfile } from './methods'; 26 - import { signUpPDS } from './settings'; 27 import { SvelteURLSearchParams } from 'svelte/reactivity'; 28 29 import type { ActorIdentifier, Did } from '@atcute/lexicons'; ··· 42 43 const clientId = dev 44 ? `http://localhost` + 45 - `?redirect_uri=${encodeURIComponent('http://127.0.0.1:5179/oauth/callback')}` + 46 `&scope=${encodeURIComponent(metadata.scope)}` 47 : metadata.client_id; 48 49 const handleResolver = new CompositeHandleResolver({ 50 methods: { 51 - dns: new DohJsonHandleResolver({ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query' }), 52 http: new WellKnownHandleResolver() 53 } 54 }); ··· 56 configureOAuth({ 57 metadata: { 58 client_id: clientId, 59 - redirect_uri: dev ? 'http://127.0.0.1:5179/oauth/callback' : metadata.redirect_uris[0] 60 }, 61 identityResolver: new LocalActorResolver({ 62 handleResolver: handleResolver, ··· 224 225 const response = await getDetailedProfile(); 226 227 - user.profile = response; 228 - localStorage.setItem(`profile-${actor}`, JSON.stringify(response)); 229 }
··· 22 import { replaceState } from '$app/navigation'; 23 24 import { metadata } from './metadata'; 25 + import { describeRepo, getDetailedProfile } from './methods'; 26 + import { DOH_RESOLVER, REDIRECT_PATH, signUpPDS } from './settings'; 27 import { SvelteURLSearchParams } from 'svelte/reactivity'; 28 29 import type { ActorIdentifier, Did } from '@atcute/lexicons'; ··· 42 43 const clientId = dev 44 ? `http://localhost` + 45 + `?redirect_uri=${encodeURIComponent('http://127.0.0.1:5179' + REDIRECT_PATH)}` + 46 `&scope=${encodeURIComponent(metadata.scope)}` 47 : metadata.client_id; 48 49 const handleResolver = new CompositeHandleResolver({ 50 methods: { 51 + dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), 52 http: new WellKnownHandleResolver() 53 } 54 }); ··· 56 configureOAuth({ 57 metadata: { 58 client_id: clientId, 59 + redirect_uri: dev ? 'http://127.0.0.1:5179' + REDIRECT_PATH : metadata.redirect_uris[0] 60 }, 61 identityResolver: new LocalActorResolver({ 62 handleResolver: handleResolver, ··· 224 225 const response = await getDetailedProfile(); 226 227 + if (!response || response.handle === 'handle.invalid') { 228 + console.log('invalid handle or no profile from bsky, fetching from repo description'); 229 + const repo = await describeRepo({ did: actor }); 230 + user.profile = { 231 + did: actor, 232 + handle: repo?.handle || 'handle.invalid' 233 + }; 234 + localStorage.setItem(`profile-${actor}`, JSON.stringify(user.profile)); 235 + } else { 236 + user.profile = response; 237 + localStorage.setItem(`profile-${actor}`, JSON.stringify(response)); 238 + } 239 }
+4 -4
src/lib/atproto/metadata.ts
··· 1 import { resolve } from '$app/paths'; 2 - import { permissions, SITE } from './settings'; 3 4 function constructScope() { 5 const repos = permissions.collections.map((collection) => 'repo:' + collection).join(' '); ··· 14 } 15 16 let blobScope: string | undefined = undefined; 17 - if (Array.isArray(permissions.blobs)) { 18 blobScope = 'blob?' + permissions.blobs.map((b) => 'accept=' + b).join('&'); 19 - } else if (permissions.blobs) { 20 blobScope = 'blob:' + permissions.blobs; 21 } 22 ··· 26 27 export const metadata = { 28 client_id: SITE + resolve('/oauth-client-metadata.json'), 29 - redirect_uris: [SITE + resolve('/oauth/callback')], 30 scope: constructScope(), 31 grant_types: ['authorization_code', 'refresh_token'], 32 response_types: ['code'],
··· 1 import { resolve } from '$app/paths'; 2 + import { permissions, REDIRECT_PATH, SITE } from './settings'; 3 4 function constructScope() { 5 const repos = permissions.collections.map((collection) => 'repo:' + collection).join(' '); ··· 14 } 15 16 let blobScope: string | undefined = undefined; 17 + if (Array.isArray(permissions.blobs) && permissions.blobs.length > 0) { 18 blobScope = 'blob?' + permissions.blobs.map((b) => 'accept=' + b).join('&'); 19 + } else if (permissions.blobs && permissions.blobs.length > 0) { 20 blobScope = 'blob:' + permissions.blobs; 21 } 22 ··· 26 27 export const metadata = { 28 client_id: SITE + resolve('/oauth-client-metadata.json'), 29 + redirect_uris: [SITE + resolve(REDIRECT_PATH)], 30 scope: constructScope(), 31 grant_types: ['authorization_code', 'refresh_token'], 32 response_types: ['code'],
+8 -1
src/lib/atproto/settings.ts
··· 46 47 // which PDS to use for signup 48 // ATTENTION: pds.rip is only for development, all accounts get deleted automatically after a week 49 - export const signUpPDS = dev ? 'https://pds.rip/' : 'https://selfhosted.social';
··· 46 47 // which PDS to use for signup 48 // ATTENTION: pds.rip is only for development, all accounts get deleted automatically after a week 49 + const devPDS = 'https://pds.rip/'; 50 + const prodPDS = 'https://selfhosted.social/'; 51 + export const signUpPDS = dev ? devPDS : prodPDS; 52 + 53 + // where to redirect after oauth login/signup, e.g. /oauth/callback 54 + export const REDIRECT_PATH = '/oauth/callback'; 55 + 56 + export const DOH_RESOLVER = 'https://mozilla.cloudflare-dns.com/dns-query';
+11 -1
src/lib/cards/FluidTextCard/EditingFluidTextCard.svelte
··· 1 <script lang="ts"> 2 import type { Item } from '$lib/types'; 3 import type { ContentComponentProps } from '../types'; 4 import FluidTextCard from './FluidTextCard.svelte'; 5 ··· 26 isEditing = false; 27 } 28 } 29 </script> 30 31 <!-- svelte-ignore a11y_no_static_element_interactions --> ··· 36 : ''}" 37 onclick={handleClick} 38 > 39 - {#key item.color} 40 <FluidTextCard {item} /> 41 {/key} 42
··· 1 <script lang="ts"> 2 import type { Item } from '$lib/types'; 3 + import { onMount, tick } from 'svelte'; 4 import type { ContentComponentProps } from '../types'; 5 import FluidTextCard from './FluidTextCard.svelte'; 6 ··· 27 isEditing = false; 28 } 29 } 30 + 31 + let rerender = $state(0); 32 + onMount(() => { 33 + window.addEventListener('theme-changed', async () => { 34 + // Force re-render to update FluidTextCard colors 35 + await tick(); 36 + rerender = Math.random(); 37 + }); 38 + }); 39 </script> 40 41 <!-- svelte-ignore a11y_no_static_element_interactions --> ··· 46 : ''}" 47 onclick={handleClick} 48 > 49 + {#key item.color + '-' + rerender.toString()} 50 <FluidTextCard {item} /> 51 {/key} 52
+123
src/lib/website/Controls.svelte
···
··· 1 + <script lang="ts"> 2 + import { SelectThemePopover } from '$lib/components/select-theme'; 3 + import { getHideProfileSection, getProfilePosition } from '$lib/helper'; 4 + import type { WebsiteData } from '$lib/types'; 5 + import { Button } from '@foxui/core'; 6 + import { getIsMobile } from './context'; 7 + 8 + let { data = $bindable() }: { data: WebsiteData } = $props(); 9 + 10 + let accentColor = $derived(data.publication?.preferences?.accentColor ?? 'pink'); 11 + let baseColor = $derived(data.publication?.preferences?.baseColor ?? 'stone'); 12 + 13 + function updateTheme(newAccent: string, newBase: string) { 14 + data.publication.preferences ??= {}; 15 + data.publication.preferences.accentColor = newAccent; 16 + data.publication.preferences.baseColor = newBase; 17 + data = { ...data }; 18 + } 19 + 20 + let profilePosition = $derived(getProfilePosition(data)); 21 + 22 + function toggleProfilePosition() { 23 + data.publication.preferences ??= {}; 24 + data.publication.preferences.profilePosition = profilePosition === 'side' ? 'top' : 'side'; 25 + data = { ...data }; 26 + } 27 + 28 + let isMobile = getIsMobile(); 29 + </script> 30 + 31 + <div class={['fixed top-2 left-14 z-20 flex gap-2']}> 32 + <Button 33 + size="icon" 34 + onclick={() => { 35 + data.publication.preferences ??= {}; 36 + data.publication.preferences.hideProfileSection = 37 + !data.publication.preferences?.hideProfileSection; 38 + data = { ...data }; 39 + }} 40 + variant="ghost" 41 + > 42 + {#if !getHideProfileSection(data)} 43 + <svg 44 + xmlns="http://www.w3.org/2000/svg" 45 + fill="none" 46 + viewBox="0 0 24 24" 47 + stroke-width="1.5" 48 + stroke="currentColor" 49 + class="size-5!" 50 + > 51 + <path 52 + stroke-linecap="round" 53 + stroke-linejoin="round" 54 + d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" 55 + /> 56 + </svg> 57 + {:else} 58 + <svg 59 + xmlns="http://www.w3.org/2000/svg" 60 + fill="none" 61 + viewBox="0 0 24 24" 62 + stroke-width="1.5" 63 + stroke="currentColor" 64 + class="size-5!" 65 + > 66 + <path 67 + stroke-linecap="round" 68 + stroke-linejoin="round" 69 + d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" 70 + /> 71 + <path 72 + stroke-linecap="round" 73 + stroke-linejoin="round" 74 + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 75 + /> 76 + </svg> 77 + {/if} 78 + </Button> 79 + 80 + <!-- Position toggle button (desktop only) --> 81 + {#if !isMobile() && !getHideProfileSection(data)} 82 + <Button size="icon" type="button" onclick={toggleProfilePosition} variant="ghost"> 83 + {#if profilePosition === 'side'} 84 + <svg 85 + xmlns="http://www.w3.org/2000/svg" 86 + fill="none" 87 + viewBox="0 0 24 24" 88 + stroke-width="1.5" 89 + stroke="currentColor" 90 + class="size-5!" 91 + > 92 + <path 93 + stroke-linecap="round" 94 + stroke-linejoin="round" 95 + d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" 96 + /> 97 + </svg> 98 + {:else} 99 + <svg 100 + xmlns="http://www.w3.org/2000/svg" 101 + fill="none" 102 + viewBox="0 0 24 24" 103 + stroke-width="1.5" 104 + stroke="currentColor" 105 + class="size-5!" 106 + > 107 + <path 108 + stroke-linecap="round" 109 + stroke-linejoin="round" 110 + d="m19.5 4.5-15 15m0 0h11.25m-11.25 0V8.25" 111 + /> 112 + </svg> 113 + {/if} 114 + </Button> 115 + {/if} 116 + 117 + <!-- Theme selection --> 118 + <SelectThemePopover 119 + {accentColor} 120 + {baseColor} 121 + onchanged={(newAccent, newBase) => updateTheme(newAccent, newBase)} 122 + /> 123 + </div>
+1 -95
src/lib/website/EditableProfile.svelte
··· 11 let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } = 12 $props(); 13 14 - let accentColor = $derived(data.publication?.preferences?.accentColor ?? 'pink'); 15 - let baseColor = $derived(data.publication?.preferences?.baseColor ?? 'stone'); 16 - 17 - function updateTheme(newAccent: string, newBase: string) { 18 - data.publication.preferences ??= {}; 19 - data.publication.preferences.accentColor = newAccent; 20 - data.publication.preferences.baseColor = newBase; 21 - data = { ...data }; 22 - } 23 - 24 - let profilePosition = $derived(getProfilePosition(data)); 25 - 26 - function toggleProfilePosition() { 27 - data.publication.preferences ??= {}; 28 - data.publication.preferences.profilePosition = profilePosition === 'side' ? 'top' : 'side'; 29 - data = { ...data }; 30 - } 31 - 32 let fileInput: HTMLInputElement; 33 let isHoveringAvatar = $state(false); 34 ··· 62 fileInput.click(); 63 } 64 65 - let isMobile = getIsMobile(); 66 </script> 67 68 <div ··· 73 : '@5xl/wrapper:max-w-4xl @5xl/wrapper:px-12' 74 ]} 75 > 76 - <div 77 - class={[ 78 - 'absolute left-2 z-20 flex gap-2', 79 - profilePosition === 'side' ? 'top-2 left-14' : 'top-2' 80 - ]} 81 - > 82 - <Button 83 - size="icon" 84 - onclick={() => { 85 - data.publication.preferences ??= {}; 86 - data.publication.preferences.hideProfileSection = true; 87 - data = { ...data }; 88 - }} 89 - variant="ghost" 90 - > 91 - <svg 92 - xmlns="http://www.w3.org/2000/svg" 93 - fill="none" 94 - viewBox="0 0 24 24" 95 - stroke-width="1.5" 96 - stroke="currentColor" 97 - class="size-6" 98 - > 99 - <path 100 - stroke-linecap="round" 101 - stroke-linejoin="round" 102 - d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" 103 - /> 104 - </svg> 105 - </Button> 106 - 107 - <!-- Position toggle button (desktop only) --> 108 - {#if !isMobile()} 109 - <Button size="icon" type="button" onclick={toggleProfilePosition} variant="ghost"> 110 - {#if profilePosition === 'side'} 111 - <svg 112 - xmlns="http://www.w3.org/2000/svg" 113 - fill="none" 114 - viewBox="0 0 24 24" 115 - stroke-width="1.5" 116 - stroke="currentColor" 117 - class="size-6" 118 - > 119 - <path 120 - stroke-linecap="round" 121 - stroke-linejoin="round" 122 - d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" 123 - /> 124 - </svg> 125 - {:else} 126 - <svg 127 - xmlns="http://www.w3.org/2000/svg" 128 - fill="none" 129 - viewBox="0 0 24 24" 130 - stroke-width="1.5" 131 - stroke="currentColor" 132 - class="size-6" 133 - > 134 - <path 135 - stroke-linecap="round" 136 - stroke-linejoin="round" 137 - d="m19.5 4.5-15 15m0 0h11.25m-11.25 0V8.25" 138 - /> 139 - </svg> 140 - {/if} 141 - </Button> 142 - {/if} 143 - 144 - <!-- Theme selection --> 145 - <SelectThemePopover 146 - {accentColor} 147 - {baseColor} 148 - onchanged={(newAccent, newBase) => updateTheme(newAccent, newBase)} 149 - /> 150 - </div> 151 - 152 <div 153 class={[ 154 'flex flex-col gap-4 pt-16 pb-4',
··· 11 let { data = $bindable(), hideBlento = false }: { data: WebsiteData; hideBlento?: boolean } = 12 $props(); 13 14 let fileInput: HTMLInputElement; 15 let isHoveringAvatar = $state(false); 16 ··· 44 fileInput.click(); 45 } 46 47 + let profilePosition = $derived(getProfilePosition(data)); 48 </script> 49 50 <div ··· 55 : '@5xl/wrapper:max-w-4xl @5xl/wrapper:px-12' 56 ]} 57 > 58 <div 59 class={[ 60 'flex flex-col gap-4 pt-16 pb-4',
+3 -38
src/lib/website/EditableWebsite.svelte
··· 37 import FloatingEditButton from './FloatingEditButton.svelte'; 38 import { user } from '$lib/atproto'; 39 import { launchConfetti } from '@foxui/visual'; 40 41 let { 42 data ··· 598 </div> 599 {/if} 600 601 {#if showingMobileView} 602 <div 603 class="bg-base-200 dark:bg-base-950 pointer-events-none fixed inset-0 -z-10 h-full w-full" ··· 643 : '@5xl/wrapper:max-w-4xl' 644 ]} 645 > 646 - {#if getHideProfileSection(data)} 647 - <div class="pointer-events-auto absolute top-2 left-2 z-20 flex gap-2"> 648 - <Button 649 - size="icon" 650 - variant="ghost" 651 - onclick={() => { 652 - data.publication.preferences ??= {}; 653 - data.publication.preferences.hideProfileSection = false; 654 - data = { ...data }; 655 - }} 656 - > 657 - <svg 658 - xmlns="http://www.w3.org/2000/svg" 659 - fill="none" 660 - viewBox="0 0 24 24" 661 - stroke-width="1.5" 662 - stroke="currentColor" 663 - class="size-6" 664 - > 665 - <path 666 - stroke-linecap="round" 667 - stroke-linejoin="round" 668 - d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" 669 - /> 670 - <path 671 - stroke-linecap="round" 672 - stroke-linejoin="round" 673 - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 674 - /> 675 - </svg> 676 - </Button> 677 - <SelectThemePopover 678 - {accentColor} 679 - {baseColor} 680 - onchanged={(newAccent, newBase) => updateTheme(newAccent, newBase)} 681 - /> 682 - </div> 683 - {/if} 684 <div class="pointer-events-none"></div> 685 <!-- svelte-ignore a11y_no_static_element_interactions --> 686 <div
··· 37 import FloatingEditButton from './FloatingEditButton.svelte'; 38 import { user } from '$lib/atproto'; 39 import { launchConfetti } from '@foxui/visual'; 40 + import Controls from './Controls.svelte'; 41 42 let { 43 data ··· 599 </div> 600 {/if} 601 602 + <Controls bind:data /> 603 + 604 {#if showingMobileView} 605 <div 606 class="bg-base-200 dark:bg-base-950 pointer-events-none fixed inset-0 -z-10 h-full w-full" ··· 646 : '@5xl/wrapper:max-w-4xl' 647 ]} 648 > 649 <div class="pointer-events-none"></div> 650 <!-- svelte-ignore a11y_no_static_element_interactions --> 651 <div