this repo has no description
10
fork

Configure Feed

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

feat(admin): list unverified projects for verification

Made-with: Cursor

+156 -3
+6 -1
i18n/messages/en.tsx
··· 418 418 categories: { 419 419 app: "App", 420 420 accountProvider: "Account Provider", 421 - moderator: "Moderator", 421 + moderator: "Moderation", 422 422 infrastructure: "Infrastructure", 423 423 developerTool: "Developer Tool", 424 424 all: "All", ··· 707 707 proactiveSuccessSuffix: "is now verified.", 708 708 proactiveNotFound: 709 709 "No active registry profile was found for that handle or DID.", 710 + unverifiedHeading: "Not yet verified", 711 + emptyUnverified: 712 + "Every active profile is either verified or waiting in the request queue.", 713 + unverifiedNeverRequestedStatus: "No verification request yet.", 714 + unverifiedDeniedStatus: "Previously denied or revoked.", 710 715 grantedHeading: "Currently verified", 711 716 emptyPending: "No requests in the queue.", 712 717 emptyGranted: "No projects are verified yet.",
+67
islands/AdminIconAccessVerifyButton.tsx
··· 1 + import { useSignal } from "@preact/signals"; 2 + 3 + interface Props { 4 + did: string; 5 + label: string; 6 + doneLabel: string; 7 + errorPrefix: string; 8 + } 9 + 10 + /** 11 + * Compact one-click verifier for existing profiles in the admin roster. 12 + * The request queue keeps its richer Grant/Deny row; this is for profiles 13 + * that simply have not been verified yet. 14 + */ 15 + export default function AdminIconAccessVerifyButton(p: Props) { 16 + const status = useSignal< 17 + | { kind: "idle" } 18 + | { kind: "submitting" } 19 + | { kind: "done" } 20 + | { kind: "error"; text: string } 21 + >({ kind: "idle" }); 22 + 23 + const onClick = async () => { 24 + status.value = { kind: "submitting" }; 25 + try { 26 + const r = await fetch( 27 + `/api/admin/icon-access/${encodeURIComponent(p.did)}/grant`, 28 + { method: "POST" }, 29 + ); 30 + if (!r.ok) throw new Error(await r.text()); 31 + status.value = { kind: "done" }; 32 + } catch (err) { 33 + status.value = { 34 + kind: "error", 35 + text: err instanceof Error ? err.message : String(err), 36 + }; 37 + } 38 + }; 39 + 40 + if (status.value.kind === "done") { 41 + return ( 42 + <span class="admin-status-badge admin-status-badge--approved"> 43 + {p.doneLabel} 44 + </span> 45 + ); 46 + } 47 + 48 + const submitting = status.value.kind === "submitting"; 49 + 50 + return ( 51 + <> 52 + <button 53 + type="button" 54 + class="profile-form-button-primary" 55 + onClick={onClick} 56 + disabled={submitting} 57 + > 58 + {submitting ? "..." : p.label} 59 + </button> 60 + {status.value.kind === "error" && ( 61 + <p class="admin-icon-row-error"> 62 + {p.errorPrefix}: {status.value.text} 63 + </p> 64 + )} 65 + </> 66 + ); 67 + }
+29
lib/registry.ts
··· 621 621 }); 622 622 } 623 623 624 + /** Active profiles that are not verified and are not already in the request queue. */ 625 + export async function listUnverifiedIconAccess(): Promise< 626 + IconAccessLookupRow[] 627 + > { 628 + return await withDb(async (c) => { 629 + const r = await c.execute(` 630 + SELECT did, handle, name, icon_access_status 631 + FROM profile 632 + WHERE takedown_status IS NULL 633 + AND (icon_access_status IS NULL OR icon_access_status = 'denied') 634 + ORDER BY indexed_at DESC 635 + `); 636 + return r.rows.map((row) => { 637 + const x = row as unknown as { 638 + did: string; 639 + handle: string; 640 + name: string; 641 + icon_access_status: string | null; 642 + }; 643 + return { 644 + did: x.did, 645 + handle: x.handle, 646 + name: x.name, 647 + status: x.icon_access_status, 648 + }; 649 + }); 650 + }); 651 + } 652 + 624 653 /** Currently-verified projects, most-recently-granted first. */ 625 654 export async function listGrantedIconAccess(): Promise<GrantedIconAccessRow[]> { 626 655 return await withDb(async (c) => {
+53 -2
routes/admin/icon-access.tsx
··· 10 10 import AdminIconAccessGrant from "../../islands/AdminIconAccessGrant.tsx"; 11 11 import AdminIconAccessRow from "../../islands/AdminIconAccessRow.tsx"; 12 12 import AdminIconAccessRevoke from "../../islands/AdminIconAccessRevoke.tsx"; 13 + import AdminIconAccessVerifyButton from "../../islands/AdminIconAccessVerifyButton.tsx"; 13 14 import { getMessages } from "../../i18n/mod.ts"; 14 15 import type { Locale } from "../../i18n/mod.ts"; 15 16 import { 16 17 type GrantedIconAccessRow, 18 + type IconAccessLookupRow, 17 19 type IconAccessRequestRow, 18 20 listGrantedIconAccess, 19 21 listPendingIconAccess, 22 + listUnverifiedIconAccess, 20 23 } from "../../lib/registry.ts"; 21 24 import { buildAccountMenuProps } from "../../lib/account-menu-props.ts"; 22 25 23 26 export const handler = define.handlers({ 24 27 async GET(ctx) { 25 - const [pending, granted] = await Promise.all([ 28 + const [pending, unverified, granted] = await Promise.all([ 26 29 listPendingIconAccess().catch(() => [] as IconAccessRequestRow[]), 30 + listUnverifiedIconAccess().catch(() => [] as IconAccessLookupRow[]), 27 31 listGrantedIconAccess().catch(() => [] as GrantedIconAccessRow[]), 28 32 ]); 29 33 return ctx.render( 30 34 <Page 31 35 account={buildAccountMenuProps(ctx.state)} 32 36 pending={pending} 37 + unverified={unverified} 33 38 granted={granted} 34 39 locale={ctx.state.locale} 35 40 />, ··· 40 45 interface PageProps { 41 46 account: ReturnType<typeof buildAccountMenuProps>; 42 47 pending: IconAccessRequestRow[]; 48 + unverified: IconAccessLookupRow[]; 43 49 granted: GrantedIconAccessRow[]; 44 50 locale: Locale; 45 51 } 46 52 47 - function Page({ account, pending, granted, locale }: PageProps) { 53 + function Page({ account, pending, unverified, granted, locale }: PageProps) { 48 54 const t = getMessages(locale).admin; 49 55 const ti = t.iconAccess; 50 56 return ( ··· 102 108 error: t.errorPrefix, 103 109 }} 104 110 /> 111 + ))} 112 + </div> 113 + )} 114 + 115 + <h2 class="text-card mt-6">{ti.unverifiedHeading}</h2> 116 + {unverified.length === 0 117 + ? <p class="text-body admin-empty">{ti.emptyUnverified}</p> 118 + : ( 119 + <div class="admin-icon-list"> 120 + {unverified.map((row) => ( 121 + <div class="admin-icon-row" key={row.did}> 122 + <div class="admin-icon-row-meta"> 123 + <p class="admin-icon-row-name"> 124 + <strong>{row.name}</strong> 125 + <span class="admin-icon-row-handle"> 126 + <a 127 + href={`/explore/${ 128 + encodeURIComponent(row.handle) 129 + }`} 130 + target="_blank" 131 + rel="noopener noreferrer" 132 + class="text-link-button" 133 + > 134 + @{row.handle} ↗ 135 + </a> 136 + </span> 137 + </p> 138 + <p class="admin-icon-row-did"> 139 + <code>{row.did}</code> 140 + </p> 141 + <p class="admin-icon-row-uploaded"> 142 + {row.status === "denied" 143 + ? ti.unverifiedDeniedStatus 144 + : ti.unverifiedNeverRequestedStatus} 145 + </p> 146 + </div> 147 + <div class="admin-icon-row-actions"> 148 + <AdminIconAccessVerifyButton 149 + did={row.did} 150 + label={ti.grant} 151 + doneLabel={ti.markedGranted} 152 + errorPrefix={t.errorPrefix} 153 + /> 154 + </div> 155 + </div> 105 156 ))} 106 157 </div> 107 158 )}
+1
vite.config.ts
··· 7 7 { 8 8 name: "atmosphere-fresh-island-dev-imports", 9 9 apply: "serve", 10 + enforce: "post", 10 11 transformIndexHtml(html) { 11 12 /** 12 13 * Fresh's dev SSR currently emits inline module imports like