v1.4 - added admin page and auth
This commit is contained in:
parent
5efdafbb97
commit
1f0aa3078f
29 changed files with 5264 additions and 217 deletions
141
server/src/routes/people.js
Normal file
141
server/src/routes/people.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/* ═══════════════════════════════════════════════════════════════
|
||||
PEOPLE ROUTES — read-only, mounted under /api
|
||||
|
||||
GET /teams/:id/people current public members of a team
|
||||
GET /people?ids=a,b,c named people, any order
|
||||
|
||||
The team route reads v_org_leadership, which already decides who
|
||||
counts as current and public — affiliation still open, marked
|
||||
public, person published. Restating those conditions here is how
|
||||
they drift apart.
|
||||
|
||||
That view is affiliation-driven, so it can't serve the lookup
|
||||
route: a person with no affiliations would vanish, and one with
|
||||
several would appear more than once. The lookup reads people
|
||||
directly and returns no title, because a bare slug names no seat.
|
||||
|
||||
Photos are filenames, as everywhere else in this API. The
|
||||
component decides where they live.
|
||||
═══════════════════════════════════════════════════════════════ */
|
||||
|
||||
import { Hono } from "hono";
|
||||
|
||||
import { asBool } from "../shape.js";
|
||||
|
||||
const people = new Hono();
|
||||
|
||||
const CACHE = "public, max-age=60, stale-while-revalidate=300";
|
||||
|
||||
const json = (c, body) => c.json(body, 200, { "Cache-Control": CACHE });
|
||||
|
||||
const marks = (n) => Array(n).fill("?").join(",");
|
||||
|
||||
/* How many slugs one request may name. Keeps the URL sane. */
|
||||
const MAX_IDS = 50;
|
||||
|
||||
function shapePerson(row) {
|
||||
return {
|
||||
id: row.id ?? row.person_id,
|
||||
name: row.display_name,
|
||||
// Only the team route knows a seat; the lookup route doesn't.
|
||||
title: row.title ?? null,
|
||||
tagline: row.tagline,
|
||||
pronouns: row.pronouns,
|
||||
photo: row.photo,
|
||||
location_label: row.location_label,
|
||||
public_email: row.public_email,
|
||||
org: row.primary_org_id
|
||||
? { id: row.primary_org_id, name: row.primary_org_name }
|
||||
: null,
|
||||
bio: splitParagraphs(row.bio),
|
||||
role: row.role ?? null,
|
||||
is_owner: row.is_owner === undefined ? false : asBool(row.is_owner),
|
||||
};
|
||||
}
|
||||
|
||||
/* people.bio is one run of prose with blank lines between
|
||||
paragraphs — unrelated to shape.js's paragraphs(), which turns
|
||||
content_block rows into text. */
|
||||
function splitParagraphs(text) {
|
||||
if (!text) return null;
|
||||
const parts = text
|
||||
.split(/\n\s*\n/)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
return parts.length ? parts : null;
|
||||
}
|
||||
|
||||
/* ── One team's people ─────────────────────────────────────── */
|
||||
|
||||
people.get("/teams/:id/people", (c) => {
|
||||
const db = c.get("db");
|
||||
const id = c.req.param("id");
|
||||
|
||||
const team = db
|
||||
.prepare(
|
||||
`SELECT id, org_id, name, tagline, color, logo
|
||||
FROM teams
|
||||
WHERE id = ? AND is_published = 1`,
|
||||
)
|
||||
.get(id);
|
||||
|
||||
if (!team) return c.json({ error: "No such team" }, 404);
|
||||
|
||||
// The view orders by org first, which isn't what a single team
|
||||
// wants, so the order is restated here.
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT * FROM v_org_leadership
|
||||
WHERE team_id = ?
|
||||
ORDER BY is_owner DESC,
|
||||
sort_order,
|
||||
COALESCE(sort_name, display_name),
|
||||
display_name`,
|
||||
)
|
||||
.all(id);
|
||||
|
||||
return json(c, { team, people: rows.map(shapePerson) });
|
||||
});
|
||||
|
||||
/* ── People by id ──────────────────────────────────────────────
|
||||
For hand-picked lists in a section. Order is the caller's, so
|
||||
there's no ORDER BY here.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
people.get("/people", (c) => {
|
||||
const db = c.get("db");
|
||||
|
||||
const ids = [
|
||||
...new Set(
|
||||
(c.req.query("ids") ?? "")
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
].slice(0, MAX_IDS);
|
||||
|
||||
if (ids.length === 0) return json(c, { people: [] });
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT p.id,
|
||||
p.display_name,
|
||||
p.pronouns,
|
||||
p.photo,
|
||||
p.tagline,
|
||||
p.location_label,
|
||||
p.public_email,
|
||||
p.bio,
|
||||
o.id AS primary_org_id,
|
||||
o.name AS primary_org_name
|
||||
FROM people p
|
||||
LEFT JOIN organizations o ON o.id = p.primary_org_id
|
||||
WHERE p.id IN (${marks(ids.length)})
|
||||
AND p.is_published = 1`,
|
||||
)
|
||||
.all(...ids);
|
||||
|
||||
return json(c, { people: rows.map(shapePerson) });
|
||||
});
|
||||
|
||||
export default people;
|
||||
Loading…
Add table
Add a link
Reference in a new issue