A person was in exactly one project, chosen at approval, and could not use the gateway before that. People belong to several teams and to none, so: - memberships: a person is a member of any number of organizations, teams and projects, or of none; a member of a team or organization may use every project in it. users join/leave, and the people page adds and takes out from either side. Approval no longer needs a project. - personal keys: a key may have an owner and no project. /profile asks what a new key spends against; keys create --for does the same. auth.personal sets default limits for personal use or turns it off. - limits for people: --scope user holds a person across every key, project and the chat page; --per-member on an org, team or project gives each member their own share there. Resolution folds the tree, each per-member limit for the key's owner, and the owner's own, each in its own pool; the quotas page shows and sets both. - the chat page asks what to spend against and checks it per message. - usage records who spent it, so a person's budget survives a restart, personal use and chat included; restore no longer drops rows with no project. Leaving a project revokes the keys made there unless another membership still reaches it. A personal key keeps no threads or notes, which are per project, and is refused rather than dropped into the anonymous pool. Migration 0010 turns each person's project into a membership, adds usage.user_id and backfills it, and makes api_keys.project_id nullable. SQLite cannot drop NOT NULL, so the table is rebuilt; that needs foreign keys off, which only works outside a transaction, so migrations marked for it run on one connection with foreign_key_check before commit. Tested on a populated SQLite store with claims, and on a copy of the cluster's PostgreSQL store: 5 people became members, 17 keys and 3 claims intact, 145 of 577 usage rows and all 69 chat rows attributed. The PostgreSQL store tests, including new membership cases, pass on 16. users move is gone; join and leave replace it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
61 lines
2.8 KiB
SQL
61 lines
2.8 KiB
SQL
-- A person could be in exactly one project, chosen once at approval, and could
|
|
-- not use the gateway at all before that. People belong to several teams and
|
|
-- to none, so membership becomes its own table and a key need not have a
|
|
-- project: a personal key is owned by a person and bounded by that person's
|
|
-- own limits.
|
|
|
|
-- Membership at any level of the tree; a member of a team may use every
|
|
-- project in it. Role is 'member' today; 'manager' is reserved for delegated
|
|
-- administration of the scope.
|
|
CREATE TABLE memberships (
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
scope_type TEXT NOT NULL,
|
|
scope_id TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'member',
|
|
added_at INTEGER NOT NULL,
|
|
added_by TEXT,
|
|
PRIMARY KEY (user_id, scope_type, scope_id)
|
|
);
|
|
CREATE INDEX memberships_by_scope ON memberships(scope_type, scope_id);
|
|
|
|
-- Everybody keeps the project they had.
|
|
INSERT INTO memberships (user_id, scope_type, scope_id, role, added_at, added_by)
|
|
SELECT id, 'project', project_id, 'member', COALESCE(approved_at, created_at), approved_by
|
|
FROM users WHERE project_id IS NOT NULL;
|
|
|
|
-- A personal key has an owner and no project. SQLite cannot drop NOT NULL
|
|
-- from a column, so the table is rebuilt — with foreign keys off while it is,
|
|
-- as SQLite's own procedure for this requires, and checked before commit.
|
|
-- migrate: foreign_keys=off
|
|
CREATE TABLE api_keys_new (
|
|
id TEXT PRIMARY KEY,
|
|
key_hash BLOB NOT NULL UNIQUE,
|
|
key_prefix TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
project_id TEXT REFERENCES projects(id),
|
|
band TEXT,
|
|
routing_profile TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
expires_at INTEGER,
|
|
revoked_at INTEGER,
|
|
last_used_at INTEGER,
|
|
user_id TEXT REFERENCES users(id)
|
|
);
|
|
INSERT INTO api_keys_new (id, key_hash, key_prefix, name, project_id, band, routing_profile,
|
|
created_at, expires_at, revoked_at, last_used_at, user_id)
|
|
SELECT id, key_hash, key_prefix, name, project_id, band, routing_profile,
|
|
created_at, expires_at, revoked_at, last_used_at, user_id
|
|
FROM api_keys;
|
|
DROP TABLE api_keys;
|
|
ALTER TABLE api_keys_new RENAME TO api_keys;
|
|
CREATE INDEX api_keys_by_project ON api_keys(project_id);
|
|
CREATE INDEX api_keys_by_user ON api_keys(user_id);
|
|
|
|
-- Who spent it, recorded with the spend. Deriving it from the key at read time
|
|
-- counted a key's past against whoever owns it now, and could not attribute
|
|
-- the chat page's requests, which have no key row at all.
|
|
ALTER TABLE usage ADD COLUMN user_id TEXT NOT NULL DEFAULT '';
|
|
UPDATE usage SET user_id = COALESCE((SELECT k.user_id FROM api_keys k WHERE k.id = usage.key_id), '');
|
|
UPDATE usage SET user_id = substr(key_id, 9) WHERE key_id LIKE 'session:%';
|
|
CREATE INDEX usage_by_user_ts ON usage(user_id, ts);
|