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
55
server/src/migrations/003_auth.sql
Normal file
55
server/src/migrations/003_auth.sql
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 003 AUTHENTICATION
|
||||
--
|
||||
-- Two tables: who may sign in, and who currently is signed in.
|
||||
--
|
||||
-- There is no self-signup and no registration endpoint. Accounts
|
||||
-- are created from the CLI, on the box, by someone with shell
|
||||
-- access. For a handful of staff that's the right trade: no
|
||||
-- invite flow, no email delivery, no password-reset surface for
|
||||
-- anyone to attack.
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE admin_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
|
||||
-- Stored lowercased. The application lowercases on every read
|
||||
-- and write, so the UNIQUE index is genuinely case-insensitive
|
||||
-- without depending on a collation.
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
name TEXT,
|
||||
|
||||
-- Nullable so a Google-only account can exist later with no
|
||||
-- password at all. A row with both can use either route in.
|
||||
password_hash TEXT,
|
||||
|
||||
-- Google's stable subject id. Nullable, unique when present —
|
||||
-- SQLite allows any number of NULLs in a unique index.
|
||||
google_sub TEXT UNIQUE,
|
||||
|
||||
role TEXT NOT NULL DEFAULT 'admin'
|
||||
CHECK (role IN ('admin', 'viewer')),
|
||||
is_active INTEGER NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1)),
|
||||
last_login_at TEXT
|
||||
) STRICT;
|
||||
|
||||
|
||||
-- One row per active login. The cookie holds a random token; this
|
||||
-- table holds only its SHA-256, so a database leak doesn't hand
|
||||
-- anyone a working session.
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
|
||||
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT NOT NULL,
|
||||
|
||||
user_agent TEXT,
|
||||
ip_hash TEXT
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX sessions_user_idx ON sessions (user_id);
|
||||
CREATE INDEX sessions_expiry_idx ON sessions (expires_at);
|
||||
7
server/src/migrations/004_award_org.sql
Normal file
7
server/src/migrations/004_award_org.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- 004_award_org.sql
|
||||
-- Who gave the award. SET NULL rather than CASCADE: retiring a
|
||||
-- partner org shouldn't erase an award people have received.
|
||||
ALTER TABLE awards
|
||||
ADD COLUMN org_id TEXT REFERENCES organizations (id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX awards_org_idx ON awards (org_id, sort_order);
|
||||
28
server/src/migrations/005_person_bio.sql
Normal file
28
server/src/migrations/005_person_bio.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- Person bio and primary organization
|
||||
--
|
||||
-- bio is one run of prose, not orderable mixed content, so it does
|
||||
-- not belong in content_blocks — whose owner_kind CHECK would need
|
||||
-- a full table rebuild to accept 'person' anyway. Paragraphs are
|
||||
-- blank-line separated and split at render time.
|
||||
--
|
||||
-- primary_org_id is nullable on purpose: plenty of people have no
|
||||
-- home organization worth printing, and ON DELETE SET NULL means
|
||||
-- deleting an org blanks the reference rather than blocking the
|
||||
-- delete or leaving a dangling id behind.
|
||||
--
|
||||
-- Check the current version before renumbering this file:
|
||||
-- PRAGMA user_version;
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE people ADD COLUMN bio TEXT;
|
||||
|
||||
-- SQLite requires an added REFERENCES column to default to NULL,
|
||||
-- which is what we want regardless.
|
||||
ALTER TABLE people ADD COLUMN primary_org_id TEXT
|
||||
REFERENCES organizations (id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS people_primary_org
|
||||
ON people (primary_org_id);
|
||||
|
||||
PRAGMA user_version = 0; -- ← set to this migration's number
|
||||
45
server/src/migrations/006_leadership_view.sql
Normal file
45
server/src/migrations/006_leadership_view.sql
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- v_org_leadership: add bio and primary organization
|
||||
--
|
||||
-- The view already carries the rules for who counts as current and
|
||||
-- public. Adding the two columns the people tiles need keeps those
|
||||
-- rules in one place instead of being restated by each route.
|
||||
--
|
||||
-- Additive only — attachLeadership does SELECT * and shapeLeader
|
||||
-- picks fields by name, so existing callers are unaffected.
|
||||
--
|
||||
-- PRAGMA user_version; -- check before renumbering this file
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
DROP VIEW IF EXISTS v_org_leadership;
|
||||
|
||||
CREATE VIEW v_org_leadership AS
|
||||
SELECT
|
||||
a.org_id,
|
||||
a.team_id,
|
||||
t.name AS team_name,
|
||||
t.sort_order AS team_sort_order,
|
||||
a.person_id,
|
||||
a.title,
|
||||
a.role,
|
||||
a.is_owner,
|
||||
a.sort_order,
|
||||
p.display_name,
|
||||
p.sort_name,
|
||||
p.pronouns,
|
||||
p.tagline,
|
||||
p.photo,
|
||||
p.public_email,
|
||||
p.location_label,
|
||||
p.bio,
|
||||
o.id AS primary_org_id,
|
||||
o.name AS primary_org_name
|
||||
FROM affiliations a
|
||||
JOIN people p ON p.id = a.person_id AND p.is_published = 1
|
||||
LEFT JOIN teams t ON t.id = a.team_id
|
||||
LEFT JOIN organizations o ON o.id = p.primary_org_id
|
||||
WHERE a.is_public = 1
|
||||
AND a.ended_on IS NULL
|
||||
ORDER BY a.org_id, a.is_owner DESC, a.sort_order, p.sort_name;
|
||||
|
||||
PRAGMA user_version = 0; -- ← set to this migration's number
|
||||
Loading…
Add table
Add a link
Reference in a new issue