Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations. pdsmoover.com
pds atproto migrations moo cow

QOL Update to append PDS URL's to auto fill #4

merged opened by baileytownsend.dev targeting main from feature/QolSelfFillingPDSInfo

You can now add PDS domain's to the /moover link like /moover/selfhosted.social to allow some extra features

  • Auto fill destination PDS
  • Hides the invite code if it's not needed
  • Shows a drop down of handle endings
  • brings in the T&S and privacy policy to be approved if set on the pds.env
  • Only does this for approved PDSs to stop phishing
Labels

None yet.

Participants 1
AT URI
at://did:plc:rnpkyqnmsw4ipey6eotbdnnf/sh.tangled.repo.pull/3m65qbbrb7w22
+214 -33
Diff #0
.tangled/images/network.webp

This is a binary file and will not be displayed.

+2 -2
README.md
··· 9 10 - Looking for the old pds moover for simple code to fork 11 check [here](https://tangled.org/@baileytownsend.dev/pds-moover/tree/803d8a70b7100c9e14df3402277441050e0f6194), if 12 - you'd like to see the newer front end check [here](./web/ui-code/src) 13 - Want to run your own instance of PDS MOOver? [check this docker compose](./compose.selfhost.yml). It should have all 14 the 15 services in one easy `docker compose up`, just don't forget to create a `.env` from [.env.template](.env.template) ··· 43 ## Do you have a pretty picture to show how the network looks? 44 45 yes. Thanks to [Orual](https://bsky.app/profile/nonbinary.computer) 46 - ![](./web/public/PDSMOOver.excalidraw.png)
··· 9 10 - Looking for the old pds moover for simple code to fork 11 check [here](https://tangled.org/@baileytownsend.dev/pds-moover/tree/803d8a70b7100c9e14df3402277441050e0f6194), if 12 + you'd like to see the newer front end check [here](./web-ui) 13 - Want to run your own instance of PDS MOOver? [check this docker compose](./compose.selfhost.yml). It should have all 14 the 15 services in one easy `docker compose up`, just don't forget to create a `.env` from [.env.template](.env.template) ··· 43 ## Do you have a pretty picture to show how the network looks? 44 45 yes. Thanks to [Orual](https://bsky.app/profile/nonbinary.computer) 46 + ![](.tangled/images/network.webp)
+1 -1
justfile
··· 25 docker buildx build \ 26 --platform linux/arm64,linux/amd64 \ 27 --tag fatfingers23/moover_ui:latest \ 28 - --tag fatfingers23/moover_ui:0.0.2 \ 29 --file Dockerfiles/web-ui.Dockerfile \ 30 --push .
··· 25 docker buildx build \ 26 --platform linux/arm64,linux/amd64 \ 27 --tag fatfingers23/moover_ui:latest \ 28 + --tag fatfingers23/moover_ui:0.0.3 \ 29 --file Dockerfiles/web-ui.Dockerfile \ 30 --push .
test.compose.yml
+1
web-ui/package.json
··· 13 "lint": "eslint ." 14 }, 15 "dependencies": { 16 "@atcute/client": "^4.0.5", 17 "@atcute/lexicons": "^1.2.2", 18 "@pds-moover/lexicons": "^1.0.1",
··· 13 "lint": "eslint ." 14 }, 15 "dependencies": { 16 + "@atcute/atproto": "^3.1.9", 17 "@atcute/client": "^4.0.5", 18 "@atcute/lexicons": "^1.2.2", 19 "@pds-moover/lexicons": "^1.0.1",
+10
web-ui/pnpm-lock.yaml
··· 8 9 .: 10 dependencies: 11 '@atcute/client': 12 specifier: ^4.0.5 13 version: 4.0.5 ··· 73 74 packages: 75 76 '@atcute/cbor@2.2.7': 77 resolution: {integrity: sha512-/mwAF0gnokOphceZqFq3uzMGdd8sbw5y6bxF8CRutRkCCUcpjjpJc5fkLwhxyGgOveF3mZuHE6p7t/+IAqb7Aw==} 78 ··· 1342 1343 snapshots: 1344 1345 '@atcute/cbor@2.2.7': 1346 dependencies: 1347 '@atcute/cid': 2.2.6
··· 8 9 .: 10 dependencies: 11 + '@atcute/atproto': 12 + specifier: ^3.1.9 13 + version: 3.1.9 14 '@atcute/client': 15 specifier: ^4.0.5 16 version: 4.0.5 ··· 76 77 packages: 78 79 + '@atcute/atproto@3.1.9': 80 + resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==} 81 + 82 '@atcute/cbor@2.2.7': 83 resolution: {integrity: sha512-/mwAF0gnokOphceZqFq3uzMGdd8sbw5y6bxF8CRutRkCCUcpjjpJc5fkLwhxyGgOveF3mZuHE6p7t/+IAqb7Aw==} 84 ··· 1348 1349 snapshots: 1350 1351 + '@atcute/atproto@3.1.9': 1352 + dependencies: 1353 + '@atcute/lexicons': 1.2.2 1354 + 1355 '@atcute/cbor@2.2.7': 1356 dependencies: 1357 '@atcute/cid': 2.2.6
+8 -8
web-ui/src/app.html
··· 1 <!doctype html> 2 <html lang="en"> 3 - <head> 4 - <meta charset="utf-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 - %sveltekit.head% 7 - </head> 8 - <body data-sveltekit-preload-data="hover"> 9 - <div style="display: contents">%sveltekit.body%</div> 10 - </body> 11 </html>
··· 1 <!doctype html> 2 <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"/> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/> 6 + %sveltekit.head% 7 + </head> 8 + <body data-sveltekit-preload-data="hover"> 9 + <div style="display: contents">%sveltekit.body%</div> 10 + </body> 11 </html>
+33
web-ui/src/lib/assets/style.css
··· 89 box-sizing: border-box; 90 } 91 92 .cow-image { 93 height: 150px; 94 margin: 20px 0 8px 0;
··· 89 box-sizing: border-box; 90 } 91 92 + /* Input group for handle with domain dropdown */ 93 + .input-group { 94 + display: flex; 95 + width: 100%; 96 + } 97 + 98 + .input-group input { 99 + flex: 1; 100 + border-top-right-radius: 0; 101 + border-bottom-right-radius: 0; 102 + border-right: none; 103 + } 104 + 105 + .input-group .domain-select { 106 + padding: 8px; 107 + border: 1px solid rgba(128, 128, 128, 0.5); 108 + border-top-left-radius: 0; 109 + border-bottom-left-radius: 0; 110 + border-top-right-radius: 4px; 111 + border-bottom-right-radius: 4px; 112 + background-color: #1a1a1a; 113 + color: rgba(255, 255, 255, 0.87); 114 + cursor: pointer; 115 + min-width: 120px; 116 + } 117 + 118 + @media (prefers-color-scheme: light) { 119 + .input-group .domain-select { 120 + background-color: #f9f9f9; 121 + color: #213547; 122 + } 123 + } 124 + 125 .cow-image { 126 height: 150px; 127 margin: 20px 0 8px 0;
+3 -2
web-ui/src/lib/components/NavBar.svelte
··· 15 ]; 16 </script> 17 18 - <header class="navbar" role="banner"> 19 20 <div class="navbar-inner"> 21 <a class="brand" href={resolve('/')}>PDS MOOver</a> ··· 45 <nav id="primary-navigation" class="nav-links {open ? 'open' : ''}" aria-label="Primary" 46 > 47 {#each links as link (link.path)} 48 - <a href={resolve(link.path)} class={page.url.pathname === link.path ? 'active' : '' } onclick={() => open = false}>{link.text}</a> 49 {/each} 50 </nav> 51 </div>
··· 15 ]; 16 </script> 17 18 + <header class="navbar"> 19 20 <div class="navbar-inner"> 21 <a class="brand" href={resolve('/')}>PDS MOOver</a> ··· 45 <nav id="primary-navigation" class="nav-links {open ? 'open' : ''}" aria-label="Primary" 46 > 47 {#each links as link (link.path)} 48 + <a href={resolve(link.path)} class={page.url.pathname.startsWith(link.path) ? 'active' : '' } 49 + onclick={() => open = false}>{link.text}</a> 50 {/each} 51 </nav> 52 </div>
+34
web-ui/src/routes/moover/[[pds]]/+page.server.ts
···
··· 1 + import type {PageServerLoad} from './$types'; 2 + import {Client, simpleFetchHandler} from '@atcute/client'; 3 + import type {} from '@atcute/atproto'; 4 + import {env} from '$env/dynamic/private'; 5 + 6 + export const load: PageServerLoad = async ({params}) => { 7 + 8 + if (!params.pds) { 9 + return {pdsOptions: null, intinalDomain: null}; 10 + } 11 + 12 + const allowedPds = env.PDS_AUTOFILL.split(','); 13 + if (!allowedPds.includes(params.pds.toLowerCase())) { 14 + console.error('PDS not allowed', params.pds); 15 + return {pdsOptions: null, intinalDomain: null}; 16 + } 17 + 18 + try { 19 + const handler = simpleFetchHandler({service: `https://${params.pds}`}); 20 + const rpc = new Client({handler}); 21 + const {ok, data} = await rpc.get('com.atproto.server.describeServer', {}) 22 + if (!ok) { 23 + console.error('Failed to describe the PDS server', data); 24 + return {pds: null}; 25 + } 26 + return { 27 + pdsOptions: data, 28 + intinalDomain: data?.availableUserDomains[0] ?? '' 29 + }; 30 + } catch (e) { 31 + console.error('Failed to describe the PDS server', e); 32 + return {pdsOptions: null, intinalDomain: null}; 33 + } 34 + };
+122 -20
web-ui/src/routes/moover/+page.svelte web-ui/src/routes/moover/[[pds]]/+page.svelte
··· 5 import {Migrator} from '@pds-moover/moover'; 6 import SignThePapers from './SignThePapers.svelte'; 7 8 let formData = $state({ 9 handle: '', 10 password: '', ··· 14 inviteCode: null, 15 twoFactorCode: null, 16 confirmation: false, 17 // Advanced options 18 createNewAccount: true, 19 migrateRepo: true, ··· 35 let errorMessage: null | string = $state(null); 36 let statusMessage: null | string = $state(null); 37 38 const updateStatusHandler = (status: string) => { 39 statusMessage = status; 40 } ··· 51 return; 52 } 53 54 try { 55 56 if (showTwoFactorCodeInput) { ··· 69 migrator.migratePrefs = formData.migratePrefs; 70 migrator.migratePlcRecord = formData.migratePlcRecord; 71 72 - console.log(migrator); 73 74 updateStatusHandler('Starting migration...'); 75 showStatusMessage = true; ··· 78 formData.password, 79 formData.newPds, 80 formData.newEmail, 81 - formData.newHandle, 82 formData.inviteCode, 83 updateStatusHandler, 84 formData.twoFactorCode, ··· 144 145 <!-- Second section: New account details --> 146 <div class="section"> 147 - <h2>Setup for the new PDS</h2> 148 - <div class="form-group"> 149 - <label for="new-pds">New PDS (URL):</label> 150 - <input type="url" id="new-pds" name="newPds" placeholder="https://coolnewpds.com" 151 - required bind:value={formData.newPds}> 152 - </div> 153 154 <div class="form-group"> 155 <label for="new-email">New Email:</label> 156 - <input type="email" id="new-email" name="newEmail" placeholder="CanBeSameEmailAsTheOldPds@email.com" 157 required bind:value={formData.newEmail}> 158 </div> 159 160 <div class="form-group"> 161 <label for="new-handle">New Handle:</label> 162 - <input type="text" id="new-handle" name="newHandle" 163 - placeholder="username.newpds.com or mycooldomain.com" required 164 - bind:value={formData.newHandle}> 165 - </div> 166 167 - <div class="form-group"> 168 - <label for="invite-code">Invite Code:</label> 169 - <input type="text" id="invite-code" name="inviteCode" 170 - placeholder="Invite code from your new PDS (Leave blank if you don't have one)" 171 - bind:value={formData.inviteCode}> 172 </div> 173 </div> 174 175 <div class="form-group"> 176 <button type="button" onclick={() => showAdvance = !showAdvance} id="advance" name="advance">Advance 177 Options ··· 227 </div> 228 {/if} 229 230 <p style="text-align: left">There are some risks that come with doing an account migration. 231 (Can view them 232 <a href="https://github.com/bluesky-social/pds/blob/main/ACCOUNT_MIGRATION.md#%EF%B8%8F-warning-%EF%B8%8F-%EF%B8%8F">here</a>) ··· 257 {/if} 258 259 <div> 260 - <button disabled={disableSubmit} type="submit">MOOve</button> 261 </div> 262 </form> 263 264 {:else} 265 - <SignThePapers migrator={migrator} newHandle={formData.newHandle}/> 266 {/if} 267 </div>
··· 5 import {Migrator} from '@pds-moover/moover'; 6 import SignThePapers from './SignThePapers.svelte'; 7 8 + let {data} = $props(); 9 + 10 + let selectedPds = $derived(data.pdsOptions); 11 + let cleanSelectedPds = $derived(selectedPds?.did.replace('did:web:', '')); 12 + //Kept as a "global" state to handle logic of passing the full handle that is used to SignThePapers 13 + let newHandle = $state(''); 14 + 15 + let selectedDomain = $state(data.intinalDomain); 16 + 17 + let handlePlaceHolder = $derived( 18 + selectedPds ? `username${selectedDomain === 'custom' ? '' : `${selectedPds?.availableUserDomains[0]}`} or mydomain.com` : 'username.newpds.com or mycooldomain.com') 19 + 20 + 21 + $effect(() => { 22 + if (!selectedPds) return; 23 + 24 + if (selectedDomain == 'custom') return; 25 + 26 + 27 + if (formData.newHandle.includes('.')) { 28 + // When a period is typed, force custom domain selection 29 + selectedDomain = 'custom'; 30 + } else { 31 + // If user clears the dot and we have provider domains, fall back to first option 32 + if ((selectedPds?.availableUserDomains?.length ?? 0) > 0 && selectedDomain === 'custom') { 33 + selectedDomain = selectedPds!.availableUserDomains[0]! 34 + } 35 + } 36 + }); 37 + 38 let formData = $state({ 39 handle: '', 40 password: '', ··· 44 inviteCode: null, 45 twoFactorCode: null, 46 confirmation: false, 47 + // Acceptance of provider policies (when required by selected PDS) 48 + acceptPolicies: false, 49 // Advanced options 50 createNewAccount: true, 51 migrateRepo: true, ··· 67 let errorMessage: null | string = $state(null); 68 let statusMessage: null | string = $state(null); 69 70 + // Links that may require acceptance prior to migration from the selected PDS 71 + const privacyUrl = $derived(selectedPds?.links?.privacyPolicy); 72 + const tosUrl = $derived(selectedPds?.links?.termsOfService); 73 + const requiresAccept = $derived(!!(privacyUrl || tosUrl)); 74 + 75 const updateStatusHandler = (status: string) => { 76 statusMessage = status; 77 } ··· 88 return; 89 } 90 91 + // If the selected PDS provides policy or privacy links, require explicit acceptance 92 + if (requiresAccept && !formData.acceptPolicies) { 93 + errorMessage = 'Please review and accept the providers policies'; 94 + disableSubmit = false; 95 + return; 96 + } 97 + newHandle = formData.newHandle; 98 + if (selectedPds) { 99 + //Not happy about this unwrap, but it should always have a value on a legit PDS that I know of 100 + 101 + formData.newPds = `https://${cleanSelectedPds!}`; 102 + // Combine username and selected domain for the new handle 103 + if (selectedDomain !== 'custom') { 104 + newHandle = formData.newHandle + selectedDomain; 105 + } 106 + } 107 + 108 try { 109 110 if (showTwoFactorCodeInput) { ··· 123 migrator.migratePrefs = formData.migratePrefs; 124 migrator.migratePlcRecord = formData.migratePlcRecord; 125 126 + console.log(formData.newPds, newHandle); 127 128 updateStatusHandler('Starting migration...'); 129 showStatusMessage = true; ··· 132 formData.password, 133 formData.newPds, 134 formData.newEmail, 135 + newHandle, 136 formData.inviteCode, 137 updateStatusHandler, 138 formData.twoFactorCode, ··· 198 199 <!-- Second section: New account details --> 200 <div class="section"> 201 + <h2>{selectedPds ? `Setup for ${cleanSelectedPds}` : 'Setup for the new PDS'}</h2> 202 + {#if !selectedPds} 203 + <div class="form-group"> 204 + <label for="new-pds">New PDS (URL):</label> 205 + <input type="url" id="new-pds" name="newPds" placeholder="https://coolnewpds.com" 206 + required bind:value={formData.newPds}> 207 + </div> 208 + {/if} 209 210 <div class="form-group"> 211 <label for="new-email">New Email:</label> 212 + <input type="email" id="new-email" name="newEmail" 213 + placeholder="CanBeSameEmailAsTheOldPds@email.com" 214 required bind:value={formData.newEmail}> 215 </div> 216 217 + 218 <div class="form-group"> 219 <label for="new-handle">New Handle:</label> 220 + <div class={selectedPds ? 'input-group' : ''}> 221 + <input type="text" id="new-handle" name="newHandle" 222 + placeholder="{handlePlaceHolder}" 223 + required 224 + bind:value={formData.newHandle}> 225 + {#if selectedPds} 226 227 + <select bind:value={selectedDomain} class="domain-select"> 228 + {#each selectedPds?.availableUserDomains as domain (domain)} 229 + <option value={domain}>{domain}</option> 230 + {/each} 231 + <option value="custom">I have my own domain setup</option> 232 + 233 + </select> 234 + {/if} 235 + </div> 236 </div> 237 + 238 + {#if !selectedPds || selectedPds.inviteCodeRequired !== false} 239 + <div class="form-group"> 240 + <label for="invite-code">Invite Code:</label> 241 + <input type="text" id="invite-code" name="inviteCode" 242 + placeholder="Invite code from your new PDS (Leave blank if you don't have one)" 243 + bind:value={formData.inviteCode}> 244 + </div> 245 + {/if} 246 </div> 247 248 + 249 <div class="form-group"> 250 <button type="button" onclick={() => showAdvance = !showAdvance} id="advance" name="advance">Advance 251 Options ··· 301 </div> 302 {/if} 303 304 + {#if requiresAccept} 305 + <div class="section" style="text-align: left"> 306 + <h3>Provider policies</h3> 307 + <p> 308 + To migrate to {cleanSelectedPds}, you must review and accept: 309 + </p> 310 + <ul> 311 + {#if privacyUrl} 312 + <li><a href={privacyUrl} target="_blank" rel="noopener noreferrer">Privacy 313 + Policy</a></li> 314 + {/if} 315 + {#if tosUrl} 316 + <li><a href={tosUrl} target="_blank" rel="noopener noreferrer">Terms of Service</a></li> 317 + {/if} 318 + </ul> 319 + <div class="form-group"> 320 + <label for="accept-policies" class="moove-checkbox-label"> 321 + <input bind:checked={formData.acceptPolicies} type="checkbox" id="accept-policies" 322 + name="acceptPolicies" required> 323 + <span> 324 + I have read and accept 325 + 326 + </span> 327 + </label> 328 + </div> 329 + </div> 330 + {/if} 331 <p style="text-align: left">There are some risks that come with doing an account migration. 332 (Can view them 333 <a href="https://github.com/bluesky-social/pds/blob/main/ACCOUNT_MIGRATION.md#%EF%B8%8F-warning-%EF%B8%8F-%EF%B8%8F">here</a>) ··· 358 {/if} 359 360 <div> 361 + <button disabled={disableSubmit} 362 + type="submit">{selectedPds ? `MOOve to ${cleanSelectedPds}` : 'MOOve'}</button> 363 </div> 364 </form> 365 366 {:else} 367 + <SignThePapers migrator={migrator} newHandle={newHandle}/> 368 {/if} 369 </div>
web-ui/src/routes/moover/SignThePapers.svelte web-ui/src/routes/moover/[[pds]]/SignThePapers.svelte

History

1 round 0 comments
sign up or login to add to the discussion
1 commit
expand
PDS auto fill link
expand 0 comments
pull request successfully merged