this repo has no description
0
fork

Configure Feed

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

Handle repo takedowns, make admin dash a bit more mobile friendly (#232)

![CleanShot 2023-07-19 at 15 03
43](https://github.com/bluesky-social/indigo/assets/1617325/ba87d9a1-8f62-4ffc-a5d0-e4285491ce19)

authored by

Jaz and committed by
GitHub
bcece694 05f31a96

+442 -32
+31 -2
bgs/admin.go
··· 2 2 3 3 import ( 4 4 "errors" 5 + "net/http" 5 6 "strconv" 6 7 "strings" 7 8 8 9 "github.com/bluesky-social/indigo/models" 9 10 "github.com/labstack/echo/v4" 10 11 dto "github.com/prometheus/client_model/go" 12 + "gorm.io/gorm" 11 13 ) 12 14 13 15 func (bgs *BGS) handleAdminBlockRepoStream(e echo.Context) error { ··· 47 49 } 48 50 } 49 51 50 - return bgs.TakeDownRepo(ctx, did) 52 + err := bgs.TakeDownRepo(ctx, did) 53 + if err != nil { 54 + if errors.Is(err, gorm.ErrRecordNotFound) { 55 + return &echo.HTTPError{ 56 + Code: http.StatusNotFound, 57 + Message: "repo not found", 58 + } 59 + } 60 + return &echo.HTTPError{ 61 + Code: http.StatusInternalServerError, 62 + Message: err.Error(), 63 + } 64 + } 65 + return nil 51 66 } 52 67 53 68 func (bgs *BGS) handleAdminReverseTakedown(e echo.Context) error { 54 69 did := e.QueryParam("did") 55 70 ctx := e.Request().Context() 71 + err := bgs.ReverseTakedown(ctx, did) 56 72 57 - return bgs.ReverseTakedown(ctx, did) 73 + if err != nil { 74 + if errors.Is(err, gorm.ErrRecordNotFound) { 75 + return &echo.HTTPError{ 76 + Code: http.StatusNotFound, 77 + Message: "repo not found", 78 + } 79 + } 80 + return &echo.HTTPError{ 81 + Code: http.StatusInternalServerError, 82 + Message: err.Error(), 83 + } 84 + } 85 + 86 + return nil 58 87 } 59 88 60 89 func (bgs *BGS) handleAdminGetUpstreamConns(e echo.Context) error {
+55 -24
ts/bgs-dash/src/App.tsx
··· 12 12 import { useEffect } from "react"; 13 13 import Logout from "./components/Logout/Logout"; 14 14 import Domains from "./components/Domains/Domains"; 15 + import Repos from "./components/Repos/Repos"; 15 16 16 17 function classNames(...classes: string[]) { 17 18 return classes.filter(Boolean).join(" "); ··· 46 47 <RequireAuth> 47 48 <Nav /> 48 49 <main> 49 - <div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> 50 + <div className="mx-auto max-w-7xl px-2 py-6 sm:px-6 lg:px-8"> 50 51 <Dash /> 51 52 </div> 52 53 </main> ··· 61 62 <RequireAuth> 62 63 <Nav /> 63 64 <main> 64 - <div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> 65 + <div className="mx-auto max-w-7xl px-2 py-6 sm:px-6 lg:px-8"> 65 66 <Domains /> 66 67 </div> 67 68 </main> 68 69 </RequireAuth> 69 70 ), 71 + requrieAuth: true, 72 + }, 73 + { 74 + path: "/repo_takedowns", 75 + name: "Repo Takedowns", 76 + element: ( 77 + <RequireAuth> 78 + <Nav /> 79 + <main> 80 + <div className="mx-auto max-w-7xl px-2 py-6 sm:px-6 lg:px-8"> 81 + <Repos /> 82 + </div> 83 + </main> 84 + </RequireAuth> 85 + ), 86 + requrieAuth: true, 70 87 }, 71 88 { 72 89 path: "/login", ··· 75 92 <> 76 93 <Nav /> 77 94 <main> 78 - <div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> 95 + <div className="mx-auto max-w-7xl px-2 py-6 sm:px-6 lg:px-8"> 79 96 <Login /> 80 97 </div> 81 98 </main> ··· 91 108 <> 92 109 <Nav /> 93 110 <main> 94 - <div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8"> 111 + <div className="mx-auto max-w-7xl py-6 px-2 sm:px-6 lg:px-8"> 95 112 <Logout /> 96 113 </div> 97 114 </main> ··· 169 186 170 187 <Disclosure.Panel className="md:hidden"> 171 188 <div className="space-y-1 px-2 pb-3 pt-2 sm:px-3"> 172 - {router.routes.map((item) => ( 173 - <Disclosure.Button 174 - key={item.id} 175 - as="a" 176 - href={item.path} 177 - className={classNames( 178 - router.state.location.pathname === item.path 179 - ? "bg-gray-900 text-white" 180 - : "text-gray-300 hover:bg-gray-700 hover:text-white", 181 - "block rounded-md px-3 py-2 text-base font-medium" 182 - )} 183 - aria-current={ 184 - router.state.location.pathname === item.path 185 - ? "page" 186 - : undefined 187 - } 188 - > 189 - {item.path} 190 - </Disclosure.Button> 191 - ))} 189 + {routes.map((item) => 190 + (isAuthed && item.hideIfAuth) || 191 + (!isAuthed && item.requrieAuth) ? null : ( 192 + <Disclosure.Button 193 + key={item.path} 194 + className={classNames( 195 + router.state.location.pathname === item.path 196 + ? "bg-gray-900 text-white" 197 + : "text-gray-300 hover:bg-gray-700 hover:text-white", 198 + "block rounded-md px-3 py-2 text-base font-medium" 199 + )} 200 + > 201 + <NavLink 202 + key={item.path} 203 + to={item.path || "/"} 204 + className={({ isActive }) => 205 + classNames( 206 + isActive 207 + ? "bg-gray-900 text-white" 208 + : "text-gray-300 hover:bg-gray-700 hover:text-white", 209 + "rounded-md px-3 py-2 text-sm font-medium" 210 + ) 211 + } 212 + aria-current={ 213 + router.state.location.pathname === item.path 214 + ? "page" 215 + : undefined 216 + } 217 + > 218 + {item.name} 219 + </NavLink> 220 + </Disclosure.Button> 221 + ) 222 + )} 192 223 </div> 193 224 </Disclosure.Panel> 194 225 </>
+1 -1
ts/bgs-dash/src/components/Dash/ConfirmModal.tsx
··· 42 42 </Transition.Child> 43 43 44 44 <div className="fixed inset-0 z-10 overflow-y-auto"> 45 - <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> 45 + <div className="flex min-h-full items-center justify-center p-4 text-center sm:items-center sm:p-0"> 46 46 <Transition.Child 47 47 as={Fragment} 48 48 enter="ease-out duration-300"
+2 -2
ts/bgs-dash/src/components/Dash/Dash.tsx
··· 359 359 A list of all PDS connections and their current status. 360 360 </p> 361 361 </div> 362 - <div> 362 + <div className="inline-flex mt-5 sm:mt-0"> 363 363 <Switch.Group as="div" className="flex items-center justify-between"> 364 364 <span className="flex flex-grow flex-col mr-5"> 365 365 <Switch.Label as="span" className="text-gray-900" passive> ··· 402 402 </div> 403 403 404 404 <div className="mt-8 flow-root"> 405 - <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg sm:rounded-b-none"> 405 + <div className="shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg sm:rounded-b-none overflow-x-auto"> 406 406 <table className="min-w-full divide-y divide-gray-300"> 407 407 <thead className="bg-gray-50"> 408 408 <tr>
+1 -1
ts/bgs-dash/src/components/Domains/ConfirmDomainBanModal.tsx
··· 44 44 </Transition.Child> 45 45 46 46 <div className="fixed inset-0 z-10 overflow-y-auto"> 47 - <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> 47 + <div className="flex min-h-full items-center justify-center p-4 text-center sm:items-center sm:p-0"> 48 48 <Transition.Child 49 49 as={Fragment} 50 50 enter="ease-out duration-300"
+2 -2
ts/bgs-dash/src/components/Domains/Domains.tsx
··· 230 230 domains are also banned. 231 231 </p> 232 232 </div> 233 - <div className="flex-grow"> 233 + <div className="flex-grow mt-5 sm:mt-0"> 234 234 <div className="max-w-3xl w-full"> 235 235 <label 236 236 htmlFor="email" ··· 269 269 </div> 270 270 271 271 <div className="mt-8 flow-root"> 272 - <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg sm:rounded-b-none"> 272 + <div className="overflow-x-auto shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg sm:rounded-b-none"> 273 273 <table className="min-w-full divide-y divide-gray-300"> 274 274 <thead className="bg-gray-50"> 275 275 <tr>
+107
ts/bgs-dash/src/components/Repos/ConfirmRepoTakedownModal.tsx
··· 1 + import { Fragment, useState } from "react"; 2 + import { Dialog, Transition } from "@headlessui/react"; 3 + import { XCircleIcon } from "@heroicons/react/24/outline"; 4 + 5 + interface ConfirmModalProps { 6 + action: { 7 + type: "takedown" | "untakedown"; 8 + repo: string; 9 + }; 10 + onConfirm: () => void; 11 + onCancel: () => void; 12 + } 13 + 14 + const ConfirmRepoTakedownModal = ({ 15 + action, 16 + onConfirm, 17 + onCancel, 18 + }: ConfirmModalProps) => { 19 + const [open, setOpen] = useState(true); 20 + 21 + const handleConfirm = () => { 22 + onConfirm(); 23 + setOpen(false); 24 + }; 25 + 26 + const handleCancel = () => { 27 + onCancel(); 28 + setOpen(false); 29 + }; 30 + 31 + return ( 32 + <Transition.Root show={open} as={Fragment}> 33 + <Dialog as="div" className="relative z-10" onClose={setOpen}> 34 + <Transition.Child 35 + as={Fragment} 36 + enter="ease-out duration-300" 37 + enterFrom="opacity-0" 38 + enterTo="opacity-100" 39 + leave="ease-in duration-200" 40 + leaveFrom="opacity-100" 41 + leaveTo="opacity-0" 42 + > 43 + <div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> 44 + </Transition.Child> 45 + 46 + <div className="fixed inset-0 z-10 overflow-y-auto"> 47 + <div className="flex min-h-full items-center justify-center p-4 text-center sm:items-center sm:p-0"> 48 + <Transition.Child 49 + as={Fragment} 50 + enter="ease-out duration-300" 51 + enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" 52 + enterTo="opacity-100 translate-y-0 sm:scale-100" 53 + leave="ease-in duration-200" 54 + leaveFrom="opacity-100 translate-y-0 sm:scale-100" 55 + leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" 56 + > 57 + <Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6"> 58 + <div> 59 + <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100"> 60 + <XCircleIcon 61 + className="h-6 w-6 text-yellow-600" 62 + aria-hidden="true" 63 + /> 64 + </div> 65 + <div className="mt-3 text-center sm:mt-5"> 66 + <Dialog.Title 67 + as="h3" 68 + className="text-lg font-medium leading-6 text-gray-900" 69 + > 70 + {`${action.type[0].toLocaleUpperCase()}${action.type.substring( 71 + 1 72 + )}`}{" "} 73 + Repo 74 + </Dialog.Title> 75 + <div className="mt-2"> 76 + <p className="text-sm text-gray-500"> 77 + Are you sure you want to {action.type} {action.repo}? 78 + </p> 79 + </div> 80 + </div> 81 + </div> 82 + <div className="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense"> 83 + <button 84 + type="button" 85 + className="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:col-start-2" 86 + onClick={handleConfirm} 87 + > 88 + Confirm 89 + </button> 90 + <button 91 + type="button" 92 + className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-medium text-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:col-start-1 sm:mt-0" 93 + onClick={handleCancel} 94 + > 95 + Cancel 96 + </button> 97 + </div> 98 + </Dialog.Panel> 99 + </Transition.Child> 100 + </div> 101 + </div> 102 + </Dialog> 103 + </Transition.Root> 104 + ); 105 + }; 106 + 107 + export default ConfirmRepoTakedownModal;
+243
ts/bgs-dash/src/components/Repos/Repos.tsx
··· 1 + import { FC, useEffect, useState } from "react"; 2 + import Notification, { 3 + NotificationMeta, 4 + NotificationType, 5 + } from "../Notification/Notification"; 6 + 7 + import { BGS_HOST } from "../../constants"; 8 + 9 + import { useNavigate } from "react-router-dom"; 10 + import ConfirmRepoTakedownModal from "./ConfirmRepoTakedownModal"; 11 + import { 12 + ShieldCheckIcon, 13 + ShieldExclamationIcon, 14 + } from "@heroicons/react/24/outline"; 15 + 16 + const Repos: FC<{}> = () => { 17 + const [repoToTakedown, setRepoToTakedown] = useState<string>(""); 18 + 19 + // Notification Management 20 + const [shouldShowNotification, setShouldShowNotification] = 21 + useState<boolean>(false); 22 + const [notification, setNotification] = useState<NotificationMeta>({ 23 + message: "", 24 + alertType: "", 25 + }); 26 + 27 + // Modal state management 28 + const [modalAction, setModalAction] = useState<{ 29 + repo: string; 30 + type: "takedown" | "untakedown"; 31 + } | null>(null); 32 + const [modalConfirm, setModalConfirm] = useState<() => void>(() => {}); 33 + const [modalCancel, setModalCancel] = useState<() => void>(() => {}); 34 + 35 + const [adminToken, setAdminToken] = useState<string>( 36 + localStorage.getItem("admin_route_token") || "" 37 + ); 38 + const navigate = useNavigate(); 39 + 40 + const setAlertWithTimeout = ( 41 + type: NotificationType, 42 + message: string, 43 + dismiss: boolean 44 + ) => { 45 + setNotification({ 46 + message, 47 + alertType: type, 48 + autodismiss: dismiss, 49 + }); 50 + setShouldShowNotification(true); 51 + }; 52 + 53 + useEffect(() => { 54 + const token = localStorage.getItem("admin_route_token"); 55 + if (token) { 56 + setAdminToken(token); 57 + } else { 58 + navigate("/login"); 59 + } 60 + }, []); 61 + 62 + const requestTakedownRepo = (repo: string) => { 63 + fetch(`${BGS_HOST}/admin/repo/takeDown`, { 64 + method: "POST", 65 + headers: { 66 + "Content-Type": "application/json", 67 + Authorization: `Bearer ${adminToken}`, 68 + }, 69 + body: JSON.stringify({ 70 + did: repo, 71 + }), 72 + }) 73 + .then((res) => res.json()) 74 + .then((res) => { 75 + if (res.error) { 76 + setAlertWithTimeout( 77 + "failure", 78 + `Failed to takedown repo: ${res.error}`, 79 + true 80 + ); 81 + } else { 82 + setAlertWithTimeout( 83 + "success", 84 + `Successfully tookdown repo ${repo}`, 85 + true 86 + ); 87 + } 88 + }) 89 + .catch((err) => { 90 + setAlertWithTimeout("failure", `Failed to takedown repo: ${err}`, true); 91 + }); 92 + }; 93 + 94 + const requestUntakedownRepo = (repo: string) => { 95 + fetch(`${BGS_HOST}/admin/repo/reverseTakedown`, { 96 + method: "POST", 97 + headers: { 98 + "Content-Type": "application/json", 99 + Authorization: `Bearer ${adminToken}`, 100 + }, 101 + body: JSON.stringify({ 102 + did: repo, 103 + }), 104 + }) 105 + .then((res) => res.json()) 106 + 107 + .then((res) => { 108 + if (res.error !== 200) { 109 + setAlertWithTimeout( 110 + "failure", 111 + `Failed to untakedown repo: ${res.error}`, 112 + true 113 + ); 114 + } else { 115 + setAlertWithTimeout( 116 + "success", 117 + `Successfully untookdown repo ${repo}`, 118 + true 119 + ); 120 + } 121 + }) 122 + .catch((err) => { 123 + setAlertWithTimeout( 124 + "failure", 125 + `Failed to untakedown repo: ${err}`, 126 + true 127 + ); 128 + }); 129 + }; 130 + 131 + const handleTakedownRepo = ( 132 + repo: string, 133 + type: "takedown" | "untakedown" 134 + ) => { 135 + setModalAction({ repo: repo, type }); 136 + 137 + setModalConfirm(() => { 138 + return () => { 139 + if (type === "takedown") requestTakedownRepo(repo); 140 + else requestUntakedownRepo(repo); 141 + 142 + setModalAction(null); 143 + }; 144 + }); 145 + 146 + setModalCancel(() => { 147 + return () => { 148 + setModalAction(null); 149 + }; 150 + }); 151 + }; 152 + 153 + return ( 154 + <div className="mx-auto max-w-full"> 155 + {shouldShowNotification ? ( 156 + <Notification 157 + message={notification.message} 158 + alertType={notification.alertType} 159 + subMessage={notification.subMessage} 160 + autodismiss={notification.autodismiss} 161 + unshow={() => { 162 + setShouldShowNotification(false); 163 + setNotification({ message: "", alertType: "" }); 164 + }} 165 + show={shouldShowNotification} 166 + ></Notification> 167 + ) : ( 168 + <></> 169 + )} 170 + <div className="sm:flex sm:items-center"> 171 + <div className="sm:flex-auto"> 172 + <h1 className="text-2xl font-semibold leading-6 text-gray-900"> 173 + Repo Takedowns 174 + </h1> 175 + <p className="mt-2 text-sm text-gray-700"> 176 + Takedown a repo to purge it from the BGS history and reject all 177 + future events for it. 178 + </p> 179 + </div> 180 + </div> 181 + <div className="flex-grow mt-5"> 182 + <div className="max-w-3xl w-full"> 183 + <label 184 + htmlFor="email" 185 + className="block text-sm font-medium leading-6 text-gray-900" 186 + > 187 + Repo DID 188 + </label> 189 + <div className="mt-2 inline-flex flex-col sm:flex-row"> 190 + <input 191 + type="text" 192 + name="repo" 193 + id="repo" 194 + className="block w-72 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" 195 + placeholder="did:plc:abadperson" 196 + value={repoToTakedown} 197 + onChange={(e) => { 198 + setRepoToTakedown(e.target.value); 199 + }} 200 + /> 201 + <div className="inline-flex mt-4 sm:mt-0"> 202 + <button 203 + type="button" 204 + onClick={() => { 205 + handleTakedownRepo(repoToTakedown.trim(), "takedown"); 206 + }} 207 + className="ml-0 sm:ml-2 inline-flex whitespace-nowrap items-center gap-x-1.5 rounded-md bg-red-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" 208 + > 209 + <ShieldExclamationIcon 210 + className="-ml-0.5 h-5 w-5" 211 + aria-hidden="true" 212 + /> 213 + Takedown Repo 214 + </button> 215 + <button 216 + type="button" 217 + onClick={() => { 218 + handleTakedownRepo(repoToTakedown.trim(), "untakedown"); 219 + }} 220 + className="ml-2 inline-flex whitespace-nowrap items-center gap-x-1.5 rounded-md bg-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600" 221 + > 222 + <ShieldCheckIcon 223 + className="-ml-0.5 h-5 w-5" 224 + aria-hidden="true" 225 + /> 226 + Untakedown Repo 227 + </button> 228 + </div> 229 + </div> 230 + </div> 231 + </div> 232 + {modalAction && ( 233 + <ConfirmRepoTakedownModal 234 + action={modalAction} 235 + onConfirm={modalConfirm} 236 + onCancel={modalCancel} 237 + /> 238 + )} 239 + </div> 240 + ); 241 + }; 242 + 243 + export default Repos;