Files
Redflag/aggregator-server/internal/database/migrations/012_add_token_seats.up.sql
jpetree331 ec0d880036 fix(database): B-1 schema integrity and migration fixes
- Fix migration 024 self-insert and bad column reference (F-B1-1, F-B1-2)
  Uses existing enabled/auto_run columns instead of non-existent deprecated
- Abort server on migration failure instead of warning (F-B1-11)
  main.go now calls log.Fatalf, prints [INFO] only on success
- Fix migration 018 scanner_config filename suffix (F-B1-3)
  Renumbered to 027 with .up.sql suffix
- Remove GRANT to non-existent role in scanner_config (F-B1-4)
- Resolve duplicate migration numbers 009 and 012 (F-B1-13)
  Renamed to 009b and 012b for unique lexical sorting
- Add IF NOT EXISTS to all non-idempotent migrations (F-B1-15)
  Fixed: 011, 012, 017, 023, 023a
- Replace N+1 dashboard stats loop with GetAllUpdateStats (F-B1-6)
  Single aggregate query replaces per-agent loop
- Add composite index on agent_commands(status, sent_at) (F-B1-5)
  New migration 028 with partial index for timeout service
- Add background refresh token cleanup goroutine (F-B1-10)
  24-hour ticker calls CleanupExpiredTokens
- ETHOS log format in migration runner (no emojis)

All 55 tests pass (41 server + 14 agent). No regressions.
See docs/B1_Fix_Implementation.md and DEV-025 through DEV-028.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 07:03:35 -04:00

120 lines
4.3 KiB
PL/PgSQL

-- Add seat tracking to registration tokens for multi-use support
-- This allows tokens to be used multiple times up to a configured limit
-- Add seats columns
ALTER TABLE registration_tokens
ADD COLUMN IF NOT EXISTS max_seats INT NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS seats_used INT NOT NULL DEFAULT 0;
-- Backfill existing tokens
-- Tokens with status='used' should have seats_used=1, max_seats=1
UPDATE registration_tokens
SET seats_used = 1,
max_seats = 1
WHERE status = 'used';
-- Active/expired/revoked tokens get max_seats=1, seats_used=0
UPDATE registration_tokens
SET seats_used = 0,
max_seats = 1
WHERE status IN ('active', 'expired', 'revoked');
-- Add constraint to ensure seats_used doesn't exceed max_seats
ALTER TABLE registration_tokens
ADD CONSTRAINT chk_seats_used_within_max
CHECK (seats_used <= max_seats);
-- Add constraint to ensure positive seat values
ALTER TABLE registration_tokens
ADD CONSTRAINT chk_seats_positive
CHECK (max_seats > 0 AND seats_used >= 0);
-- Create table to track all agents that used a token (for audit trail)
CREATE TABLE IF NOT EXISTS registration_token_usage (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token_id UUID NOT NULL REFERENCES registration_tokens(id) ON DELETE CASCADE,
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
used_at TIMESTAMP DEFAULT NOW(),
UNIQUE(token_id, agent_id)
);
CREATE INDEX IF NOT EXISTS idx_token_usage_token_id ON registration_token_usage(token_id);
CREATE INDEX IF NOT EXISTS idx_token_usage_agent_id ON registration_token_usage(agent_id);
-- Backfill token usage table from existing used_by_agent_id
INSERT INTO registration_token_usage (token_id, agent_id, used_at)
SELECT id, used_by_agent_id, used_at
FROM registration_tokens
WHERE used_by_agent_id IS NOT NULL
ON CONFLICT (token_id, agent_id) DO NOTHING;
-- Update is_registration_token_valid function to check seats
CREATE OR REPLACE FUNCTION is_registration_token_valid(token_input VARCHAR)
RETURNS BOOLEAN AS $$
DECLARE
token_valid BOOLEAN;
BEGIN
SELECT (status = 'active' AND expires_at > NOW() AND seats_used < max_seats) INTO token_valid
FROM registration_tokens
WHERE token = token_input;
RETURN COALESCE(token_valid, FALSE);
END;
$$ LANGUAGE plpgsql;
-- Update mark_registration_token_used function to increment seats
DROP FUNCTION IF EXISTS mark_registration_token_used(VARCHAR, UUID);
CREATE FUNCTION mark_registration_token_used(token_input VARCHAR, agent_id_param UUID)
RETURNS BOOLEAN AS $$
DECLARE
rows_updated INTEGER; -- Fixed: Changed from BOOLEAN to INTEGER to match ROW_COUNT type
token_id_val UUID;
new_seats_used INT;
token_max_seats INT;
BEGIN
-- Get token ID and current seat info
SELECT id, seats_used + 1, max_seats INTO token_id_val, new_seats_used, token_max_seats
FROM registration_tokens
WHERE token = token_input
AND status = 'active'
AND expires_at > NOW()
AND seats_used < max_seats;
-- If no token found or already full, return false
IF token_id_val IS NULL THEN
RETURN FALSE;
END IF;
-- Increment seats_used
UPDATE registration_tokens
SET seats_used = new_seats_used,
used_at = CASE
WHEN used_at IS NULL THEN NOW() -- First use
ELSE used_at -- Keep original first use time
END,
-- Only mark as 'used' if all seats are now taken
status = CASE
WHEN new_seats_used >= token_max_seats THEN 'used'
ELSE 'active'
END
WHERE token = token_input
AND status = 'active';
GET DIAGNOSTICS rows_updated = ROW_COUNT;
-- Record this usage in the audit table
IF rows_updated > 0 THEN
INSERT INTO registration_token_usage (token_id, agent_id, used_at)
VALUES (token_id_val, agent_id_param, NOW())
ON CONFLICT (token_id, agent_id) DO NOTHING;
END IF;
RETURN rows_updated > 0;
END;
$$ LANGUAGE plpgsql;
-- Add comment for documentation
COMMENT ON COLUMN registration_tokens.max_seats IS 'Maximum number of agents that can register with this token';
COMMENT ON COLUMN registration_tokens.seats_used IS 'Number of agents that have registered with this token';
COMMENT ON TABLE registration_token_usage IS 'Audit trail of all agents registered with each token';