eny.space Landingpage
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(dashboard): add user display

+157 -3
+75
app/api/pds/atproto/accounts/route.ts
··· 1 + import { NextResponse } from "next/server"; 2 + 3 + import { getPdsBaseUrlFromService, getPdsServiceForCurrentUser } from "../helpers"; 4 + 5 + function toBasicAuth(user: string, pass: string) { 6 + return `Basic ${Buffer.from(`${user}:${pass}`).toString("base64")}`; 7 + } 8 + 9 + export async function GET() { 10 + try { 11 + const { service } = await getPdsServiceForCurrentUser(); 12 + 13 + const adminPassword = service?.encrypted_config?.adminPassword as string | undefined; 14 + if (!adminPassword) { 15 + return NextResponse.json( 16 + { message: "Missing PDS admin credentials" }, 17 + { status: 500 }, 18 + ); 19 + } 20 + 21 + const pdsBaseUrl = getPdsBaseUrlFromService(service); 22 + const authHeader = toBasicAuth("admin", String(adminPassword).trim()); 23 + 24 + // Step 1: list all repos (public endpoint, gives us DIDs) 25 + const listUrl = new URL(`${pdsBaseUrl}/xrpc/com.atproto.sync.listRepos`); 26 + listUrl.searchParams.set("limit", "100"); 27 + 28 + const listRes = await fetch(listUrl.toString(), { 29 + cache: "no-store", 30 + headers: { Accept: "application/json" }, 31 + }); 32 + 33 + if (!listRes.ok) { 34 + const body = await listRes.json().catch(() => ({})); 35 + return NextResponse.json( 36 + { message: "Failed to list repos", status: listRes.status, upstream: body }, 37 + { status: 502 }, 38 + ); 39 + } 40 + 41 + const { repos = [] } = await listRes.json(); 42 + const dids: string[] = repos.map((r: { did: string }) => r.did); 43 + 44 + if (dids.length === 0) { 45 + return NextResponse.json({ accounts: [] }); 46 + } 47 + 48 + // Step 2: get account details for all DIDs (admin Basic auth accepted here) 49 + const infoUrl = new URL(`${pdsBaseUrl}/xrpc/com.atproto.admin.getAccountInfos`); 50 + dids.forEach((did) => infoUrl.searchParams.append("dids", did)); 51 + 52 + const infoRes = await fetch(infoUrl.toString(), { 53 + cache: "no-store", 54 + headers: { 55 + Accept: "application/json", 56 + Authorization: authHeader, 57 + }, 58 + }); 59 + 60 + if (!infoRes.ok) { 61 + const body = await infoRes.json().catch(() => ({})); 62 + return NextResponse.json( 63 + { message: "Failed to fetch account details", status: infoRes.status, upstream: body }, 64 + { status: 502 }, 65 + ); 66 + } 67 + 68 + const { infos = [] } = await infoRes.json(); 69 + return NextResponse.json({ accounts: infos }); 70 + } catch (error) { 71 + const message = error instanceof Error ? error.message : "Unknown error"; 72 + const status = (error as any)?.status ?? 500; 73 + return NextResponse.json({ message }, { status }); 74 + } 75 + }
+82 -3
app/dashboard/user-dashboard-client.tsx
··· 48 48 } 49 49 50 50 return ( 51 - <div className="grid gap-6 md:grid-cols-2"> 52 - <CreateUserSection pdsBareHost={pdsBareHost} /> 53 - <InviteSection /> 51 + <div className="space-y-6"> 52 + <div className="grid gap-6 md:grid-cols-2"> 53 + <CreateUserSection pdsBareHost={pdsBareHost} /> 54 + <InviteSection /> 55 + </div> 56 + <UsersSection /> 54 57 </div> 55 58 ); 56 59 } ··· 187 190 </section> 188 191 ); 189 192 } 193 + 194 + type PdsAccount = { 195 + did: string; 196 + handle: string; 197 + email?: string; 198 + indexedAt?: string; 199 + }; 200 + 201 + function UsersSection() { 202 + const [accounts, setAccounts] = useState<PdsAccount[]>([]); 203 + const [loading, setLoading] = useState(true); 204 + const [error, setError] = useState<string | null>(null); 205 + 206 + const load = async () => { 207 + setLoading(true); 208 + setError(null); 209 + try { 210 + const res = await fetch("/api/pds/atproto/accounts"); 211 + const payload = await res.json().catch(() => ({})); 212 + if (!res.ok) throw new Error(`${payload?.message || "Failed to load accounts"} — ${JSON.stringify(payload?.upstream ?? {})}`); 213 + setAccounts(payload?.accounts ?? []); 214 + } catch (e) { 215 + setError(e instanceof Error ? e.message : String(e)); 216 + } finally { 217 + setLoading(false); 218 + } 219 + }; 220 + 221 + useEffect(() => { load(); }, []); 222 + 223 + return ( 224 + <section className="space-y-3 rounded-md border border-white/10 bg-white/5 p-4"> 225 + <div className="flex items-center justify-between"> 226 + <Paragraph className="text-sm font-semibold text-white">Users</Paragraph> 227 + <button 228 + onClick={load} 229 + disabled={loading} 230 + className="text-xs text-white/40 hover:text-white/80 transition-colors disabled:opacity-40" 231 + > 232 + {loading ? "Loading…" : "Refresh"} 233 + </button> 234 + </div> 235 + 236 + {error && ( 237 + <Paragraph className="text-sm text-rose-300">{error}</Paragraph> 238 + )} 239 + 240 + {!loading && !error && accounts.length === 0 && ( 241 + <Paragraph className="text-sm text-white/40">No accounts found.</Paragraph> 242 + )} 243 + 244 + {accounts.length > 0 && ( 245 + <div className="divide-y divide-white/5"> 246 + {accounts.map((account) => ( 247 + <div key={account.did} className="flex items-start justify-between gap-4 py-3 text-sm"> 248 + <div className="space-y-0.5 min-w-0"> 249 + <Paragraph className="font-medium text-white truncate"> 250 + {account.handle} 251 + </Paragraph> 252 + {account.email && ( 253 + <Paragraph className="text-xs text-white/50 truncate">{account.email}</Paragraph> 254 + )} 255 + <Paragraph className="font-mono text-xs text-white/30 truncate">{account.did}</Paragraph> 256 + </div> 257 + {account.indexedAt && ( 258 + <Paragraph className="text-xs text-white/40 whitespace-nowrap shrink-0"> 259 + {new Date(account.indexedAt).toLocaleDateString()} 260 + </Paragraph> 261 + )} 262 + </div> 263 + ))} 264 + </div> 265 + )} 266 + </section> 267 + ); 268 + }