eny.space Landingpage
1
fork

Configure Feed

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

feat(dashboard): implemente account deletion

+188 -24
+80 -8
app/api/pds/atproto/accounts/route.ts
··· 1 1 import { NextResponse } from "next/server"; 2 2 3 - import { getPdsBaseUrlFromService, getPdsServiceForCurrentUser } from "../helpers"; 3 + import { 4 + getPdsBaseUrlFromService, 5 + getPdsServiceForCurrentUser, 6 + } from "../helpers"; 4 7 5 8 function toBasicAuth(user: string, pass: string) { 6 9 return `Basic ${Buffer.from(`${user}:${pass}`).toString("base64")}`; ··· 10 13 try { 11 14 const { service } = await getPdsServiceForCurrentUser(); 12 15 13 - const adminPassword = service?.encrypted_config?.adminPassword as string | undefined; 16 + const adminPassword = service?.encrypted_config?.adminPassword as 17 + | string 18 + | undefined; 14 19 if (!adminPassword) { 15 20 return NextResponse.json( 16 21 { message: "Missing PDS admin credentials" }, 17 - { status: 500 }, 22 + { status: 500 } 18 23 ); 19 24 } 20 25 ··· 33 38 if (!listRes.ok) { 34 39 const body = await listRes.json().catch(() => ({})); 35 40 return NextResponse.json( 36 - { message: "Failed to list repos", status: listRes.status, upstream: body }, 37 - { status: 502 }, 41 + { 42 + message: "Failed to list repos", 43 + status: listRes.status, 44 + upstream: body, 45 + }, 46 + { status: 502 } 38 47 ); 39 48 } 40 49 ··· 46 55 } 47 56 48 57 // Step 2: get account details for all DIDs (admin Basic auth accepted here) 49 - const infoUrl = new URL(`${pdsBaseUrl}/xrpc/com.atproto.admin.getAccountInfos`); 58 + const infoUrl = new URL( 59 + `${pdsBaseUrl}/xrpc/com.atproto.admin.getAccountInfos` 60 + ); 50 61 dids.forEach((did) => infoUrl.searchParams.append("dids", did)); 51 62 52 63 const infoRes = await fetch(infoUrl.toString(), { ··· 60 71 if (!infoRes.ok) { 61 72 const body = await infoRes.json().catch(() => ({})); 62 73 return NextResponse.json( 63 - { message: "Failed to fetch account details", status: infoRes.status, upstream: body }, 64 - { status: 502 }, 74 + { 75 + message: "Failed to fetch account details", 76 + status: infoRes.status, 77 + upstream: body, 78 + }, 79 + { status: 502 } 65 80 ); 66 81 } 67 82 ··· 73 88 return NextResponse.json({ message }, { status }); 74 89 } 75 90 } 91 + 92 + export async function DELETE(req: Request) { 93 + try { 94 + const { did } = (await req.json()) as { did?: string }; 95 + if (!did) { 96 + return NextResponse.json( 97 + { message: "Missing required field: did" }, 98 + { status: 400 } 99 + ); 100 + } 101 + 102 + const { service } = await getPdsServiceForCurrentUser(); 103 + const adminPassword = service?.encrypted_config?.adminPassword as 104 + | string 105 + | undefined; 106 + if (!adminPassword) { 107 + return NextResponse.json( 108 + { message: "Missing PDS admin credentials" }, 109 + { status: 500 } 110 + ); 111 + } 112 + 113 + const pdsBaseUrl = getPdsBaseUrlFromService(service); 114 + const authHeader = toBasicAuth("admin", String(adminPassword).trim()); 115 + 116 + const res = await fetch( 117 + `${pdsBaseUrl}/xrpc/com.atproto.admin.deleteAccount`, 118 + { 119 + method: "POST", 120 + headers: { 121 + "Content-Type": "application/json", 122 + Authorization: authHeader, 123 + }, 124 + body: JSON.stringify({ did }), 125 + } 126 + ); 127 + 128 + if (!res.ok) { 129 + const body = await res.json().catch(() => ({})); 130 + return NextResponse.json( 131 + { 132 + message: "Failed to delete account", 133 + status: res.status, 134 + upstream: body, 135 + }, 136 + { status: 502 } 137 + ); 138 + } 139 + 140 + return NextResponse.json({ success: true }); 141 + } catch (error) { 142 + const message = error instanceof Error ? error.message : "Unknown error"; 143 + const status = (error as any)?.status ?? 500; 144 + return NextResponse.json({ message }, { status }); 145 + } 146 + } 147 +
+108 -16
app/dashboard/user-dashboard-client.tsx
··· 196 196 handle: string; 197 197 email?: string; 198 198 indexedAt?: string; 199 + deactivatedAt?: string; 199 200 }; 200 201 202 + function AccountRow({ account, onRefresh }: { account: PdsAccount; onRefresh: () => void }) { 203 + const [confirmDelete, setConfirmDelete] = useState(false); 204 + const [busy, setBusy] = useState(false); 205 + const [rowError, setRowError] = useState<string | null>(null); 206 + const isDeactivated = Boolean(account.deactivatedAt); 207 + 208 + const deactivate = async () => { 209 + setBusy(true); 210 + setRowError(null); 211 + try { 212 + const res = await fetch("/api/pds/atproto/accounts", { 213 + method: "PATCH", 214 + headers: { "Content-Type": "application/json" }, 215 + body: JSON.stringify({ did: account.did, active: isDeactivated }), 216 + }); 217 + const payload = await res.json().catch(() => ({})); 218 + if (!res.ok) throw new Error(payload?.message || "Failed to update account"); 219 + onRefresh(); 220 + } catch (e) { 221 + setRowError(e instanceof Error ? e.message : String(e)); 222 + } finally { 223 + setBusy(false); 224 + } 225 + }; 226 + 227 + const deleteAccount = async () => { 228 + setBusy(true); 229 + setRowError(null); 230 + try { 231 + const res = await fetch("/api/pds/atproto/accounts", { 232 + method: "DELETE", 233 + headers: { "Content-Type": "application/json" }, 234 + body: JSON.stringify({ did: account.did }), 235 + }); 236 + const payload = await res.json().catch(() => ({})); 237 + if (!res.ok) throw new Error(payload?.message || "Failed to delete account"); 238 + onRefresh(); 239 + } catch (e) { 240 + setRowError(e instanceof Error ? e.message : String(e)); 241 + setConfirmDelete(false); 242 + } finally { 243 + setBusy(false); 244 + } 245 + }; 246 + 247 + return ( 248 + <div className="py-3 text-sm space-y-1"> 249 + <div className="flex items-start justify-between gap-4"> 250 + <div className="space-y-0.5 min-w-0"> 251 + <div className="flex items-center gap-2"> 252 + <Paragraph className="font-medium text-white truncate">{account.handle}</Paragraph> 253 + {isDeactivated && ( 254 + <span className="text-xs text-amber-400/80 border border-amber-400/30 rounded px-1">deactivated</span> 255 + )} 256 + </div> 257 + {account.email && ( 258 + <Paragraph className="text-xs text-white/50 truncate">{account.email}</Paragraph> 259 + )} 260 + <Paragraph className="font-mono text-xs text-white/30 truncate">{account.did}</Paragraph> 261 + </div> 262 + <div className="flex items-center gap-2 shrink-0"> 263 + {account.indexedAt && ( 264 + <Paragraph className="text-xs text-white/40 whitespace-nowrap"> 265 + {new Date(account.indexedAt).toLocaleDateString()} 266 + </Paragraph> 267 + )} 268 + <button 269 + onClick={deactivate} 270 + disabled={busy} 271 + className="text-xs text-white/40 hover:text-amber-300 transition-colors disabled:opacity-40" 272 + > 273 + {busy ? "…" : isDeactivated ? "Reactivate" : "Deactivate"} 274 + </button> 275 + {confirmDelete ? ( 276 + <div className="flex items-center gap-1"> 277 + <button 278 + onClick={deleteAccount} 279 + disabled={busy} 280 + className="text-xs text-rose-400 hover:text-rose-300 transition-colors disabled:opacity-40" 281 + > 282 + Confirm 283 + </button> 284 + <button 285 + onClick={() => setConfirmDelete(false)} 286 + className="text-xs text-white/30 hover:text-white/60 transition-colors" 287 + > 288 + Cancel 289 + </button> 290 + </div> 291 + ) : ( 292 + <button 293 + onClick={() => setConfirmDelete(true)} 294 + className="text-xs text-white/40 hover:text-rose-400 transition-colors" 295 + > 296 + Delete 297 + </button> 298 + )} 299 + </div> 300 + </div> 301 + {rowError && ( 302 + <Paragraph className="text-xs text-rose-300 break-all">{rowError}</Paragraph> 303 + )} 304 + </div> 305 + ); 306 + } 307 + 201 308 function UsersSection() { 202 309 const [accounts, setAccounts] = useState<PdsAccount[]>([]); 203 310 const [loading, setLoading] = useState(true); ··· 244 351 {accounts.length > 0 && ( 245 352 <div className="divide-y divide-white/5"> 246 353 {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> 354 + <AccountRow key={account.did} account={account} onRefresh={load} /> 263 355 ))} 264 356 </div> 265 357 )}