create extension if not exists pgcrypto; create extension if not exists citext; create table if not exists roles ( id uuid primary key default gen_random_uuid(), name text unique not null, description text not null default '', created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists users ( id uuid primary key default gen_random_uuid(), role_id uuid not null references roles(id), username citext unique not null, display_name text not null, email citext unique, password_hash text not null, is_active boolean not null default true, last_login_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), deleted_at timestamptz ); create table if not exists sessions ( id uuid primary key default gen_random_uuid(), user_id uuid not null references users(id), ip_address inet, user_agent text, last_seen_at timestamptz not null default now(), expires_at timestamptz not null, created_at timestamptz not null default now(), revoked_at timestamptz ); create table if not exists refresh_tokens ( id uuid primary key default gen_random_uuid(), session_id uuid not null references sessions(id), user_id uuid not null references users(id), token_hash text not null, expires_at timestamptz not null, created_at timestamptz not null default now(), revoked_at timestamptz ); create table if not exists gateways ( id uuid primary key default gen_random_uuid(), name text unique not null, endpoint text not null, public_key text not null, listen_port integer not null, vpn_cidr cidr not null, dns_servers text[] not null default '{}', is_active boolean not null default true, last_sync_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), deleted_at timestamptz ); create table if not exists devices ( id uuid primary key default gen_random_uuid(), user_id uuid not null references users(id), gateway_id uuid references gateways(id), name text not null, platform text not null, os_version text not null default '', app_version text not null default '', device_fingerprint text not null, public_key text not null, status text not null default 'active', last_seen_at timestamptz, last_connected_at timestamptz, approved_at timestamptz, revoked_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), deleted_at timestamptz ); create unique index if not exists idx_devices_user_fingerprint on devices(user_id, device_fingerprint) where deleted_at is null; create table if not exists wireguard_peers ( id uuid primary key default gen_random_uuid(), device_id uuid not null references devices(id), gateway_id uuid not null references gateways(id), public_key text unique not null, assigned_ip inet not null, preshared_key_ciphertext text, allowed_ips cidr[] not null default '{}', dns_servers text[] not null default '{}', profile_revision integer not null default 1, last_profile_issued_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), deleted_at timestamptz ); create table if not exists ip_allocations ( id uuid primary key default gen_random_uuid(), gateway_id uuid not null references gateways(id), device_id uuid references devices(id), address inet not null, status text not null default 'allocated', allocated_at timestamptz not null default now(), released_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create unique index if not exists idx_ip_allocations_gateway_address on ip_allocations(gateway_id, address); create unique index if not exists idx_ip_allocations_device_active on ip_allocations(device_id) where status = 'allocated'; create table if not exists policies ( id uuid primary key default gen_random_uuid(), name text unique not null, description text not null default '', priority integer not null default 100, effect text not null default 'allow', is_active boolean not null default true, full_tunnel boolean not null default false, created_by uuid references users(id), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), deleted_at timestamptz ); create table if not exists policy_targets ( id uuid primary key default gen_random_uuid(), policy_id uuid not null references policies(id), target_type text not null, target_id uuid not null, created_at timestamptz not null default now() ); create table if not exists policy_destinations ( id uuid primary key default gen_random_uuid(), policy_id uuid not null references policies(id), destination cidr not null, description text not null default '', created_at timestamptz not null default now() ); create table if not exists groups ( id uuid primary key default gen_random_uuid(), name text unique not null, description text not null default '', created_at timestamptz not null default now(), updated_at timestamptz not null default now(), deleted_at timestamptz ); create table if not exists group_memberships ( id uuid primary key default gen_random_uuid(), group_id uuid not null references groups(id), user_id uuid not null references users(id), created_at timestamptz not null default now() ); create table if not exists audit_logs ( id uuid primary key default gen_random_uuid(), actor_user_id uuid references users(id), actor_device_id uuid references devices(id), event_type text not null, entity_type text not null, entity_id uuid, status text not null, ip_address inet, message text not null, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now() ); create table if not exists settings ( id uuid primary key default gen_random_uuid(), category text not null, key text not null, value jsonb not null, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), unique(category, key) );