UNPKG

@accounter/server

Version:
169 lines (151 loc) 11.3 kB
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