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
Changed files
+214 -33
.tangled
images
web-ui
.tangled/images/network.webp

This is a binary file and will not be displayed.

+2 -2
README.md
···
- Looking for the old pds moover for simple code to fork
check [here](https://tangled.org/@baileytownsend.dev/pds-moover/tree/803d8a70b7100c9e14df3402277441050e0f6194), if
-
you'd like to see the newer front end check [here](./web/ui-code/src)
- Want to run your own instance of PDS MOOver? [check this docker compose](./compose.selfhost.yml). It should have all
the
services in one easy `docker compose up`, just don't forget to create a `.env` from [.env.template](.env.template)
···
## Do you have a pretty picture to show how the network looks?
yes. Thanks to [Orual](https://bsky.app/profile/nonbinary.computer)
-
![](./web/public/PDSMOOver.excalidraw.png)
···
- Looking for the old pds moover for simple code to fork
check [here](https://tangled.org/@baileytownsend.dev/pds-moover/tree/803d8a70b7100c9e14df3402277441050e0f6194), if
+
you'd like to see the newer front end check [here](./web-ui)
- Want to run your own instance of PDS MOOver? [check this docker compose](./compose.selfhost.yml). It should have all
the
services in one easy `docker compose up`, just don't forget to create a `.env` from [.env.template](.env.template)
···
## Do you have a pretty picture to show how the network looks?
yes. Thanks to [Orual](https://bsky.app/profile/nonbinary.computer)
+
![](.tangled/images/network.webp)
+1 -1
justfile
···
docker buildx build \
--platform linux/arm64,linux/amd64 \
--tag fatfingers23/moover_ui:latest \
-
--tag fatfingers23/moover_ui:0.0.2 \
--file Dockerfiles/web-ui.Dockerfile \
--push .
···
docker buildx build \
--platform linux/arm64,linux/amd64 \
--tag fatfingers23/moover_ui:latest \
+
--tag fatfingers23/moover_ui:0.0.3 \
--file Dockerfiles/web-ui.Dockerfile \
--push .
test.compose.yml
+1
web-ui/package.json
···
"lint": "eslint ."
},
"dependencies": {
"@atcute/client": "^4.0.5",
"@atcute/lexicons": "^1.2.2",
"@pds-moover/lexicons": "^1.0.1",
···
"lint": "eslint ."
},
"dependencies": {
+
"@atcute/atproto": "^3.1.9",
"@atcute/client": "^4.0.5",
"@atcute/lexicons": "^1.2.2",
"@pds-moover/lexicons": "^1.0.1",
+10
web-ui/pnpm-lock.yaml
···
.:
dependencies:
'@atcute/client':
specifier: ^4.0.5
version: 4.0.5
···
packages:
'@atcute/cbor@2.2.7':
resolution: {integrity: sha512-/mwAF0gnokOphceZqFq3uzMGdd8sbw5y6bxF8CRutRkCCUcpjjpJc5fkLwhxyGgOveF3mZuHE6p7t/+IAqb7Aw==}
···
snapshots:
'@atcute/cbor@2.2.7':
dependencies:
'@atcute/cid': 2.2.6
···
.:
dependencies:
+
'@atcute/atproto':
+
specifier: ^3.1.9
+
version: 3.1.9
'@atcute/client':
specifier: ^4.0.5
version: 4.0.5
···
packages:
+
'@atcute/atproto@3.1.9':
+
resolution: {integrity: sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w==}
+
'@atcute/cbor@2.2.7':
resolution: {integrity: sha512-/mwAF0gnokOphceZqFq3uzMGdd8sbw5y6bxF8CRutRkCCUcpjjpJc5fkLwhxyGgOveF3mZuHE6p7t/+IAqb7Aw==}
···
snapshots:
+
'@atcute/atproto@3.1.9':
+
dependencies:
+
'@atcute/lexicons': 1.2.2
+
'@atcute/cbor@2.2.7':
dependencies:
'@atcute/cid': 2.2.6
+8 -8
web-ui/src/app.html
···
<!doctype html>
<html lang="en">
-
<head>
-
<meta charset="utf-8" />
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
-
%sveltekit.head%
-
</head>
-
<body data-sveltekit-preload-data="hover">
-
<div style="display: contents">%sveltekit.body%</div>
-
</body>
</html>
···
<!doctype html>
<html lang="en">
+
<head>
+
<meta charset="utf-8"/>
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+
%sveltekit.head%
+
</head>
+
<body data-sveltekit-preload-data="hover">
+
<div style="display: contents">%sveltekit.body%</div>
+
</body>
</html>
+33
web-ui/src/lib/assets/style.css
···
box-sizing: border-box;
}
.cow-image {
height: 150px;
margin: 20px 0 8px 0;
···
box-sizing: border-box;
}
+
/* Input group for handle with domain dropdown */
+
.input-group {
+
display: flex;
+
width: 100%;
+
}
+
+
.input-group input {
+
flex: 1;
+
border-top-right-radius: 0;
+
border-bottom-right-radius: 0;
+
border-right: none;
+
}
+
+
.input-group .domain-select {
+
padding: 8px;
+
border: 1px solid rgba(128, 128, 128, 0.5);
+
border-top-left-radius: 0;
+
border-bottom-left-radius: 0;
+
border-top-right-radius: 4px;
+
border-bottom-right-radius: 4px;
+
background-color: #1a1a1a;
+
color: rgba(255, 255, 255, 0.87);
+
cursor: pointer;
+
min-width: 120px;
+
}
+
+
@media (prefers-color-scheme: light) {
+
.input-group .domain-select {
+
background-color: #f9f9f9;
+
color: #213547;
+
}
+
}
+
.cow-image {
height: 150px;
margin: 20px 0 8px 0;
+3 -2
web-ui/src/lib/components/NavBar.svelte
···
];
</script>
-
<header class="navbar" role="banner">
<div class="navbar-inner">
<a class="brand" href={resolve('/')}>PDS MOOver</a>
···
<nav id="primary-navigation" class="nav-links {open ? 'open' : ''}" aria-label="Primary"
>
{#each links as link (link.path)}
-
<a href={resolve(link.path)} class={page.url.pathname === link.path ? 'active' : '' } onclick={() => open = false}>{link.text}</a>
{/each}
</nav>
</div>
···
];
</script>
+
<header class="navbar">
<div class="navbar-inner">
<a class="brand" href={resolve('/')}>PDS MOOver</a>
···
<nav id="primary-navigation" class="nav-links {open ? 'open' : ''}" aria-label="Primary"
>
{#each links as link (link.path)}
+
<a href={resolve(link.path)} class={page.url.pathname.startsWith(link.path) ? 'active' : '' }
+
onclick={() => open = false}>{link.text}</a>
{/each}
</nav>
</div>
+34
web-ui/src/routes/moover/[[pds]]/+page.server.ts
···
···
+
import type {PageServerLoad} from './$types';
+
import {Client, simpleFetchHandler} from '@atcute/client';
+
import type {} from '@atcute/atproto';
+
import {env} from '$env/dynamic/private';
+
+
export const load: PageServerLoad = async ({params}) => {
+
+
if (!params.pds) {
+
return {pdsOptions: null, intinalDomain: null};
+
}
+
+
const allowedPds = env.PDS_AUTOFILL.split(',');
+
if (!allowedPds.includes(params.pds.toLowerCase())) {
+
console.error('PDS not allowed', params.pds);
+
return {pdsOptions: null, intinalDomain: null};
+
}
+
+
try {
+
const handler = simpleFetchHandler({service: `https://${params.pds}`});
+
const rpc = new Client({handler});
+
const {ok, data} = await rpc.get('com.atproto.server.describeServer', {})
+
if (!ok) {
+
console.error('Failed to describe the PDS server', data);
+
return {pds: null};
+
}
+
return {
+
pdsOptions: data,
+
intinalDomain: data?.availableUserDomains[0] ?? ''
+
};
+
} catch (e) {
+
console.error('Failed to describe the PDS server', e);
+
return {pdsOptions: null, intinalDomain: null};
+
}
+
};
+122 -20
web-ui/src/routes/moover/+page.svelte web-ui/src/routes/moover/[[pds]]/+page.svelte
···
import {Migrator} from '@pds-moover/moover';
import SignThePapers from './SignThePapers.svelte';
let formData = $state({
handle: '',
password: '',
···
inviteCode: null,
twoFactorCode: null,
confirmation: false,
// Advanced options
createNewAccount: true,
migrateRepo: true,
···
let errorMessage: null | string = $state(null);
let statusMessage: null | string = $state(null);
const updateStatusHandler = (status: string) => {
statusMessage = status;
}
···
return;
}
try {
if (showTwoFactorCodeInput) {
···
migrator.migratePrefs = formData.migratePrefs;
migrator.migratePlcRecord = formData.migratePlcRecord;
-
console.log(migrator);
updateStatusHandler('Starting migration...');
showStatusMessage = true;
···
formData.password,
formData.newPds,
formData.newEmail,
-
formData.newHandle,
formData.inviteCode,
updateStatusHandler,
formData.twoFactorCode,
···
<!-- Second section: New account details -->
<div class="section">
-
<h2>Setup for the new PDS</h2>
-
<div class="form-group">
-
<label for="new-pds">New PDS (URL):</label>
-
<input type="url" id="new-pds" name="newPds" placeholder="https://coolnewpds.com"
-
required bind:value={formData.newPds}>
-
</div>
<div class="form-group">
<label for="new-email">New Email:</label>
-
<input type="email" id="new-email" name="newEmail" placeholder="CanBeSameEmailAsTheOldPds@email.com"
required bind:value={formData.newEmail}>
</div>
<div class="form-group">
<label for="new-handle">New Handle:</label>
-
<input type="text" id="new-handle" name="newHandle"
-
placeholder="username.newpds.com or mycooldomain.com" required
-
bind:value={formData.newHandle}>
-
</div>
-
<div class="form-group">
-
<label for="invite-code">Invite Code:</label>
-
<input type="text" id="invite-code" name="inviteCode"
-
placeholder="Invite code from your new PDS (Leave blank if you don't have one)"
-
bind:value={formData.inviteCode}>
</div>
</div>
<div class="form-group">
<button type="button" onclick={() => showAdvance = !showAdvance} id="advance" name="advance">Advance
Options
···
</div>
{/if}
<p style="text-align: left">There are some risks that come with doing an account migration.
(Can view them
<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>)
···
{/if}
<div>
-
<button disabled={disableSubmit} type="submit">MOOve</button>
</div>
</form>
{:else}
-
<SignThePapers migrator={migrator} newHandle={formData.newHandle}/>
{/if}
</div>
···
import {Migrator} from '@pds-moover/moover';
import SignThePapers from './SignThePapers.svelte';
+
let {data} = $props();
+
+
let selectedPds = $derived(data.pdsOptions);
+
let cleanSelectedPds = $derived(selectedPds?.did.replace('did:web:', ''));
+
//Kept as a "global" state to handle logic of passing the full handle that is used to SignThePapers
+
let newHandle = $state('');
+
+
let selectedDomain = $state(data.intinalDomain);
+
+
let handlePlaceHolder = $derived(
+
selectedPds ? `username${selectedDomain === 'custom' ? '' : `${selectedPds?.availableUserDomains[0]}`} or mydomain.com` : 'username.newpds.com or mycooldomain.com')
+
+
+
$effect(() => {
+
if (!selectedPds) return;
+
+
if (selectedDomain == 'custom') return;
+
+
+
if (formData.newHandle.includes('.')) {
+
// When a period is typed, force custom domain selection
+
selectedDomain = 'custom';
+
} else {
+
// If user clears the dot and we have provider domains, fall back to first option
+
if ((selectedPds?.availableUserDomains?.length ?? 0) > 0 && selectedDomain === 'custom') {
+
selectedDomain = selectedPds!.availableUserDomains[0]!
+
}
+
}
+
});
+
let formData = $state({
handle: '',
password: '',
···
inviteCode: null,
twoFactorCode: null,
confirmation: false,
+
// Acceptance of provider policies (when required by selected PDS)
+
acceptPolicies: false,
// Advanced options
createNewAccount: true,
migrateRepo: true,
···
let errorMessage: null | string = $state(null);
let statusMessage: null | string = $state(null);
+
// Links that may require acceptance prior to migration from the selected PDS
+
const privacyUrl = $derived(selectedPds?.links?.privacyPolicy);
+
const tosUrl = $derived(selectedPds?.links?.termsOfService);
+
const requiresAccept = $derived(!!(privacyUrl || tosUrl));
+
const updateStatusHandler = (status: string) => {
statusMessage = status;
}
···
return;
}
+
// If the selected PDS provides policy or privacy links, require explicit acceptance
+
if (requiresAccept && !formData.acceptPolicies) {
+
errorMessage = 'Please review and accept the providers policies';
+
disableSubmit = false;
+
return;
+
}
+
newHandle = formData.newHandle;
+
if (selectedPds) {
+
//Not happy about this unwrap, but it should always have a value on a legit PDS that I know of
+
+
formData.newPds = `https://${cleanSelectedPds!}`;
+
// Combine username and selected domain for the new handle
+
if (selectedDomain !== 'custom') {
+
newHandle = formData.newHandle + selectedDomain;
+
}
+
}
+
try {
if (showTwoFactorCodeInput) {
···
migrator.migratePrefs = formData.migratePrefs;
migrator.migratePlcRecord = formData.migratePlcRecord;
+
console.log(formData.newPds, newHandle);
updateStatusHandler('Starting migration...');
showStatusMessage = true;
···
formData.password,
formData.newPds,
formData.newEmail,
+
newHandle,
formData.inviteCode,
updateStatusHandler,
formData.twoFactorCode,
···
<!-- Second section: New account details -->
<div class="section">
+
<h2>{selectedPds ? `Setup for ${cleanSelectedPds}` : 'Setup for the new PDS'}</h2>
+
{#if !selectedPds}
+
<div class="form-group">
+
<label for="new-pds">New PDS (URL):</label>
+
<input type="url" id="new-pds" name="newPds" placeholder="https://coolnewpds.com"
+
required bind:value={formData.newPds}>
+
</div>
+
{/if}
<div class="form-group">
<label for="new-email">New Email:</label>
+
<input type="email" id="new-email" name="newEmail"
+
placeholder="CanBeSameEmailAsTheOldPds@email.com"
required bind:value={formData.newEmail}>
</div>
+
<div class="form-group">
<label for="new-handle">New Handle:</label>
+
<div class={selectedPds ? 'input-group' : ''}>
+
<input type="text" id="new-handle" name="newHandle"
+
placeholder="{handlePlaceHolder}"
+
required
+
bind:value={formData.newHandle}>
+
{#if selectedPds}
+
<select bind:value={selectedDomain} class="domain-select">
+
{#each selectedPds?.availableUserDomains as domain (domain)}
+
<option value={domain}>{domain}</option>
+
{/each}
+
<option value="custom">I have my own domain setup</option>
+
+
</select>
+
{/if}
+
</div>
</div>
+
+
{#if !selectedPds || selectedPds.inviteCodeRequired !== false}
+
<div class="form-group">
+
<label for="invite-code">Invite Code:</label>
+
<input type="text" id="invite-code" name="inviteCode"
+
placeholder="Invite code from your new PDS (Leave blank if you don't have one)"
+
bind:value={formData.inviteCode}>
+
</div>
+
{/if}
</div>
+
<div class="form-group">
<button type="button" onclick={() => showAdvance = !showAdvance} id="advance" name="advance">Advance
Options
···
</div>
{/if}
+
{#if requiresAccept}
+
<div class="section" style="text-align: left">
+
<h3>Provider policies</h3>
+
<p>
+
To migrate to {cleanSelectedPds}, you must review and accept:
+
</p>
+
<ul>
+
{#if privacyUrl}
+
<li><a href={privacyUrl} target="_blank" rel="noopener noreferrer">Privacy
+
Policy</a></li>
+
{/if}
+
{#if tosUrl}
+
<li><a href={tosUrl} target="_blank" rel="noopener noreferrer">Terms of Service</a></li>
+
{/if}
+
</ul>
+
<div class="form-group">
+
<label for="accept-policies" class="moove-checkbox-label">
+
<input bind:checked={formData.acceptPolicies} type="checkbox" id="accept-policies"
+
name="acceptPolicies" required>
+
<span>
+
I have read and accept
+
+
</span>
+
</label>
+
</div>
+
</div>
+
{/if}
<p style="text-align: left">There are some risks that come with doing an account migration.
(Can view them
<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>)
···
{/if}
<div>
+
<button disabled={disableSubmit}
+
type="submit">{selectedPds ? `MOOve to ${cleanSelectedPds}` : 'MOOve'}</button>
</div>
</form>
{:else}
+
<SignThePapers migrator={migrator} newHandle={newHandle}/>
{/if}
</div>
web-ui/src/routes/moover/SignThePapers.svelte web-ui/src/routes/moover/[[pds]]/SignThePapers.svelte