Graphical PDS migrator for AT Protocol

otel instead of posthog

+1 -1
deno.json
···
"jsxPrecompileSkipElements": ["a", "img", "source", "body", "html", "head"],
"types": ["node"]
},
-
"unstable": ["kv"]
}
···
"jsxPrecompileSkipElements": ["a", "img", "source", "body", "html", "head"],
"types": ["node"]
},
+
"unstable": ["kv", "otel"]
}
+36 -28
islands/CredLogin.tsx
···
-
import { useState } from 'preact/hooks'
-
import { JSX } from 'preact'
export default function CredLogin() {
-
const [handle, setHandle] = useState('')
-
const [password, setPassword] = useState('')
-
const [error, setError] = useState<string | null>(null)
-
const [isPending, setIsPending] = useState(false)
const handleSubmit = async (e: JSX.TargetedEvent<HTMLFormElement>) => {
-
e.preventDefault()
-
if (!handle.trim() || !password.trim()) return
-
setError(null)
-
setIsPending(true)
try {
-
const response = await fetch('/api/cred/login', {
-
method: 'POST',
headers: {
-
'Content-Type': 'application/json',
},
body: JSON.stringify({ handle, password }),
-
})
if (!response.ok) {
-
const errorText = await response.text()
-
throw new Error(errorText || 'Login failed')
}
// Add a small delay before redirecting for better UX
-
await new Promise((resolve) => setTimeout(resolve, 500))
// Redirect to home page after successful login
-
globalThis.location.href = '/'
} catch (err) {
-
const message = err instanceof Error ? err.message : 'Login failed'
-
setError(message)
} finally {
-
setIsPending(false)
}
-
}
return (
<form onSubmit={handleSubmit}>
···
className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors"
/>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
-
This is your main account password, not an app password. This is required for migrations.
</p>
</div>
···
type="submit"
disabled={isPending}
className={`w-full px-4 py-2 rounded-md bg-blue-500 dark:bg-blue-600 text-white font-medium hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 relative ${
-
isPending ? 'opacity-90 cursor-not-allowed' : ''
}`}
>
-
<span className={isPending ? 'invisible' : ''}>Login with Password</span>
{isPending && (
<span className="absolute inset-0 flex items-center justify-center">
<svg
···
r="10"
stroke="currentColor"
strokeWidth="4"
-
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
-
></path>
</svg>
<span>Logging in...</span>
</span>
)}
</button>
</form>
-
)
}
···
+
import { useState } from "preact/hooks";
+
import { JSX } from "preact";
export default function CredLogin() {
+
const [handle, setHandle] = useState("");
+
const [password, setPassword] = useState("");
+
const [error, setError] = useState<string | null>(null);
+
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (e: JSX.TargetedEvent<HTMLFormElement>) => {
+
e.preventDefault();
+
if (!handle.trim() || !password.trim()) return;
+
setError(null);
+
setIsPending(true);
try {
+
const response = await fetch("/api/cred/login", {
+
method: "POST",
headers: {
+
"Content-Type": "application/json",
},
body: JSON.stringify({ handle, password }),
+
});
if (!response.ok) {
+
const errorText = await response.text();
+
throw new Error(errorText || "Login failed");
}
// Add a small delay before redirecting for better UX
+
await new Promise((resolve) => setTimeout(resolve, 500));
// Redirect to home page after successful login
+
globalThis.location.href = "/";
} catch (err) {
+
const message = err instanceof Error ? err.message : "Login failed";
+
setError(message);
} finally {
+
setIsPending(false);
}
+
};
return (
<form onSubmit={handleSubmit}>
···
className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors"
/>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
+
This is your main account password, not an app password. This is
+
required for migrations.
+
<br />
+
Ensure that 2 Factor Authentication is turned off before proceeding.
+
You can turn it back on after the migration is complete.
</p>
</div>
···
type="submit"
disabled={isPending}
className={`w-full px-4 py-2 rounded-md bg-blue-500 dark:bg-blue-600 text-white font-medium hover:bg-blue-600 dark:hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 relative ${
+
isPending ? "opacity-90 cursor-not-allowed" : ""
}`}
>
+
<span className={isPending ? "invisible" : ""}>
+
Login with Password
+
</span>
{isPending && (
<span className="absolute inset-0 flex items-center justify-center">
<svg
···
r="10"
stroke="currentColor"
strokeWidth="4"
+
>
+
</circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+
>
+
</path>
</svg>
<span>Logging in...</span>
</span>
)}
</button>
</form>
+
);
}
-114
islands/PostHogAnalytics.tsx
···
-
import { useEffect } from "preact/hooks";
-
-
interface PostHogConfig {
-
api_host: string;
-
[key: string]: unknown; // Allow additional optional properties
-
}
-
-
interface PostHogInstance {
-
__SV?: number;
-
_i: Array<[string, PostHogConfig, string?]>;
-
people?: PostHogPeople;
-
init: (apiKey: string, config: PostHogConfig, name?: string) => void;
-
capture: (event: string, properties?: Record<string, unknown>) => void;
-
identify: (distinctId: string, properties?: Record<string, unknown>) => void;
-
toString: (includeStub?: number) => string;
-
[key: string]: unknown;
-
}
-
-
interface PostHogPeople {
-
toString: () => string;
-
[key: string]: unknown;
-
}
-
-
declare global {
-
var posthog: PostHogInstance | undefined;
-
}
-
-
interface PostHogProps {
-
apiKey: string;
-
apiHost?: string;
-
}
-
-
export default function PostHogAnalytics({
-
apiKey,
-
apiHost = "https://us.i.posthog.com"
-
}: PostHogProps) {
-
useEffect(() => {
-
// PostHog initialization script
-
(function(t: Document, e: PostHogInstance) {
-
let o: string[];
-
let n: number;
-
let p: HTMLScriptElement;
-
let r: HTMLScriptElement | null;
-
-
if (e.__SV) return;
-
-
globalThis.posthog = e;
-
e._i = [];
-
e.init = function(i: string, s: PostHogConfig, a?: string) {
-
function g(target: Record<string, unknown>, methodName: string) {
-
const parts = methodName.split(".");
-
if (2 == parts.length) {
-
target = target[parts[0]] as Record<string, unknown>;
-
methodName = parts[1];
-
}
-
target[methodName] = function() {
-
(target as { push: (args: unknown[]) => void }).push([methodName].concat(Array.prototype.slice.call(arguments, 0)));
-
};
-
}
-
-
p = t.createElement("script");
-
p.type = "text/javascript";
-
p.crossOrigin = "anonymous";
-
p.async = true;
-
p.src = s.api_host.replace(".i.posthog.com", "-assets.i.posthog.com") + "/static/array.js";
-
r = t.getElementsByTagName("script")[0];
-
if (r && r.parentNode) {
-
r.parentNode.insertBefore(p, r);
-
}
-
-
let u: PostHogInstance = e;
-
if (typeof a !== "undefined") {
-
u = (e as Record<string, PostHogInstance>)[a] = {} as PostHogInstance;
-
u._i = [];
-
} else {
-
a = "posthog";
-
}
-
-
u.people = u.people || {} as PostHogPeople;
-
u.toString = function(includeStub?: number) {
-
let name = "posthog";
-
if ("posthog" !== a) {
-
name += "." + a;
-
}
-
if (!includeStub) {
-
name += " (stub)";
-
}
-
return name;
-
};
-
-
u.people.toString = function() {
-
return u.toString(1) + ".people (stub)";
-
};
-
-
o = "init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" ");
-
-
for (n = 0; n < o.length; n++) {
-
g(u as Record<string, unknown>, o[n]);
-
}
-
-
e._i.push([i, s, a]);
-
};
-
-
e.__SV = 1;
-
})(document, globalThis.posthog || {} as PostHogInstance);
-
-
// Initialize PostHog
-
if (globalThis.posthog) {
-
globalThis.posthog.init(apiKey, { api_host: apiHost });
-
}
-
}, [apiKey, apiHost]);
-
-
return null; // This component doesn't render anything visible
-
}
···
-15
islands/PostHogInitializer.tsx
···
-
import { useEffect } from "preact/hooks";
-
import posthog from "posthog-js";
-
-
interface Props {
-
apiKey: string;
-
apiHost: string;
-
}
-
-
export default function PostHogInitializer({ apiKey, apiHost }: Props) {
-
useEffect(() => {
-
posthog.default.init(apiKey, { api_host: apiHost });
-
}, [apiKey, apiHost]);
-
-
return null;
-
}
···
+4 -2
main.ts
···
const needsAuth = url.pathname.startsWith("/migrate");
// Skip auth check if not a protected route
-
if (!needsAuth || url.pathname === "/login" || url.pathname.startsWith("/api/")) {
return ctx.next();
}
try {
-
const session = await getSession(ctx.req)
console.log("[auth] Session:", session);
···
const needsAuth = url.pathname.startsWith("/migrate");
// Skip auth check if not a protected route
+
if (
+
!needsAuth || url.pathname === "/login" || url.pathname.startsWith("/api/")
+
) {
return ctx.next();
}
try {
+
const session = await getSession(ctx.req);
console.log("[auth] Session:", session);
+11 -8
routes/_app.tsx
···
import { type PageProps } from "fresh";
import Header from "../islands/Header.tsx";
-
import PostHogInitializer from "../islands/PostHogInitializer.tsx";
export default function App({ Component }: PageProps) {
-
const apiKey = Deno.env.get("PUBLIC_POSTHOG_KEY")!;
-
const apiHost = Deno.env.get("PUBLIC_POSTHOG_HOST")!;
-
return (
<html>
<head>
···
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="og:image" content="/og-image.jpg" />
<meta name="og:title" content="Airport" />
-
<meta name="og:description" content="
Airport is an AT Protocol PDS Migration Tool that allows you to seamlessly
migrate your account from one PDS to another.
-
" />
<meta name="og:locale" content="en_US" />
<title>Airport</title>
<link rel="stylesheet" href="/styles.css" />
</head>
-
<script defer src="https://cloud.umami.is/script.js" data-website-id={Deno.env.get("UMAMI_ID")}></script>
<body>
-
<PostHogInitializer apiKey={apiKey} apiHost={apiHost} />
<Header />
<main className="pt-8">
<Component />
···
import { type PageProps } from "fresh";
import Header from "../islands/Header.tsx";
export default function App({ Component }: PageProps) {
return (
<html>
<head>
···
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="og:image" content="/og-image.jpg" />
<meta name="og:title" content="Airport" />
+
<meta
+
name="og:description"
+
content="
Airport is an AT Protocol PDS Migration Tool that allows you to seamlessly
migrate your account from one PDS to another.
+
"
+
/>
<meta name="og:locale" content="en_US" />
<title>Airport</title>
<link rel="stylesheet" href="/styles.css" />
</head>
+
<script
+
defer
+
src="https://cloud.umami.is/script.js"
+
data-website-id={Deno.env.get("UMAMI_ID")}
+
>
+
</script>
<body>
<Header />
<main className="pt-8">
<Component />