Public: anyone can read this; the crew can change it.
An app's private destinations on its settings page are §5 rows in a setting list, with an outline Grant #3842
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3825-an-app-s-private-destinations-on-its-set → main 3 commits fork
2 files
changed +45
−37
src/web/pages/settings.ts +26 −37
| @@ -1185,42 +1185,31 @@ export async function appDomainDelete(ctx: Ctx): Promise<Response> { | ||
| 1185 | 1185 | function appEgressView(base: string, egress: EgressView): Html { |
| 1186 | 1186 | const at = (destination: string) => |
| 1187 | 1187 | `${base}/settings/app/egress/${encodeURIComponent(destination)}`; |
| 1188 | const rows = egress.rows.map( | |
| 1189 | (r) => | |
| 1190 | html`<li> | |
| 1191 | <div class="row"> | |
| 1192 | <span class="row-lead">${icon("globe")}</span> | |
| 1193 | <span class="row-main"> | |
| 1194 | <span class="row-title" | |
| 1195 | ><span class="mono">${r.destination}</span> ${r.granted | |
| 1196 | ? badge("granted", { tone: "success" }) | |
| 1197 | : badge("not granted", { tone: "attn" })} | |
| 1198 | ${r.asked ? null : badge("no longer asked")}</span | |
| 1199 | > | |
| 1200 | <span class="row-meta" | |
| 1201 | >${r.granted && r.grantedAt !== null | |
| 1202 | ? html`Granted by ${r.grantedBy ?? "someone"} | |
| 1203 | ${time(r.grantedAt)}` | |
| 1204 | : "Nothing reaches it until a site admin grants it."}</span | |
| 1205 | > | |
| 1206 | </span> | |
| 1207 | ${when( | |
| 1208 | egress.canChange, | |
| 1209 | () => | |
| 1210 | html`<span class="row-trail"> | |
| 1211 | ${r.granted | |
| 1212 | ? confirmAction({ | |
| 1213 | action: `${at(r.destination)}/revoke`, | |
| 1214 | verb: "Revoke", | |
| 1215 | size: "sm", | |
| 1216 | }) | |
| 1217 | : html`<form method="post" action="${at(r.destination)}/grant"> | |
| 1218 | ${btn("Grant", { size: "sm" })} | |
| 1219 | </form>`} | |
| 1220 | </span>`, | |
| 1221 | )} | |
| 1222 | </div> | |
| 1223 | </li>`, | |
| 1188 | const rows = egress.rows.map((r) => | |
| 1189 | row({ | |
| 1190 | lead: icon("globe"), | |
| 1191 | title: html`<span class="mono">${r.destination}</span> ${r.granted | |
| 1192 | ? badge("granted", { tone: "success" }) | |
| 1193 | : badge("not granted", { tone: "attn" })} | |
| 1194 | ${r.asked ? null : badge("no longer asked")}`, | |
| 1195 | meta: | |
| 1196 | r.granted && r.grantedAt !== null | |
| 1197 | ? html`Granted by ${r.grantedBy ?? "someone"} ${time(r.grantedAt)}` | |
| 1198 | : html`Nothing reaches it until a site admin grants it.`, | |
| 1199 | trail: egress.canChange | |
| 1200 | ? html`<div class="row-fields"> | |
| 1201 | ${r.granted | |
| 1202 | ? confirmAction({ | |
| 1203 | action: `${at(r.destination)}/revoke`, | |
| 1204 | verb: "Revoke", | |
| 1205 | size: "sm", | |
| 1206 | }) | |
| 1207 | : html`<form method="post" action="${at(r.destination)}/grant"> | |
| 1208 | ${btn("Grant", { size: "sm", variant: "outline" })} | |
| 1209 | </form>`} | |
| 1210 | </div>` | |
| 1211 | : html``, | |
| 1212 | }), | |
| 1224 | 1213 | ); |
| 1225 | 1214 | const changes = egress.changes.map( |
| 1226 | 1215 | (c) => |
| @@ -1241,7 +1230,7 @@ function appEgressView(base: string, egress: EgressView): Html { | ||
| 1241 | 1230 | : " Only a site admin grants or revokes."} |
| 1242 | 1231 | </p> |
| 1243 | 1232 | </div> |
| 1244 | <ul class="list"> | |
| 1233 | <ul class="list setting-list"> | |
| 1245 | 1234 | ${rows} |
| 1246 | 1235 | </ul> |
| 1247 | 1236 | ${when( |
test/app-egress-grants.test.ts +19
| @@ -187,6 +187,25 @@ describe("private destinations asked for", () => { | ||
| 187 | 187 | expect(cyPage).not.toContain(">Grant<"); |
| 188 | 188 | }); |
| 189 | 189 | |
| 190 | test("the section is a setting list: an outline Grant for a site admin, no buttons for anyone else", async () => { | |
| 191 | const section = async (cookie: string) => { | |
| 192 | const page = await (await web(cookie, "/cy/arm/settings/app")).text(); | |
| 193 | const from = page.indexOf("Private destinations asked for"); | |
| 194 | return page.slice(from, page.indexOf("</ul>", from)); | |
| 195 | }; | |
| 196 | const admin = await section(adaCookie); | |
| 197 | expect(admin).toContain('<ul class="list setting-list">'); | |
| 198 | expect(admin).toContain('class="row-fields"'); | |
| 199 | expect(admin).toMatch(/<button[^>]*data-variant="outline"[^>]*>\s*Grant\s*<\/button>/); | |
| 200 | expect(admin).toContain("Revoke"); | |
| 201 | const owner = await section(cyCookie); | |
| 202 | expect(owner).toContain('<ul class="list setting-list">'); | |
| 203 | expect(owner).toContain(CAMERA); | |
| 204 | expect(owner).not.toContain("Grant<"); | |
| 205 | expect(owner).not.toContain("Revoke"); | |
| 206 | expect(owner).not.toContain("/settings/app/egress/"); | |
| 207 | }); | |
| 208 | ||
| 190 | 209 | test("the page grants too, and refuses a foreign origin", async () => { |
| 191 | 210 | const path = `/cy/arm/settings/app/egress/${encodeURIComponent(CAMERA)}/grant`; |
| 192 | 211 | await web(adaCookie, path, "https://evil.example"); |
new above
offline