Public: anyone can read this; the crew can change it.
The front door's Made here shows the forge owner's apps and pins, never an app a signed-up stranger made #11906
Check · Review · Land · Live — No review yet.
scout/forge:scout/11902-the-front-door-s-made-here-shows-the-for → main 2 commits fork
3 files
changed +52
−6
src/actions/front.ts +15
| @@ -247,6 +247,18 @@ export function frontKey(app: App): string { | ||
| 247 | 247 | return String(row?.n); |
| 248 | 248 | } |
| 249 | 249 | |
| 250 | /** | |
| 251 | * The repositories a signed-out visitor is offered by name: the shelf's own | |
| 252 | * set, featured first, so the jump palette never lists more than the door. | |
| 253 | */ | |
| 254 | export function doorRepos(app: App): Repo[] { | |
| 255 | const page = frontPage(app); | |
| 256 | const seen = new Set<string>(); | |
| 257 | return [...(page.featured === undefined ? [] : [page.featured]), ...page.shelf] | |
| 258 | .map((e) => e.repo) | |
| 259 | .filter((r) => !seen.has(r.id) && seen.add(r.id)); | |
| 260 | } | |
| 261 | ||
| 250 | 262 | /** The front page as a stranger is shown it; `request` is told what the key cost. */ |
| 251 | 263 | export function frontPage(app: App, now = Date.now(), request?: Request): FrontPage { |
| 252 | 264 | const key = |
| @@ -357,6 +369,9 @@ function buildFrontPage(app: App, now: number): FrontPage { | ||
| 357 | 369 | const ranked = rankFront( |
| 358 | 370 | publicApps |
| 359 | 371 | .filter((r) => !pinnedIds.has(r.repoId) && !picks.hidden.has(r.repoId)) |
| 372 | // The door shows the forge's own makers; a stranger's app is found on | |
| 373 | // /explore or pinned by an admin. Filtered here, before any ranking is kept. | |
| 374 | .filter((r) => owners.get(repos.get(r.repoId)!.ownerId)?.isAdmin === true) | |
| 360 | 375 | .map((r) => entry(repos.get(r.repoId)!, false)) |
| 361 | 376 | .filter(tileable) |
| 362 | 377 | .map((e) => ({ ...e, id: e.repo.id, owner: e.repo.ownerName })), |
src/web/layout.ts +10 −6
| @@ -17,6 +17,7 @@ import { administersAny } from "../core/admin_repos.ts"; | ||
| 17 | 17 | import { appsForRepos } from "../core/apps.ts"; |
| 18 | 18 | import { foundryHref } from "../actions/foundry_door.ts"; |
| 19 | 19 | import { appsRuntime } from "../apps/runtime.ts"; |
| 20 | import { doorRepos } from "../actions/front.ts"; | |
| 20 | 21 | import { actionRows, peopleRows, settingsRows } from "./palette_rows.ts"; |
| 21 | 22 | import { wordClip } from "../text/word_clip.ts"; |
| 22 | 23 | import { absorbedForNav } from "../actions/features.ts"; |
| @@ -479,12 +480,15 @@ function paletteIndex(ctx: Ctx, extra: PaletteItem[]): Html { | ||
| 479 | 480 | ? listReposByOwner(ctx.app.db, ctx.auth.user.username, ctx.auth.user, wanted) |
| 480 | 481 | : []; |
| 481 | 482 | const taken = new Set(mine.map((r) => r.id)); |
| 482 | const repos = [ | |
| 483 | ...mine, | |
| 484 | ...listReposVisibleTo(ctx.app.db, ctx.auth.user, wanted).filter( | |
| 485 | (r) => !taken.has(r.id), | |
| 486 | ), | |
| 487 | ].slice(0, 30); | |
| 483 | // A signed-out visitor is offered what the front door shows, no more. | |
| 484 | const repos = ctx.auth.user | |
| 485 | ? [ | |
| 486 | ...mine, | |
| 487 | ...listReposVisibleTo(ctx.app.db, ctx.auth.user, wanted).filter( | |
| 488 | (r) => !taken.has(r.id), | |
| 489 | ), | |
| 490 | ].slice(0, 30) | |
| 491 | : doorRepos(ctx.app).slice(0, 30); | |
| 488 | 492 | const apps = appsForRepos( |
| 489 | 493 | ctx.app.db, |
| 490 | 494 | repos.map((r) => r.id), |
test/front.test.ts +27
| @@ -393,6 +393,8 @@ describe("the front page", () => { | ||
| 393 | 393 | }); |
| 394 | 394 | |
| 395 | 395 | test("ranked, not one owner's: forks lift a stranger's app; stopped, failed and unpictured apps are not shown", async () => { |
| 396 | // The door shows apps of the forge's admins unless pinned (#11902); zed is one here. | |
| 397 | app.db.query(`UPDATE user SET is_admin = 1 WHERE id = ?1`).run(other.id); | |
| 396 | 398 | const forked = await make(other, "loved"); |
| 397 | 399 | for (let n = 0; n < 3; n++) { |
| 398 | 400 | const f = await createRepo(app, member, { |
| @@ -783,6 +785,7 @@ describe("the front page", () => { | ||
| 783 | 785 | test("a site admin's weights change the order, within bounds; a value out of bounds is refused (#767)", async () => { |
| 784 | 786 | // One app with forks by people, one with ships: which leads depends on |
| 785 | 787 | // what counts. |
| 788 | app.db.query(`UPDATE user SET is_admin = 1 WHERE id = ?1`).run(other.id); | |
| 786 | 789 | const forked = await make(other, "weighed-forks"); |
| 787 | 790 | const forkerA = await createUser(app.db, { origin: "seed", trusted: true, username: "forker-a", email: "[email protected]", password: "a-good-passphrase" }); |
| 788 | 791 | if (!forkerA.ok) throw new Error(forkerA.error); |
| @@ -836,4 +839,28 @@ describe("the front page", () => { | ||
| 836 | 839 | expect(reset.status).toBe(303); |
| 837 | 840 | expect(frontWeights(app.db)).toEqual(DEFAULT_WEIGHTS); |
| 838 | 841 | }); |
| 842 | ||
| 843 | test("the door shows the owner's apps and pinned ones, never an unpinned stranger's (#11902)", async () => { | |
| 844 | const mine = await make(admin, "door-mine"); | |
| 845 | const pinned = await make(member, "door-pinned"); | |
| 846 | const loose = await make(member, "door-loose"); | |
| 847 | expect((await put(adminToken, { pinned: ["mia/door-pinned"], hidden: [] })).status).toBe(200); | |
| 848 | forgetFrontScores(app); | |
| 849 | const json = (await (await fetch(`${base}/api/v1/front`)).json()) as any; | |
| 850 | const names: string[] = [json.featured?.repo, ...json.apps.map((a: any) => a.repo)]; | |
| 851 | expect(names).toContain("owner/door-mine"); | |
| 852 | expect(names).toContain("mia/door-pinned"); | |
| 853 | expect(names).not.toContain("mia/door-loose"); | |
| 854 | // The signed-out jump palette offers the same set. | |
| 855 | const body = await door(); | |
| 856 | const index = JSON.parse(body.match(/id="palette-index">([^<]*)</)![1]!); | |
| 857 | const offered = index.items.filter((i: any) => i.c === "Repositories").map((i: any) => i.t); | |
| 858 | expect(offered).toContain("mia/door-pinned"); | |
| 859 | expect(offered).not.toContain("mia/door-loose"); | |
| 860 | expect(offered.sort()).toEqual(names.filter(Boolean).sort()); | |
| 861 | // Still reachable at its own address. | |
| 862 | expect((await fetch(`${base}/mia/door-loose`)).status).toBe(200); | |
| 863 | expect((await put(adminToken, { pinned: [], hidden: [] })).status).toBe(200); | |
| 864 | void mine; void pinned; void loose; | |
| 865 | }); | |
| 839 | 866 | }); |
new above
offline