Files
microdao-daarion/migrations/006_create_passkey_tables.sql

149 lines
5.3 KiB
PL/PgSQL

-- migrations/006_create_passkey_tables.sql
-- Passkey (WebAuthn) Authentication Schema
-- Enable UUID extension if not already enabled
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================================================
-- Users Table (if not exists)
-- ============================================================================
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT UNIQUE,
username TEXT UNIQUE,
display_name TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ
);
-- ============================================================================
-- Passkeys Table (WebAuthn Credentials)
-- ============================================================================
CREATE TABLE IF NOT EXISTS passkeys (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
credential_id TEXT NOT NULL UNIQUE,
public_key TEXT NOT NULL,
sign_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_used_at TIMESTAMPTZ,
device_name TEXT,
transports TEXT[], -- USB, NFC, BLE, internal
aaguid TEXT,
attestation_format TEXT
);
CREATE INDEX IF NOT EXISTS idx_passkeys_user_id ON passkeys(user_id);
CREATE INDEX IF NOT EXISTS idx_passkeys_credential_id ON passkeys(credential_id);
CREATE INDEX IF NOT EXISTS idx_passkeys_last_used ON passkeys(last_used_at DESC);
-- ============================================================================
-- Sessions Table
-- ============================================================================
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
last_activity_at TIMESTAMPTZ,
user_agent TEXT,
ip_address TEXT
);
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
-- ============================================================================
-- Passkey Challenges (temporary, for WebAuthn flow)
-- ============================================================================
CREATE TABLE IF NOT EXISTS passkey_challenges (
challenge TEXT PRIMARY KEY,
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
email TEXT, -- For registration flow when user doesn't exist yet
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
challenge_type TEXT NOT NULL, -- 'register' or 'authenticate'
rp_id TEXT NOT NULL,
origin TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_passkey_challenges_expires_at ON passkey_challenges(expires_at);
CREATE INDEX IF NOT EXISTS idx_passkey_challenges_user_id ON passkey_challenges(user_id);
-- ============================================================================
-- User MicroDAO Memberships (for ActorIdentity)
-- ============================================================================
CREATE TABLE IF NOT EXISTS user_microdao_memberships (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
microdao_id TEXT NOT NULL,
role TEXT NOT NULL, -- 'owner', 'admin', 'member'
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
left_at TIMESTAMPTZ,
UNIQUE (user_id, microdao_id)
);
CREATE INDEX IF NOT EXISTS idx_memberships_user_id ON user_microdao_memberships(user_id);
CREATE INDEX IF NOT EXISTS idx_memberships_microdao_id ON user_microdao_memberships(microdao_id);
-- ============================================================================
-- Cleanup Function (remove expired challenges and sessions)
-- ============================================================================
CREATE OR REPLACE FUNCTION cleanup_expired_auth_data()
RETURNS void AS $$
BEGIN
DELETE FROM passkey_challenges WHERE expires_at < now();
DELETE FROM sessions WHERE expires_at < now();
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- Sample Data (for testing)
-- ============================================================================
-- Create test user
INSERT INTO users (id, email, username, display_name)
VALUES
('00000000-0000-0000-0000-000000000001', 'admin@daarion.city', 'admin', 'Admin User'),
('00000000-0000-0000-0000-000000000093', 'user93@daarion.city', 'user93', 'User 93')
ON CONFLICT (email) DO NOTHING;
-- Add microDAO memberships
INSERT INTO user_microdao_memberships (user_id, microdao_id, role)
VALUES
('00000000-0000-0000-0000-000000000001', 'microdao:daarion', 'owner'),
('00000000-0000-0000-0000-000000000093', 'microdao:7', 'owner'),
('00000000-0000-0000-0000-000000000093', 'microdao:daarion', 'member')
ON CONFLICT (user_id, microdao_id) DO NOTHING;
-- ============================================================================
-- Triggers for updated_at
-- ============================================================================
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_users_timestamp
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();