@accounter/server
Version:
Accounter GraphQL server
169 lines (151 loc) • 11.3 kB
JavaScript
export default {
name: '2026-01-26T12-00-00.create-invitations-apikeys-audit-tables.sql',
run: ({ sql }) => sql `
-- Migration: Create Invitations, API Keys, and Audit Logs Tables
--
-- This migration creates tables for:
-- 1. Invitation management (Auth0 pre-registration flow)
-- 2. API key authentication (independent of Auth0 for automated processes)
-- 3. Audit logging (security and compliance monitoring)
--
-- Key Design Decisions:
-- 1. Invitations trigger Auth0 Management API calls to create blocked users
-- 2. Auth0 user is unblocked only after user sets password and accepts invitation
-- 3. API keys provide authentication independent of Auth0 for automated processes
-- 4. API keys use SHA-256 hashed storage for security
-- 5. Audit logs preserve records even after user/business deletion (ON DELETE SET NULL)
-- ========================================================================
-- TABLE: invitations
-- ========================================================================
-- Stores pending user invitations for Auth0 pre-registration flow.
-- Flow: invitation created → Auth0 user created (blocked) → user sets password
-- → accepts invitation → Auth0 user unblocked
CREATE TABLE accounter_schema.invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
business_id UUID NOT NULL REFERENCES accounter_schema.businesses_admin(id) ON DELETE CASCADE,
email TEXT NOT NULL,
role_id TEXT NOT NULL REFERENCES accounter_schema.roles(id) ON DELETE RESTRICT,
token TEXT UNIQUE NOT NULL,
auth0_user_created BOOLEAN NOT NULL DEFAULT FALSE,
auth0_user_id TEXT,
invited_by_user_id UUID,
invited_by_business_id UUID,
accepted_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE accounter_schema.invitations IS 'Pre-registration flow: invitation created → Auth0 user created (blocked) → user sets password → accepts invitation → Auth0 user unblocked';
COMMENT ON COLUMN accounter_schema.invitations.id IS 'Unique invitation identifier';
COMMENT ON COLUMN accounter_schema.invitations.business_id IS 'Business the user is being invited to';
COMMENT ON COLUMN accounter_schema.invitations.email IS 'Email address of the invited user';
COMMENT ON COLUMN accounter_schema.invitations.role_id IS 'Role to assign to the user upon acceptance';
COMMENT ON COLUMN accounter_schema.invitations.token IS 'Cryptographically secure 64-character random string for invitation URL';
COMMENT ON COLUMN accounter_schema.invitations.auth0_user_created IS 'Tracks whether Auth0 Management API call succeeded';
COMMENT ON COLUMN accounter_schema.invitations.auth0_user_id IS 'Auth0 user ID from pre-registration (e.g., ''auth0|507f1f77bcf86cd799439011''), used for cleanup';
COMMENT ON COLUMN accounter_schema.invitations.invited_by_user_id IS 'User ID of the admin who created this invitation';
COMMENT ON COLUMN accounter_schema.invitations.invited_by_business_id IS 'Business ID context of the admin who created this invitation (should match business_id)';
COMMENT ON COLUMN accounter_schema.invitations.accepted_at IS 'Timestamp when invitation was accepted (NULL until accepted, single-use token tracking)';
COMMENT ON COLUMN accounter_schema.invitations.expires_at IS 'Invitation expiration timestamp (typically 7 days from creation)';
COMMENT ON COLUMN accounter_schema.invitations.created_at IS 'Timestamp when invitation was created';
-- Create composite foreign key constraint for invited_by (user + business context)
-- This enforces that the inviter is a valid member of the business
ALTER TABLE accounter_schema.invitations
ADD CONSTRAINT invitations_invited_by_fkey
FOREIGN KEY (invited_by_user_id, invited_by_business_id)
REFERENCES accounter_schema.business_users(user_id, business_id)
ON DELETE SET NULL;
-- Create indexes for efficient lookups and cleanup queries
CREATE INDEX idx_invitations_business_id ON accounter_schema.invitations(business_id);
CREATE INDEX idx_invitations_expires_at ON accounter_schema.invitations(expires_at);
CREATE INDEX idx_invitations_email ON accounter_schema.invitations(email);
-- Ensure only one pending invitation exists per user per business
CREATE UNIQUE INDEX idx_invitations_unique_pending ON accounter_schema.invitations(business_id, email) WHERE accepted_at IS NULL;
-- ========================================================================
-- TABLE: api_keys
-- ========================================================================
-- Stores API keys for programmatic access independent of Auth0.
-- Used for automated processes like scrapers that need long-lived credentials.
CREATE TABLE accounter_schema.api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
business_id UUID NOT NULL REFERENCES accounter_schema.businesses_admin(id) ON DELETE CASCADE,
role_id TEXT NOT NULL REFERENCES accounter_schema.roles(id) ON DELETE RESTRICT,
key_hash TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
last_used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT api_keys_revoked_at_check CHECK (revoked_at IS NULL OR revoked_at > created_at)
);
COMMENT ON TABLE accounter_schema.api_keys IS 'API keys are independent of Auth0, used for programmatic access (e.g., scraper role)';
COMMENT ON COLUMN accounter_schema.api_keys.id IS 'Unique API key identifier';
COMMENT ON COLUMN accounter_schema.api_keys.business_id IS 'Business this API key belongs to';
COMMENT ON COLUMN accounter_schema.api_keys.role_id IS 'Role assigned to this API key (e.g., ''scraper'')';
COMMENT ON COLUMN accounter_schema.api_keys.key_hash IS 'SHA-256 hash of the API key (never store plaintext keys)';
COMMENT ON COLUMN accounter_schema.api_keys.name IS 'Descriptive name for this API key (e.g., ''Production Scraper'')';
COMMENT ON COLUMN accounter_schema.api_keys.last_used_at IS 'Timestamp of last API key usage (updated hourly to prevent write amplification)';
COMMENT ON COLUMN accounter_schema.api_keys.revoked_at IS 'Timestamp when API key was revoked (NULL if active)';
COMMENT ON COLUMN accounter_schema.api_keys.created_at IS 'Timestamp when API key was created';
-- Create indexes for efficient lookups
CREATE INDEX idx_api_keys_business_id ON accounter_schema.api_keys(business_id);
-- ========================================================================
-- TABLE: api_key_permission_overrides
-- ========================================================================
-- Stores API-key-specific permission grants/revokes.
-- NOT ENFORCED INITIALLY - prepared for future granular permission system.
CREATE TABLE accounter_schema.api_key_permission_overrides (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
api_key_id UUID NOT NULL REFERENCES accounter_schema.api_keys(id) ON DELETE CASCADE,
permission_id TEXT NOT NULL REFERENCES accounter_schema.permissions(id) ON DELETE CASCADE,
grant_type accounter_schema.grant_type_enum NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (api_key_id, permission_id)
);
COMMENT ON TABLE accounter_schema.api_key_permission_overrides IS 'API-key-specific permission overrides (future use - not initially enforced)';
COMMENT ON COLUMN accounter_schema.api_key_permission_overrides.id IS 'Unique override identifier';
COMMENT ON COLUMN accounter_schema.api_key_permission_overrides.api_key_id IS 'API key this override applies to';
COMMENT ON COLUMN accounter_schema.api_key_permission_overrides.permission_id IS 'Permission being granted or revoked';
COMMENT ON COLUMN accounter_schema.api_key_permission_overrides.grant_type IS 'Whether this is a grant or revoke override';
-- ========================================================================
-- TABLE: audit_logs
-- ========================================================================
-- Stores a trail of critical actions for security and compliance monitoring.
-- Records user actions, system events, and security-relevant operations.
CREATE TABLE accounter_schema.audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
business_id UUID REFERENCES accounter_schema.businesses_admin(id) ON DELETE SET NULL,
user_id UUID,
auth0_user_id TEXT,
action TEXT NOT NULL,
entity TEXT,
entity_id TEXT,
details JSONB,
ip_address INET,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE accounter_schema.audit_logs IS 'Audit trail for critical security and business actions (e.g., login, user creation, sensitive data access)';
COMMENT ON COLUMN accounter_schema.audit_logs.id IS 'Unique audit log entry identifier';
COMMENT ON COLUMN accounter_schema.audit_logs.business_id IS 'Business associated with this action (NULL for system-wide actions or failed logins)';
COMMENT ON COLUMN accounter_schema.audit_logs.user_id IS 'Local user identifier (from business_users table) who performed the action';
COMMENT ON COLUMN accounter_schema.audit_logs.auth0_user_id IS 'Auth0 user identifier for audit trail (e.g., ''auth0|507f1f77bcf86cd799439011'')';
COMMENT ON COLUMN accounter_schema.audit_logs.action IS 'Action type (e.g., ''USER_LOGIN'', ''INVOICE_UPDATE'', ''PERMISSION_CHANGE'')';
COMMENT ON COLUMN accounter_schema.audit_logs.entity IS 'Entity type affected by the action (e.g., ''Invoice'', ''User'', ''Document'')';
COMMENT ON COLUMN accounter_schema.audit_logs.entity_id IS 'Identifier of the affected entity';
COMMENT ON COLUMN accounter_schema.audit_logs.details IS 'Additional action metadata (e.g., before/after state, error details)';
COMMENT ON COLUMN accounter_schema.audit_logs.ip_address IS 'IP address of the client that initiated the action';
COMMENT ON COLUMN accounter_schema.audit_logs.created_at IS 'Timestamp when the action occurred';
-- Create indexes for efficient audit log queries
CREATE INDEX idx_audit_logs_business_id_created_at ON accounter_schema.audit_logs(business_id, created_at DESC);
CREATE INDEX idx_audit_logs_user_id ON accounter_schema.audit_logs(user_id);
CREATE INDEX idx_audit_logs_action ON accounter_schema.audit_logs(action);
CREATE INDEX idx_audit_logs_created_at ON accounter_schema.audit_logs(created_at DESC);
-- Create composite foreign key constraint for user + business context
-- This enforces that the user is a valid member of the business for business-scoped actions
-- If business_id is NULL (system actions), the constraint is not enforced (MATCH SIMPLE default)
ALTER TABLE accounter_schema.audit_logs
ADD CONSTRAINT audit_logs_user_business_fkey
FOREIGN KEY (user_id, business_id)
REFERENCES accounter_schema.business_users(user_id, business_id)
ON DELETE SET NULL;
`,
};
//# sourceMappingURL=2026-01-26T12-00-00.create-invitations-apikeys-tables.js.map